public inbox for [email protected]
help / color / mirror / Atom feedFrom: Tatsuo Ishii <[email protected]>
To: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Subject: Re: Row pattern recognition
Date: Sun, 15 Feb 2026 18:06:52 +0900 (JST)
Message-ID: <[email protected]> (raw)
In-Reply-To: <CAAAe_zB+W+xBJpYNzRLzh6BH9HrFycTjoPUnVvnU1BT_7RR8Bw@mail.gmail.com>
References: <CAAAe_zAzOgB2KR9ACDD2o3QNP_gaCKnUd2bRGgbhcD=og50XXA@mail.gmail.com>
<[email protected]>
<CAAAe_zB+W+xBJpYNzRLzh6BH9HrFycTjoPUnVvnU1BT_7RR8Bw@mail.gmail.com>
Hi Henson,
Thanks for the patches! I applied them on top of v42 and rebased
against current master. Attached are the v43 patches.
> Hi Tatsuo,
>
> This round focused on reviewing nodeWindowAgg.c -- the NFA executor
> -- which was the last piece remaining on my review list. I've also
> added a comprehensive NFA runtime test suite.
>
> The attached patch continues the incremental series on top of v42.
> Here are the changes:
>
>
> FIXME issues documented:
>
> These require non-trivial structural changes, so I've documented
> them as FIXME comments for now rather than attempting a fix.
>
> - altPriority tracks only the last ALT choice, so repeated or nested
> ALTs like (A|B)+ cannot correctly implement SQL standard lexical
> ordering. A full-path classifier structure is needed.
>
> - Cycle prevention condition (count == 0 && min == 0) is insufficient
> for patterns like (A*)* where cycles occur at count > 0. Currently
> relies on implicit duplicate detection in nfa_add_state_unique.
>
>
> Bugs fixed:
>
> - Fix nfa_advance_begin() routing order: enter group first (lexically
> first), then skip path (lexically second).
>
> - Add ALT scope boundary check in nfa_advance_alt() and
> computeAbsorbabilityRecursive() to stop branch traversal when
> element depth exits ALT scope.
>
> - Disallow RANGE and GROUPS frame types with row pattern
> recognition, as the SQL standard only requires ROWS.
> Also remove the now-dead frame-type determination code.
>
>
> NFA executor refactoring (nodeWindowAgg.c):
>
> - Simplify nfa_process_row() phase 1: remove nfa_advance() call
> after forced mismatch at frame boundary, since all states are
> already at VAR positions and mismatch removes them. Replace
> the redundant phase 3 frame boundary check with Assert.
>
> - Simplify update_reduced_frame() result registration: mismatch
> path now returns early, and the post-processing SKIP mode cleanup
> block (nfa_remove_contexts_up_to vs nfa_context_free) is removed
> since eager pruning handles SKIP PAST LAST ROW and both paths
> now simply call nfa_context_free().
>
> - Replace nfa_remove_contexts_up_to() with eager pruning inside
> nfa_add_matched_state(). When matchEndRow is extended during
> greedy matching, pending contexts within the match range are
> pruned incrementally instead of after match completion. For
> SKIP PAST LAST ROW patterns like START UP+, this reduces the
> number of live contexts at each row from O(n) to O(1), avoiding
> O(n^2) per-row processing of contexts that would be skipped
> anyway.
>
> - Optimize nfa_states_equal() to compare counts only up to the
> current element's depth instead of the full maxDepth.
>
> - Rename nfa_state_clone -> nfa_state_create,
> nfa_find_context_for_pos -> nfa_get_head_context for clarity.
>
> - Add nfa_record_context_success/skipped/absorbed statistics helpers.
>
> - Remove unused RPRPattern parameter from
> nfa_update_absorption_flags().
>
>
> Absorbability refactoring (rpr.c):
>
> - Remove parentDepth parameter from isUnboundedStart() and
> computeAbsorbabilityRecursive(). Scope boundaries are now
> determined by element depth comparison instead of an explicit
> parent depth parameter.
>
> - Simplify isUnboundedStart(): check simple VAR case first, then
> GROUP case with a depth-bounded loop instead of a FIN-terminated
> traversal with multiple break conditions.
>
>
> Test changes:
>
> - Add rpr_nfa.sql: comprehensive NFA runtime test suite covering
> quantifier boundaries (min/max/exact), alternation priority, nested
> patterns, frame boundary variations, INITIAL mode (syntax error
> expected), pathological patterns, context absorption, and FIXME
> reproduction cases.
>
> - Add nth_value out-of-frame tests, ReScan/LATERAL test, and
> nth_value IGNORE NULLS test to rpr.sql.
>
> - Change selected EXPLAIN test queries in rpr_explain.sql from
> EXPLAIN (COSTS OFF) to EXPLAIN (ANALYZE, ...) to verify actual
> NFA execution statistics.
>
> - Fix stale comments across rpr.sql, rpr_base.sql, and rpr_nfa.sql:
> remove resolved BUG annotations, update error messages to match
> actual output, correct optimization result descriptions, and
> standardize Expected comment placement to after SQL statements.
>
>
> Other changes:
>
> - Run pgindent on RPR source files. Add /*----------*/ comment
> guards to protect structured comments from reformatting.
>
> Coverage:
>
> I ran gcov on the modified lines (diff-only coverage). The attached
> coverage.zip contains an HTML report. Summary:
>
> - 18 files, 124 functions, 2238 modified lines analyzed
> - Overall: 92.1% line coverage (2061/2238)
> - Core files (nodeWindowAgg.c, rpr.c, explain.c, parse_rpr.c):
> 98.0-98.5% each
> - Node serialization (outfuncs.c, readfuncs.c, equalfuncs.c):
> 0% -- these implement RPRPattern serialization for plan caching,
> but regression tests don't exercise prepared statements with RPR
> - ruleutils.c: 96.3% -- untested lines are the reluctant quantifier
> display path ({1}?), which is currently rejected at parse time
Thanks for the report.
> The node serialization functions (141 lines, 0% coverage) are the
> largest untested area. I'm not sure how to trigger these paths
> in the regression test framework. Any suggestions?
I think we can leave it as it is, until reluctant quantifier is
implemented.
> I'll send a separate email within a few days listing the FIXME
> issues and other unresolved items from the mailing list discussion
> for your review.
Looking forward to reading your email.
Best regards,
--
Tatsuo Ishii
SRA OSS K.K.
English: http://www.sraoss.co.jp/index_en/
Japanese:http://www.sraoss.co.jp
Attachments:
[application/octet-stream] v43-0001-Row-pattern-recognition-patch-for-raw-parser.patch (32.2K, 2-v43-0001-Row-pattern-recognition-patch-for-raw-parser.patch)
download | inline diff:
From 3dba60529bdebbd2d118bd05347eb863109ffce9 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sun, 15 Feb 2026 17:47:47 +0900
Subject: [PATCH v43 1/8] Row pattern recognition patch for raw parser.
The series of patches are to implement the row pattern recognition
(SQL/RPR) feature. Currently the implementation is a subset of SQL/RPR
(ISO/IEC 19075-2:2016). Namely, implementation of some features of
R020 (WINDOW clause). R010 (MATCH_RECOGNIZE) is out of the scope of
the patches.
Currently following features are implemented in the patches.
- PATTERN
- PATTERN regular expressions (+, *, ?)
alternation (|), grouping () , {n}, {n,}, {n,m}, {,m}
- DEFINE
- INITIAL
- AFTER MATCH SKIP TO PAST LAST ROW
- AFTER MATCH SKIP TO NEXT ROW
Currently following features are not implemented in the patches.
- MEASURE
- SUBSET
- SEEK
- AFTER MATCH SKIP TO
- AFTER MATCH SKIP TO FIRST
- AFTER MATCH SKIP TO LAST
- PATTERN regular expression reluctant quantifiers
(*? etc.), {- and -}, () (empty pattern)
Anchores (^, $) are not permitted with RPR in Window clause by the
standard.
- PERMUTE
- FIRST, LAST, CLASSIFIER
Author: Tatsuo Ishii <[email protected]>
Author: Henson Choi <[email protected]>
Reviewed-by: Vik Fearing <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Reviewed-by: Peter Eisentraut <[email protected]>
Reviewed-by: NINGWEI CHEN <[email protected]>
Reviewed-by: "David G. Johnston" <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Henson Choi <[email protected]>
Reviewed-by: Tatsuo Ishii <[email protected]>
Discussion: https://postgr.es/m/20230625.210509.1276733411677577841.t-ishii%40sranhm.sra.co.jp
---- Major differences from v41 patches ----
- Add Peter as a reviewer to commit message. He gave a good valuable
suggestions to the gram.y patch. I accidentally forgot to credit his
name.
- Tons of enhancements from Henson.
-- Fully rewrite absorption logic (optimizer/plan/rpr.c).
Example: (a+ b* c?){2,} -> (a+" b* c?){2,}
-- Add safety check to tryMultiplyQuantifiers (optimizer/plan/rpr.c)
Example: (A{2}){2,3} cannot be rewritten to A{4,6} because it allows
5 times reputation which is not correct. Perform multiplication
optimization only in safe case.
-- Fix infinite loop/segfault when unlimited quantifiers (e.g. (A*)*,
(A+)+) are used.
-- Optimize unlimited quantifiers if possible (e.g. (A*)* -> A*, (A+)+ -> A+)
-- Fix segfault case (SELECT id, flag ...WINDOW w AS (... DEFINE T AS flag)
-- More accurate error position report
-- Some code refactoring in parser/planner patches
-- Split regression test file into rpr.sql and rpr_base.sql
-- Fix outfuncs.c and readfuncs.c
-- Fix some corner cases bug of regular expression optimization
-- Remove unused PATTERN variable to enhance performance
-- Change treatment of undefined DEFINE variables. Previously if
variable "A" is used in PATTERN but not defined in DEFINE, A is
automatically defined as "A AS TRUE". Now A is evaluated as TRUE in
executor and the generated entry for A is removed. This improves
memory usage.
-- EXPLAIN now shows the absorbable points (a+")
- Bug fix from me.
-- Assertion failure when two or more window clauses are used.
---
src/backend/parser/gram.y | 425 ++++++++++++++++++++++++++++++--
src/backend/parser/scan.l | 4 +-
src/include/nodes/parsenodes.h | 67 +++++
src/include/parser/kwlist.h | 5 +
src/include/parser/parse_node.h | 1 +
5 files changed, 483 insertions(+), 19 deletions(-)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c567252acc4..e6ae23f368b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -209,6 +209,8 @@ static void preprocess_pub_all_objtype_list(List *all_objects_list,
static void preprocess_pubobj_list(List *pubobjspec_list,
core_yyscan_t yyscanner);
static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
+static RPRPatternNode *makeRPRQuantifier(int min, int max, ParseLoc reluctant, int location,
+ core_yyscan_t yyscanner);
%}
@@ -682,6 +684,15 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
json_object_constructor_null_clause_opt
json_array_constructor_null_clause_opt
+%type <target> row_pattern_definition
+%type <node> opt_row_pattern_common_syntax
+ row_pattern row_pattern_alt row_pattern_seq
+ row_pattern_term row_pattern_primary
+ row_pattern_quantifier_opt
+%type <list> row_pattern_definition_list
+%type <ival> opt_row_pattern_skip_to
+%type <boolean> opt_row_pattern_initial_or_seek
+
/*
* Non-keyword token types. These are hard-wired into the "flex" lexer.
* They must be listed first so that their numeric codes do not depend on
@@ -724,7 +735,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURSOR CYCLE
DATA_P DATABASE DAY_P DEALLOCATE DEC DECIMAL_P DECLARE DEFAULT DEFAULTS
- DEFERRABLE DEFERRED DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DEPTH DESC
+ DEFERRABLE DEFERRED DEFINE DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DEPTH DESC
DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
DOUBLE_P DROP
@@ -740,7 +751,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
HANDLER HAVING HEADER_P HOLD HOUR_P
IDENTITY_P IF_P IGNORE_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P INCLUDE
- INCLUDING INCREMENT INDENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P
+ INCLUDING INCREMENT INDENT INDEX INDEXES INHERIT INHERITS INITIAL INITIALLY INLINE_P
INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER
INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION
@@ -765,8 +776,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
ORDER ORDINALITY OTHERS OUT_P OUTER_P
OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
- PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD PATH
- PERIOD PLACING PLAN PLANS POLICY
+ PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD PAST PATH
+ PATTERN_P PERIOD PLACING PLAN PLANS POLICY
POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
@@ -777,7 +788,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
RESET RESPECT_P RESTART RESTRICT RETURN RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP
ROUTINE ROUTINES ROW ROWS RULE
- SAVEPOINT SCALAR SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT
+ SAVEPOINT SCALAR SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SEEK SELECT
SEQUENCE SEQUENCES
SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SPLIT SOURCE SQL_P STABLE STANDALONE_P
@@ -860,8 +871,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
* reference point for a precedence level that we can assign to other
* keywords that lack a natural precedence level.
*
- * We need to do this for PARTITION, RANGE, ROWS, and GROUPS to support
- * opt_existing_window_name (see comment there).
+ * We need to do this for PARTITION, RANGE, ROWS, GROUPS, AFTER, INITIAL,
+ * SEEK, PATTERN_P to support opt_existing_window_name (see comment there).
*
* The frame_bound productions UNBOUNDED PRECEDING and UNBOUNDED FOLLOWING
* are even messier: since UNBOUNDED is an unreserved keyword (per spec!),
@@ -891,7 +902,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */
%nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP
SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH
-%left Op OPERATOR /* multi-character ops and user-defined operators */
+ AFTER INITIAL SEEK PATTERN_P
+%left Op OPERATOR '|' /* multi-character ops and user-defined operators */
%left '+' '-'
%left '*' '/' '%'
%left '^'
@@ -16636,6 +16648,8 @@ over_clause: OVER window_specification
n->startOffset = NULL;
n->endOffset = NULL;
n->location = @2;
+ n->frameLocation = -1;
+ n->excludeLocation = -1;
$$ = n;
}
| /*EMPTY*/
@@ -16643,7 +16657,8 @@ over_clause: OVER window_specification
;
window_specification: '(' opt_existing_window_name opt_partition_clause
- opt_sort_clause opt_frame_clause ')'
+ opt_sort_clause opt_frame_clause
+ opt_row_pattern_common_syntax ')'
{
WindowDef *n = makeNode(WindowDef);
@@ -16655,20 +16670,23 @@ window_specification: '(' opt_existing_window_name opt_partition_clause
n->frameOptions = $5->frameOptions;
n->startOffset = $5->startOffset;
n->endOffset = $5->endOffset;
+ n->frameLocation = $5->frameLocation;
+ n->excludeLocation = $5->excludeLocation;
+ n->rpCommonSyntax = (RPCommonSyntax *)$6;
n->location = @1;
$$ = n;
}
;
/*
- * If we see PARTITION, RANGE, ROWS or GROUPS as the first token after the '('
- * of a window_specification, we want the assumption to be that there is
- * no existing_window_name; but those keywords are unreserved and so could
- * be ColIds. We fix this by making them have the same precedence as IDENT
- * and giving the empty production here a slightly higher precedence, so
- * that the shift/reduce conflict is resolved in favor of reducing the rule.
- * These keywords are thus precluded from being an existing_window_name but
- * are not reserved for any other purpose.
+ * If we see PARTITION, RANGE, ROWS, GROUPS, AFTER, INITIAL, SEEK or PATTERN_P
+ * as the first token after the '(' of a window_specification, we want the
+ * assumption to be that there is no existing_window_name; but those keywords
+ * are unreserved and so could be ColIds. We fix this by making them have the
+ * same precedence as IDENT and giving the empty production here a slightly
+ * higher precedence, so that the shift/reduce conflict is resolved in favor
+ * of reducing the rule. These keywords are thus precluded from being an
+ * existing_window_name but are not reserved for any other purpose.
*/
opt_existing_window_name: ColId { $$ = $1; }
| /*EMPTY*/ %prec Op { $$ = NULL; }
@@ -16689,6 +16707,8 @@ opt_frame_clause:
n->frameOptions |= FRAMEOPTION_NONDEFAULT | FRAMEOPTION_RANGE;
n->frameOptions |= $3;
+ n->frameLocation = @1;
+ n->excludeLocation = ($3 != 0) ? @3 : -1;
$$ = n;
}
| ROWS frame_extent opt_window_exclusion_clause
@@ -16697,6 +16717,8 @@ opt_frame_clause:
n->frameOptions |= FRAMEOPTION_NONDEFAULT | FRAMEOPTION_ROWS;
n->frameOptions |= $3;
+ n->frameLocation = @1;
+ n->excludeLocation = ($3 != 0) ? @3 : -1;
$$ = n;
}
| GROUPS frame_extent opt_window_exclusion_clause
@@ -16705,6 +16727,8 @@ opt_frame_clause:
n->frameOptions |= FRAMEOPTION_NONDEFAULT | FRAMEOPTION_GROUPS;
n->frameOptions |= $3;
+ n->frameLocation = @1;
+ n->excludeLocation = ($3 != 0) ? @3 : -1;
$$ = n;
}
| /*EMPTY*/
@@ -16714,6 +16738,8 @@ opt_frame_clause:
n->frameOptions = FRAMEOPTION_DEFAULTS;
n->startOffset = NULL;
n->endOffset = NULL;
+ n->frameLocation = -1;
+ n->excludeLocation = -1;
$$ = n;
}
;
@@ -16789,6 +16815,8 @@ frame_bound:
n->frameOptions = FRAMEOPTION_START_UNBOUNDED_PRECEDING;
n->startOffset = NULL;
n->endOffset = NULL;
+ n->frameLocation = -1;
+ n->excludeLocation = -1;
$$ = n;
}
| UNBOUNDED FOLLOWING
@@ -16798,6 +16826,8 @@ frame_bound:
n->frameOptions = FRAMEOPTION_START_UNBOUNDED_FOLLOWING;
n->startOffset = NULL;
n->endOffset = NULL;
+ n->frameLocation = -1;
+ n->excludeLocation = -1;
$$ = n;
}
| CURRENT_P ROW
@@ -16807,6 +16837,8 @@ frame_bound:
n->frameOptions = FRAMEOPTION_START_CURRENT_ROW;
n->startOffset = NULL;
n->endOffset = NULL;
+ n->frameLocation = -1;
+ n->excludeLocation = -1;
$$ = n;
}
| a_expr PRECEDING
@@ -16816,6 +16848,8 @@ frame_bound:
n->frameOptions = FRAMEOPTION_START_OFFSET_PRECEDING;
n->startOffset = $1;
n->endOffset = NULL;
+ n->frameLocation = -1;
+ n->excludeLocation = -1;
$$ = n;
}
| a_expr FOLLOWING
@@ -16825,6 +16859,8 @@ frame_bound:
n->frameOptions = FRAMEOPTION_START_OFFSET_FOLLOWING;
n->startOffset = $1;
n->endOffset = NULL;
+ n->frameLocation = -1;
+ n->excludeLocation = -1;
$$ = n;
}
;
@@ -16837,6 +16873,325 @@ opt_window_exclusion_clause:
| /*EMPTY*/ { $$ = 0; }
;
+opt_row_pattern_common_syntax:
+opt_row_pattern_skip_to opt_row_pattern_initial_or_seek
+ PATTERN_P '(' row_pattern ')'
+ DEFINE row_pattern_definition_list
+ {
+ RPCommonSyntax *n = makeNode(RPCommonSyntax);
+ n->rpSkipTo = $1;
+ n->initial = $2;
+ n->rpPattern = (RPRPatternNode *) $5;
+ n->rpDefs = $8;
+ $$ = (Node *) n;
+ }
+ | /*EMPTY*/ { $$ = NULL; }
+ ;
+
+opt_row_pattern_skip_to:
+ AFTER MATCH SKIP TO NEXT ROW
+ {
+ $$ = ST_NEXT_ROW;
+ }
+ | AFTER MATCH SKIP PAST LAST_P ROW
+ {
+ $$ = ST_PAST_LAST_ROW;
+ }
+ | /*EMPTY*/
+ {
+ $$ = ST_PAST_LAST_ROW;
+ }
+ ;
+
+opt_row_pattern_initial_or_seek:
+ INITIAL { $$ = true; }
+ | SEEK
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("SEEK is not supported"),
+ errhint("Use INITIAL instead."),
+ parser_errposition(@1)));
+ }
+ | /*EMPTY*/ { $$ = true; }
+ ;
+
+row_pattern:
+ row_pattern_alt { $$ = $1; }
+ ;
+
+row_pattern_alt:
+ row_pattern_seq { $$ = $1; }
+ | row_pattern_alt '|' row_pattern_seq
+ {
+ RPRPatternNode *n;
+ /* If left side is already ALT, append to it */
+ if (IsA($1, RPRPatternNode) &&
+ ((RPRPatternNode *) $1)->nodeType == RPR_PATTERN_ALT)
+ {
+ n = (RPRPatternNode *) $1;
+ n->children = lappend(n->children, $3);
+ $$ = (Node *) n;
+ }
+ else
+ {
+ n = makeNode(RPRPatternNode);
+ n->nodeType = RPR_PATTERN_ALT;
+ n->children = list_make2($1, $3);
+ n->min = 1;
+ n->max = 1;
+ n->reluctant = -1;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ }
+ ;
+
+row_pattern_seq:
+ row_pattern_term { $$ = $1; }
+ | row_pattern_seq row_pattern_term
+ {
+ RPRPatternNode *n;
+ /* If left side is already SEQ, append to it */
+ if (IsA($1, RPRPatternNode) &&
+ ((RPRPatternNode *) $1)->nodeType == RPR_PATTERN_SEQ)
+ {
+ n = (RPRPatternNode *) $1;
+ n->children = lappend(n->children, $2);
+ $$ = (Node *) n;
+ }
+ else
+ {
+ n = makeNode(RPRPatternNode);
+ n->nodeType = RPR_PATTERN_SEQ;
+ n->children = list_make2($1, $2);
+ n->min = 1;
+ n->max = 1;
+ n->reluctant = -1;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ }
+ ;
+
+row_pattern_term:
+ row_pattern_primary row_pattern_quantifier_opt
+ {
+ RPRPatternNode *n = (RPRPatternNode *) $1;
+ RPRPatternNode *q = (RPRPatternNode *) $2;
+
+ n->min = q->min;
+ n->max = q->max;
+ n->reluctant = q->reluctant;
+ $$ = (Node *) n;
+ }
+ ;
+
+row_pattern_primary:
+ ColId
+ {
+ RPRPatternNode *n = makeNode(RPRPatternNode);
+ n->nodeType = RPR_PATTERN_VAR;
+ n->varName = $1;
+ n->min = 1;
+ n->max = 1;
+ n->reluctant = -1;
+ n->children = NIL;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | '(' row_pattern ')'
+ {
+ RPRPatternNode *inner = (RPRPatternNode *) $2;
+ RPRPatternNode *n = makeNode(RPRPatternNode);
+ n->nodeType = RPR_PATTERN_GROUP;
+ n->children = list_make1(inner);
+ n->min = 1;
+ n->max = 1;
+ n->reluctant = -1;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ ;
+
+row_pattern_quantifier_opt:
+ /* EMPTY */ { $$ = (Node *) makeRPRQuantifier(1, 1, -1, @$, yyscanner); }
+ | '*' { $$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner); }
+ | '+' { $$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner); }
+ | Op
+ {
+ /* Handle single Op: ? or reluctant quantifiers *?, +?, ?? */
+ if (strcmp($1, "?") == 0)
+ $$ = (Node *) makeRPRQuantifier(0, 1, -1, @1, yyscanner);
+ else if (strcmp($1, "*?") == 0)
+ $$ = (Node *) makeRPRQuantifier(0, INT_MAX, @1 + 1, @1, yyscanner);
+ else if (strcmp($1, "+?") == 0)
+ $$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
+ else if (strcmp($1, "??") == 0)
+ $$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("unsupported quantifier \"%s\"", $1),
+ errhint("Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions."),
+ parser_errposition(@1)));
+ }
+ /* RELUCTANT quantifiers (when lexer separates tokens) */
+ | '*' Op
+ {
+ if (strcmp($2, "?") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid token after \"*\" quantifier"),
+ errhint("Did you mean \"*?\" for reluctant quantifier?"),
+ parser_errposition(@2)));
+ $$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+ }
+ | '+' Op
+ {
+ if (strcmp($2, "?") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid token after \"+\" quantifier"),
+ errhint("Did you mean \"+?\" for reluctant quantifier?"),
+ parser_errposition(@2)));
+ $$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+ }
+ | Op Op
+ {
+ if (strcmp($1, "?") != 0 || strcmp($2, "?") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid quantifier combination"),
+ errhint("Did you mean \"??\" for reluctant quantifier?"),
+ parser_errposition(@1)));
+ $$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
+ }
+ /* {n}, {n,}, {,m}, {n,m} quantifiers */
+ | '{' Iconst '}'
+ {
+ if ($2 <= 0 || $2 >= INT_MAX)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+ parser_errposition(@2));
+ $$ = (Node *) makeRPRQuantifier($2, $2, -1, @1, yyscanner);
+ }
+ | '{' Iconst ',' '}'
+ {
+ if ($2 < 0 || $2 >= INT_MAX)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
+ parser_errposition(@2));
+ $$ = (Node *) makeRPRQuantifier($2, INT_MAX, -1, @1, yyscanner);
+ }
+ | '{' ',' Iconst '}'
+ {
+ if ($3 <= 0 || $3 >= INT_MAX)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+ parser_errposition(@3));
+ $$ = (Node *) makeRPRQuantifier(0, $3, -1, @1, yyscanner);
+ }
+ | '{' Iconst ',' Iconst '}'
+ {
+ if ($2 < 0 || $4 <= 0 || $2 >= INT_MAX || $4 >= INT_MAX)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("quantifier bounds must be between 0 and %d with max >= 1", INT_MAX - 1),
+ parser_errposition(@2));
+ if ($2 > $4)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("quantifier minimum bound must not exceed maximum"),
+ parser_errposition(@2));
+ $$ = (Node *) makeRPRQuantifier($2, $4, -1, @1, yyscanner);
+ }
+ /* Reluctant versions: {n}?, {n,}?, {,m}?, {n,m}? */
+ | '{' Iconst '}' Op
+ {
+ if (strcmp($4, "?") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid token after range quantifier"),
+ errhint("Only \"?\" is allowed after {n} to make it reluctant."),
+ parser_errposition(@4)));
+ if ($2 <= 0 || $2 >= INT_MAX)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+ parser_errposition(@2));
+ $$ = (Node *) makeRPRQuantifier($2, $2, @4, @1, yyscanner);
+ }
+ | '{' Iconst ',' '}' Op
+ {
+ if (strcmp($5, "?") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid token after range quantifier"),
+ errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
+ parser_errposition(@5)));
+ if ($2 < 0 || $2 >= INT_MAX)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
+ parser_errposition(@2));
+ $$ = (Node *) makeRPRQuantifier($2, INT_MAX, @5, @1, yyscanner);
+ }
+ | '{' ',' Iconst '}' Op
+ {
+ if (strcmp($5, "?") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid token after range quantifier"),
+ errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
+ parser_errposition(@5)));
+ if ($3 <= 0 || $3 >= INT_MAX)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+ parser_errposition(@3));
+ $$ = (Node *) makeRPRQuantifier(0, $3, @5, @1, yyscanner);
+ }
+ | '{' Iconst ',' Iconst '}' Op
+ {
+ if (strcmp($6, "?") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid token after range quantifier"),
+ errhint("Only \"?\" is allowed after {n,m} to make it reluctant."),
+ parser_errposition(@6)));
+ if ($2 < 0 || $4 <= 0 || $2 >= INT_MAX || $4 >= INT_MAX)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("quantifier bounds must be between 0 and %d with max >= 1", INT_MAX - 1),
+ parser_errposition(@2));
+ if ($2 > $4)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("quantifier minimum bound must not exceed maximum"),
+ parser_errposition(@2));
+ $$ = (Node *) makeRPRQuantifier($2, $4, @6, @1, yyscanner);
+ }
+ ;
+
+row_pattern_definition_list:
+ row_pattern_definition { $$ = list_make1($1); }
+ | row_pattern_definition_list ',' row_pattern_definition { $$ = lappend($1, $3); }
+ ;
+
+row_pattern_definition:
+ ColId AS a_expr
+ {
+ $$ = makeNode(ResTarget);
+ $$->name = $1;
+ $$->indirection = NIL;
+ $$->val = (Node *) $3;
+ $$->location = @1;
+ }
+ ;
/*
* Supporting nonterminals for expressions.
@@ -16881,12 +17236,15 @@ MathOp: '+' { $$ = "+"; }
| LESS_EQUALS { $$ = "<="; }
| GREATER_EQUALS { $$ = ">="; }
| NOT_EQUALS { $$ = "<>"; }
+ | '|' { $$ = "|"; }
;
qual_Op: Op
{ $$ = list_make1(makeString($1)); }
| OPERATOR '(' any_operator ')'
{ $$ = $3; }
+ | '|'
+ { $$ = list_make1(makeString("|")); }
;
qual_all_Op:
@@ -17976,6 +18334,7 @@ unreserved_keyword:
| DECLARE
| DEFAULTS
| DEFERRED
+ | DEFINE
| DEFINER
| DELETE_P
| DELIMITER
@@ -18041,6 +18400,7 @@ unreserved_keyword:
| INDEXES
| INHERIT
| INHERITS
+ | INITIAL
| INLINE_P
| INPUT_P
| INSENSITIVE
@@ -18116,7 +18476,9 @@ unreserved_keyword:
| PARTITIONS
| PASSING
| PASSWORD
+ | PAST
| PATH
+ | PATTERN_P
| PERIOD
| PLAN
| PLANS
@@ -18170,6 +18532,7 @@ unreserved_keyword:
| SEARCH
| SECOND_P
| SECURITY
+ | SEEK
| SEQUENCE
| SEQUENCES
| SERIALIZABLE
@@ -18557,6 +18920,7 @@ bare_label_keyword:
| DEFAULTS
| DEFERRABLE
| DEFERRED
+ | DEFINE
| DEFINER
| DELETE_P
| DELIMITER
@@ -18635,6 +18999,7 @@ bare_label_keyword:
| INDEXES
| INHERIT
| INHERITS
+ | INITIAL
| INITIALLY
| INLINE_P
| INNER_P
@@ -18748,7 +19113,9 @@ bare_label_keyword:
| PARTITIONS
| PASSING
| PASSWORD
+ | PAST
| PATH
+ | PATTERN_P
| PERIOD
| PLACING
| PLAN
@@ -18807,6 +19174,7 @@ bare_label_keyword:
| SCROLL
| SEARCH
| SECURITY
+ | SEEK
| SELECT
| SEQUENCE
| SEQUENCES
@@ -19998,6 +20366,29 @@ makeRecursiveViewSelect(char *relname, List *aliases, Node *query)
return (Node *) s;
}
+/*
+ * makeRPRQuantifier
+ * Create an RPRPatternNode with specified quantifier bounds.
+ */
+static RPRPatternNode *
+makeRPRQuantifier(int min, int max, ParseLoc reluctant, int location,
+ core_yyscan_t yyscanner)
+{
+ RPRPatternNode *n = makeNode(RPRPatternNode);
+
+ if (reluctant >= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("reluctant quantifiers are not yet supported"),
+ parser_errposition(reluctant)));
+
+ n->min = min;
+ n->max = max;
+ n->reluctant = reluctant;
+ n->location = location;
+ return n;
+}
+
/* parser_init()
* Initialize to parse one query string
*/
diff --git a/src/backend/parser/scan.l b/src/backend/parser/scan.l
index 6c162f48342..fa04ec2b951 100644
--- a/src/backend/parser/scan.l
+++ b/src/backend/parser/scan.l
@@ -359,7 +359,7 @@ not_equals "!="
* If you change either set, adjust the character lists appearing in the
* rule for "operator"!
*/
-self [,()\[\].;\:\+\-\*\/\%\^\<\>\=]
+self [,()\[\].;\:\+\-\*\/\%\^\<\>\=\|]
op_chars [\~\!\@\#\^\&\|\`\?\+\-\*\/\%\<\>\=]
operator {op_chars}+
@@ -930,7 +930,7 @@ other .
* that the "self" rule would have.
*/
if (nchars == 1 &&
- strchr(",()[].;:+-*/%^<>=", yytext[0]))
+ strchr(",()[].;:+-*/%^<>=|", yytext[0]))
return yytext[0];
/*
* Likewise, if what we have left is two chars, and
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0aec49bdd22..3a472481fb2 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -578,6 +578,57 @@ typedef struct SortBy
ParseLoc location; /* operator location, or -1 if none/unknown */
} SortBy;
+/*
+ * AFTER MATCH row pattern skip to types in row pattern common syntax
+ */
+typedef enum RPSkipTo
+{
+ ST_NONE, /* AFTER MATCH omitted */
+ ST_NEXT_ROW, /* SKIP TO NEXT ROW */
+ ST_PAST_LAST_ROW, /* SKIP TO PAST LAST ROW */
+} RPSkipTo;
+
+/*
+ * RPRPatternNodeType - Row Pattern Recognition pattern node types
+ */
+typedef enum RPRPatternNodeType
+{
+ RPR_PATTERN_VAR, /* variable reference */
+ RPR_PATTERN_SEQ, /* sequence (concatenation) */
+ RPR_PATTERN_ALT, /* alternation (|) */
+ RPR_PATTERN_GROUP /* group (parentheses) */
+} RPRPatternNodeType;
+
+/*
+ * RPRPatternNode - Row Pattern Recognition pattern AST node
+ */
+typedef struct RPRPatternNode
+{
+ NodeTag type; /* T_RPRPatternNode */
+ RPRPatternNodeType nodeType; /* VAR, SEQ, ALT, GROUP */
+ int min; /* minimum repetitions (0 for *, ?) */
+ int max; /* maximum repetitions (INT_MAX for *, +) */
+ ParseLoc reluctant; /* location of '?' for reluctant, -1 for
+ * greedy */
+ ParseLoc location; /* token location, or -1 */
+ char *varName; /* VAR: variable name */
+ List *children; /* SEQ, ALT, GROUP: child nodes */
+} RPRPatternNode;
+
+/*
+ * RowPatternCommonSyntax - raw representation of row pattern common syntax
+ */
+typedef struct RPCommonSyntax
+{
+ NodeTag type;
+ RPSkipTo rpSkipTo; /* Row Pattern AFTER MATCH SKIP type */
+ bool initial; /* true if <row pattern initial or seek> is
+ * initial */
+ RPRPatternNode *rpPattern; /* PATTERN clause AST */
+ List *rpDefs; /* row pattern definitions clause (list of
+ * ResTarget) */
+} RPCommonSyntax;
+
/*
* WindowDef - raw representation of WINDOW and OVER clauses
*
@@ -593,10 +644,13 @@ typedef struct WindowDef
char *refname; /* referenced window name, if any */
List *partitionClause; /* PARTITION BY expression list */
List *orderClause; /* ORDER BY (list of SortBy) */
+ RPCommonSyntax *rpCommonSyntax; /* row pattern common syntax */
int frameOptions; /* frame_clause options, see below */
Node *startOffset; /* expression for starting bound, if any */
Node *endOffset; /* expression for ending bound, if any */
ParseLoc location; /* parse location, or -1 if none/unknown */
+ ParseLoc frameLocation; /* ROWS/RANGE/GROUPS location, or -1 */
+ ParseLoc excludeLocation; /* EXCLUDE location, or -1 */
} WindowDef;
/*
@@ -1589,6 +1643,11 @@ typedef struct GroupingSet
* the orderClause might or might not be copied (see copiedOrder); the framing
* options are never copied, per spec.
*
+ * "defineClause" is Row Pattern Recognition DEFINE clause (list of
+ * TargetEntry). TargetEntry.resname represents row pattern definition
+ * variable name. "rpPattern" represents PATTERN clause as an AST tree
+ * (RPRPatternNode).
+ *
* The information relevant for the query jumbling is the partition clause
* type and its bounds.
*/
@@ -1618,6 +1677,14 @@ typedef struct WindowClause
Index winref; /* ID referenced by window functions */
/* did we copy orderClause from refname? */
bool copiedOrder pg_node_attr(query_jumble_ignore);
+ /* Row Pattern AFTER MATCH SKIP clause */
+ RPSkipTo rpSkipTo; /* Row Pattern Skip To type */
+ bool initial; /* true if <row pattern initial or seek> is
+ * initial */
+ /* Row Pattern DEFINE clause (list of TargetEntry) */
+ List *defineClause;
+ /* Row Pattern PATTERN clause AST */
+ RPRPatternNode *rpPattern;
} WindowClause;
/*
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f7753c5c8a8..1624f4412dc 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -129,6 +129,7 @@ PG_KEYWORD("default", DEFAULT, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("defaults", DEFAULTS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("deferrable", DEFERRABLE, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("deferred", DEFERRED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("define", DEFINE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("definer", DEFINER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("delete", DELETE_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("delimiter", DELIMITER, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -217,6 +218,7 @@ PG_KEYWORD("index", INDEX, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("indexes", INDEXES, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("inherit", INHERIT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("inherits", INHERITS, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("initial", INITIAL, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("initially", INITIALLY, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("inline", INLINE_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("inner", INNER_P, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
@@ -342,7 +344,9 @@ PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("partitions", PARTITIONS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("past", PAST, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("pattern", PATTERN_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("period", PERIOD, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -405,6 +409,7 @@ PG_KEYWORD("scroll", SCROLL, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("search", SEARCH, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("second", SECOND_P, UNRESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("security", SECURITY, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("seek", SEEK, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("select", SELECT, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("sequence", SEQUENCE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("sequences", SEQUENCES, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index f23e21f318b..4dc7e5738ae 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -51,6 +51,7 @@ typedef enum ParseExprKind
EXPR_KIND_WINDOW_FRAME_RANGE, /* window frame clause with RANGE */
EXPR_KIND_WINDOW_FRAME_ROWS, /* window frame clause with ROWS */
EXPR_KIND_WINDOW_FRAME_GROUPS, /* window frame clause with GROUPS */
+ EXPR_KIND_RPR_DEFINE, /* DEFINE */
EXPR_KIND_SELECT_TARGET, /* SELECT target list item */
EXPR_KIND_INSERT_TARGET, /* INSERT target list item */
EXPR_KIND_UPDATE_SOURCE, /* UPDATE assignment source item */
--
2.43.0
[application/octet-stream] v43-0002-Row-pattern-recognition-patch-parse-analysis.patch (27.4K, 3-v43-0002-Row-pattern-recognition-patch-parse-analysis.patch)
download | inline diff:
From 5757d5ab5daa77484355dcf0eab550a42994d175 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sun, 15 Feb 2026 17:47:48 +0900
Subject: [PATCH v43 2/8] Row pattern recognition patch (parse/analysis).
---
src/backend/nodes/copyfuncs.c | 27 +++
src/backend/nodes/equalfuncs.c | 35 +++
src/backend/nodes/outfuncs.c | 51 +++++
src/backend/nodes/readfuncs.c | 85 +++++++
src/backend/parser/Makefile | 1 +
src/backend/parser/README | 1 +
src/backend/parser/meson.build | 1 +
src/backend/parser/parse_agg.c | 7 +
src/backend/parser/parse_clause.c | 10 +-
src/backend/parser/parse_expr.c | 6 +
src/backend/parser/parse_func.c | 3 +
src/backend/parser/parse_rpr.c | 367 ++++++++++++++++++++++++++++++
src/include/parser/parse_clause.h | 3 +
src/include/parser/parse_rpr.h | 22 ++
14 files changed, 615 insertions(+), 4 deletions(-)
create mode 100644 src/backend/parser/parse_rpr.c
create mode 100644 src/include/parser/parse_rpr.h
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index ff22a04abe5..e67ad39bdb8 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -16,6 +16,7 @@
#include "postgres.h"
#include "miscadmin.h"
+#include "nodes/plannodes.h"
#include "utils/datum.h"
@@ -166,6 +167,32 @@ _copyBitmapset(const Bitmapset *from)
return bms_copy(from);
}
+static RPRPattern *
+_copyRPRPattern(const RPRPattern *from)
+{
+ RPRPattern *newnode = makeNode(RPRPattern);
+
+ COPY_SCALAR_FIELD(numVars);
+ COPY_SCALAR_FIELD(maxDepth);
+ COPY_SCALAR_FIELD(numElements);
+
+ /* Deep copy the varNames array (DEFINE clause is required) */
+ Assert(from->numVars > 0);
+ newnode->varNames = palloc0(from->numVars * sizeof(char *));
+ for (int i = 0; i < from->numVars; i++)
+ newnode->varNames[i] = pstrdup(from->varNames[i]);
+
+ /* Deep copy the elements array (always has at least one element + FIN) */
+ Assert(from->numElements >= 2);
+ newnode->elements = palloc(from->numElements * sizeof(RPRPatternElement));
+ memcpy(newnode->elements, from->elements,
+ from->numElements * sizeof(RPRPatternElement));
+
+ COPY_SCALAR_FIELD(isAbsorbable);
+
+ return newnode;
+}
+
/*
* copyObjectImpl -- implementation of copyObject(); see nodes/nodes.h
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3d1a1adf86e..328199918b8 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -20,6 +20,7 @@
#include "postgres.h"
#include "miscadmin.h"
+#include "nodes/plannodes.h"
#include "utils/datum.h"
@@ -149,6 +150,40 @@ _equalBitmapset(const Bitmapset *a, const Bitmapset *b)
return bms_equal(a, b);
}
+static bool
+_equalRPRPattern(const RPRPattern *a, const RPRPattern *b)
+{
+ COMPARE_SCALAR_FIELD(numVars);
+ COMPARE_SCALAR_FIELD(maxDepth);
+ COMPARE_SCALAR_FIELD(numElements);
+
+ /* Compare varNames array */
+ if (a->numVars > 0)
+ {
+ if (a->varNames == NULL || b->varNames == NULL)
+ return false;
+ for (int i = 0; i < a->numVars; i++)
+ {
+ if (strcmp(a->varNames[i], b->varNames[i]) != 0)
+ return false;
+ }
+ }
+
+ /* Compare elements array */
+ if (a->numElements > 0)
+ {
+ if (a->elements == NULL || b->elements == NULL)
+ return false;
+ if (memcmp(a->elements, b->elements,
+ a->numElements * sizeof(RPRPatternElement)) != 0)
+ return false;
+ }
+
+ COMPARE_SCALAR_FIELD(isAbsorbable);
+
+ return true;
+}
+
/*
* Lists are handled specially
*/
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 40990143927..aacf8e9f8c7 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -23,6 +23,7 @@
#include "nodes/bitmapset.h"
#include "nodes/nodes.h"
#include "nodes/pg_list.h"
+#include "nodes/plannodes.h"
#include "utils/datum.h"
/* State flag that determines how nodeToStringInternal() should treat location fields */
@@ -718,6 +719,56 @@ _outA_Const(StringInfo str, const A_Const *node)
WRITE_LOCATION_FIELD(location);
}
+static void
+_outRPRPattern(StringInfo str, const RPRPattern *node)
+{
+ WRITE_NODE_TYPE("RPRPATTERN");
+
+ WRITE_INT_FIELD(numVars);
+ WRITE_INT_FIELD(maxDepth);
+ WRITE_INT_FIELD(numElements);
+
+ /* Write varNames array as list of strings */
+ appendStringInfoString(str, " :varNames");
+ if (node->numVars > 0 && node->varNames != NULL)
+ {
+ appendStringInfoString(str, " (");
+ for (int i = 0; i < node->numVars; i++)
+ {
+ if (i > 0)
+ appendStringInfoChar(str, ' ');
+ outToken(str, node->varNames[i]);
+ }
+ appendStringInfoChar(str, ')');
+ }
+ else
+ appendStringInfoString(str, " <>");
+
+ /* Write elements array */
+ appendStringInfoString(str, " :elements");
+ if (node->numElements > 0 && node->elements != NULL)
+ {
+ appendStringInfoChar(str, ' ');
+ for (int i = 0; i < node->numElements; i++)
+ {
+ const RPRPatternElement *elem = &node->elements[i];
+
+ appendStringInfo(str, "(%d %d %u %d %d %d %d)",
+ (int) elem->varId,
+ (int) elem->depth,
+ (unsigned) elem->flags,
+ (int) elem->min,
+ (int) elem->max,
+ (int) elem->next,
+ (int) elem->jump);
+ }
+ }
+ else
+ appendStringInfoString(str, " <>");
+
+ WRITE_BOOL_FIELD(isAbsorbable);
+}
+
/*
* outNode -
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 981ab9c34ef..02be217b167 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -28,6 +28,7 @@
#include "miscadmin.h"
#include "nodes/bitmapset.h"
+#include "nodes/plannodes.h"
#include "nodes/readfuncs.h"
@@ -558,6 +559,90 @@ _readExtensibleNode(void)
READ_DONE();
}
+static RPRPattern *
+_readRPRPattern(void)
+{
+ READ_LOCALS(RPRPattern);
+
+ READ_INT_FIELD(numVars);
+ READ_INT_FIELD(maxDepth);
+ READ_INT_FIELD(numElements);
+
+ /* Read varNames array */
+ token = pg_strtok(&length); /* skip :varNames */
+ token = pg_strtok(&length); /* get '(' or '<>' */
+ if (local_node->numVars > 0 && token[0] == '(')
+ {
+ local_node->varNames = palloc(local_node->numVars * sizeof(char *));
+ for (int i = 0; i < local_node->numVars; i++)
+ {
+ token = pg_strtok(&length);
+ local_node->varNames[i] = debackslash(token, length);
+ }
+ token = pg_strtok(&length); /* skip ')' */
+ }
+ else
+ {
+ local_node->varNames = NULL;
+ }
+
+ /* Read elements array */
+ token = pg_strtok(&length); /* skip :elements */
+ token = pg_strtok(&length); /* get '(' or '<>' */
+ if (local_node->numElements > 0 && token[0] == '(')
+ {
+ local_node->elements = palloc0(local_node->numElements * sizeof(RPRPatternElement));
+ for (int i = 0; i < local_node->numElements; i++)
+ {
+ RPRPatternElement *elem = &local_node->elements[i];
+ int varId,
+ flags,
+ depth,
+ min,
+ max,
+ next,
+ jump;
+
+ /* Parse "(varId depth flags min max next jump)" */
+ token = pg_strtok(&length);
+ varId = atoi(token);
+ token = pg_strtok(&length);
+ depth = atoi(token);
+ token = pg_strtok(&length);
+ flags = atoi(token);
+ token = pg_strtok(&length);
+ min = atoi(token);
+ token = pg_strtok(&length);
+ max = atoi(token);
+ token = pg_strtok(&length);
+ next = atoi(token);
+ token = pg_strtok(&length);
+ jump = atoi(token);
+ token = pg_strtok(&length); /* skip ')' */
+
+ elem->varId = (RPRVarId) varId;
+ elem->flags = (RPRElemFlags) flags;
+ elem->depth = (RPRDepth) depth;
+ elem->min = (RPRQuantity) min;
+ elem->max = (RPRQuantity) max;
+ elem->next = (RPRElemIdx) next;
+ elem->jump = (RPRElemIdx) jump;
+
+ /* Read next element's '(' or end */
+ if (i < local_node->numElements - 1)
+ token = pg_strtok(&length); /* get '(' */
+ }
+ }
+ else
+ {
+ local_node->elements = NULL;
+ }
+
+ READ_BOOL_FIELD(isAbsorbable);
+
+ READ_DONE();
+}
+
/*
* parseNodeString
diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile
index 8c0fe28d63f..f4c7d605fe3 100644
--- a/src/backend/parser/Makefile
+++ b/src/backend/parser/Makefile
@@ -29,6 +29,7 @@ OBJS = \
parse_oper.o \
parse_param.o \
parse_relation.o \
+ parse_rpr.o \
parse_target.o \
parse_type.o \
parse_utilcmd.o \
diff --git a/src/backend/parser/README b/src/backend/parser/README
index e26eb437a9f..2baffa9517e 100644
--- a/src/backend/parser/README
+++ b/src/backend/parser/README
@@ -26,6 +26,7 @@ parse_node.c create nodes for various structures
parse_oper.c handle operators in expressions
parse_param.c handle Params (for the cases used in the core backend)
parse_relation.c support routines for tables and column handling
+parse_rpr.c handle Row Pattern Recognition
parse_target.c handle the result list of the query
parse_type.c support routines for data type handling
parse_utilcmd.c parse analysis for utility commands (done at execution time)
diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build
index 924ee87a453..a07102b37a0 100644
--- a/src/backend/parser/meson.build
+++ b/src/backend/parser/meson.build
@@ -16,6 +16,7 @@ backend_sources += files(
'parse_oper.c',
'parse_param.c',
'parse_relation.c',
+ 'parse_rpr.c',
'parse_target.c',
'parse_type.c',
'parse_utilcmd.c',
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 25ee0f87d93..5ed785ea0d5 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -584,6 +584,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
errkind = true;
break;
+ case EXPR_KIND_RPR_DEFINE:
+ errkind = true;
+ break;
+
/*
* There is intentionally no default: case here, so that the
* compiler will warn if we add a new ParseExprKind without
@@ -1023,6 +1027,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_CYCLE_MARK:
errkind = true;
break;
+ case EXPR_KIND_RPR_DEFINE:
+ errkind = true;
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 06b65d4a605..b30c22933ec 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -37,6 +37,7 @@
#include "parser/parse_func.h"
#include "parser/parse_oper.h"
#include "parser/parse_relation.h"
+#include "parser/parse_rpr.h"
#include "parser/parse_target.h"
#include "parser/parse_type.h"
#include "parser/parser.h"
@@ -84,8 +85,6 @@ static void checkExprIsVarFree(ParseState *pstate, Node *n,
const char *constructName);
static TargetEntry *findTargetlistEntrySQL92(ParseState *pstate, Node *node,
List **tlist, ParseExprKind exprKind);
-static TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node,
- List **tlist, ParseExprKind exprKind);
static int get_matching_location(int sortgroupref,
List *sortgrouprefs, List *exprs);
static List *resolve_unique_index_expr(ParseState *pstate, InferClause *infer,
@@ -97,7 +96,6 @@ static Node *transformFrameOffset(ParseState *pstate, int frameOptions,
Oid rangeopfamily, Oid rangeopcintype, Oid *inRangeFunc,
Node *clause);
-
/*
* transformFromClause -
* Process the FROM clause and add items to the query's range table,
@@ -2168,7 +2166,7 @@ findTargetlistEntrySQL92(ParseState *pstate, Node *node, List **tlist,
* tlist the target list (passed by reference so we can append to it)
* exprKind identifies clause type being processed
*/
-static TargetEntry *
+TargetEntry *
findTargetlistEntrySQL99(ParseState *pstate, Node *node, List **tlist,
ParseExprKind exprKind)
{
@@ -3019,6 +3017,10 @@ transformWindowDefinitions(ParseState *pstate,
rangeopfamily, rangeopcintype,
&wc->endInRangeFunc,
windef->endOffset);
+
+ /* Process Row Pattern Recognition related clauses */
+ transformRPR(pstate, wc, windef, targetlist);
+
wc->winref = winref;
result = lappend(result, wc);
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index dcfe1acc4c3..219681d6e88 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -577,6 +577,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
case EXPR_KIND_COPY_WHERE:
case EXPR_KIND_GENERATED_COLUMN:
case EXPR_KIND_CYCLE_MARK:
+ case EXPR_KIND_RPR_DEFINE:
/* okay */
break;
@@ -1871,6 +1872,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
case EXPR_KIND_GENERATED_COLUMN:
err = _("cannot use subquery in column generation expression");
break;
+ case EXPR_KIND_RPR_DEFINE:
+ err = _("cannot use subquery in DEFINE expression");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -3230,6 +3234,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "GENERATED AS";
case EXPR_KIND_CYCLE_MARK:
return "CYCLE";
+ case EXPR_KIND_RPR_DEFINE:
+ return "DEFINE";
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 24f6745923b..357b236a818 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2783,6 +2783,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
case EXPR_KIND_CYCLE_MARK:
errkind = true;
break;
+ case EXPR_KIND_RPR_DEFINE:
+ errkind = true;
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
new file mode 100644
index 00000000000..9e1fa228759
--- /dev/null
+++ b/src/backend/parser/parse_rpr.c
@@ -0,0 +1,367 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_rpr.c
+ * Handle Row Pattern Recognition clauses in parser.
+ *
+ * This file transforms RPR-related clauses from raw parse tree to planner
+ * structures during query analysis:
+ * - Validates frame options (must start at CURRENT ROW, no EXCLUDE)
+ * - Validates PATTERN variable count (max RPR_VARID_MAX)
+ * - Transforms DEFINE clause into TargetEntry list
+ * - Stores PATTERN/SKIP TO/INITIAL clauses for planner
+ *
+ * Pattern optimization and compilation to NFA bytecode happens later
+ * in the planner (see optimizer/plan/rpr.c).
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/parser/parse_rpr.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/rpr.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_collate.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_rpr.h"
+#include "parser/parse_target.h"
+
+/* Forward declarations */
+static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
+ List **varNames);
+static List *transformDefineClause(ParseState *pstate, WindowClause *wc,
+ WindowDef *windef, List **targetlist);
+
+/*
+ * transformRPR
+ * Process Row Pattern Recognition related clauses.
+ *
+ * Validates and transforms RPR clauses from parse tree to planner structures:
+ * - Validates frame options (must start at CURRENT ROW, no EXCLUDE)
+ * - Stores AFTER MATCH SKIP TO clause
+ * - Stores SEEK/INITIAL clause
+ * - Transforms DEFINE clause into TargetEntry list
+ * - Stores PATTERN AST for deparsing (optimization happens in planner)
+ *
+ * Returns early if windef has no rpCommonSyntax (non-RPR window).
+ */
+void
+transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
+ List **targetlist)
+{
+ /* Window definition must exist when called */
+ Assert(windef != NULL);
+
+ /*
+ * Row Pattern Common Syntax clause exists?
+ */
+ if (windef->rpCommonSyntax == NULL)
+ return;
+
+ /* Check Frame options */
+
+ /* Frame type must be "ROW" */
+ if (wc->frameOptions & FRAMEOPTION_GROUPS)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("FRAME option GROUP is not permitted when row pattern recognition is used"),
+ errhint("Use: ROWS instead"),
+ parser_errposition(pstate,
+ windef->frameLocation >= 0 ?
+ windef->frameLocation : windef->location)));
+ if (wc->frameOptions & FRAMEOPTION_RANGE)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("FRAME option RANGE is not permitted when row pattern recognition is used"),
+ errhint("Use: ROWS instead"),
+ parser_errposition(pstate,
+ windef->frameLocation >= 0 ?
+ windef->frameLocation : windef->location)));
+
+ /* Frame must start at current row */
+ if ((wc->frameOptions & FRAMEOPTION_START_CURRENT_ROW) == 0)
+ {
+ const char *frameType = "ROWS";
+ const char *startBound = "unknown";
+
+ /* Determine current start bound */
+ if (wc->frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING)
+ startBound = "UNBOUNDED PRECEDING";
+ else if (wc->frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING)
+ startBound = "offset PRECEDING";
+ else if (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING)
+ startBound = "offset FOLLOWING";
+
+ /* At least one valid frame start option should be set */
+ Assert((wc->frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING) ||
+ (wc->frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING) ||
+ (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING));
+
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("FRAME must start at CURRENT ROW when row pattern recognition is used"),
+ errdetail("Current frame starts with %s.", startBound),
+ errhint("Use: %s BETWEEN CURRENT ROW AND ...", frameType),
+ parser_errposition(pstate, windef->frameLocation >= 0 ? windef->frameLocation : windef->location)));
+ }
+
+ /* EXCLUDE options are not permitted */
+ if ((wc->frameOptions & FRAMEOPTION_EXCLUSION) != 0)
+ {
+ const char *excludeType = "EXCLUDE";
+
+ /* Determine which EXCLUDE option was used */
+ if (wc->frameOptions & FRAMEOPTION_EXCLUDE_CURRENT_ROW)
+ excludeType = "EXCLUDE CURRENT ROW";
+ else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_GROUP)
+ excludeType = "EXCLUDE GROUP";
+ else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES)
+ excludeType = "EXCLUDE TIES";
+
+ /* At least one valid exclude option should be set */
+ Assert((wc->frameOptions & FRAMEOPTION_EXCLUDE_CURRENT_ROW) ||
+ (wc->frameOptions & FRAMEOPTION_EXCLUDE_GROUP) ||
+ (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES));
+
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("EXCLUDE options are not permitted when row pattern recognition is used"),
+ errdetail("Frame definition includes %s.", excludeType),
+ errhint("Remove the EXCLUDE clause from the window definition."),
+ parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location)));
+ }
+
+ /* Transform AFTER MATCH SKIP TO clause */
+ wc->rpSkipTo = windef->rpCommonSyntax->rpSkipTo;
+
+ /* Transform SEEK or INITIAL clause */
+ wc->initial = windef->rpCommonSyntax->initial;
+
+ /* Transform DEFINE clause into list of TargetEntry's */
+ wc->defineClause = transformDefineClause(pstate, wc, windef, targetlist);
+
+ /* Store PATTERN AST for deparsing */
+ wc->rpPattern = windef->rpCommonSyntax->rpPattern;
+}
+
+/*
+ * validateRPRPatternVarCount
+ * Validate that PATTERN variables don't exceed RPR_VARID_MAX.
+ *
+ * Recursively traverses the pattern tree, collecting unique variable names.
+ * Throws an error if the number of unique variables exceeds RPR_VARID_MAX.
+ *
+ * varNames is both input and output: existing names are preserved, new ones added.
+ */
+static void
+validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
+ List **varNames)
+{
+ ListCell *lc;
+
+ /* Pattern node must exist - parser always provides non-NULL root */
+ Assert(node != NULL);
+
+ switch (node->nodeType)
+ {
+ case RPR_PATTERN_VAR:
+ /* Add variable name if not already in list */
+ {
+ bool found = false;
+
+ foreach(lc, *varNames)
+ {
+ if (strcmp(strVal(lfirst(lc)), node->varName) == 0)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ {
+ /* Check against RPR_VARID_MAX before adding */
+ if (list_length(*varNames) >= RPR_VARID_MAX)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("too many pattern variables"),
+ errdetail("Maximum is %d.", RPR_VARID_MAX),
+ parser_errposition(pstate,
+ exprLocation((Node *) node))));
+
+ *varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
+ }
+ }
+ break;
+
+ case RPR_PATTERN_SEQ:
+ case RPR_PATTERN_ALT:
+ case RPR_PATTERN_GROUP:
+ /* Recurse into children */
+ foreach(lc, node->children)
+ {
+ validateRPRPatternVarCount(pstate, (RPRPatternNode *) lfirst(lc),
+ varNames);
+ }
+ break;
+ }
+}
+
+/*
+ * transformDefineClause
+ * Process DEFINE clause and transform ResTarget into list of TargetEntry.
+ *
+ * For each DEFINE variable:
+ * 1. Validates PATTERN variable count via validateRPRPatternVarCount
+ * 2. Checks for duplicate variable names in DEFINE clause
+ * 3. Transforms expressions and adds to targetlist via findTargetlistEntrySQL99
+ * 4. Creates defineClause entry with proper resname (pattern variable name)
+ * 5. Coerces expressions to boolean type
+ * 6. Marks column origins and assigns collation information
+ *
+ * Note: Variables not in DEFINE are evaluated as TRUE by the executor.
+ * Variables in DEFINE but not in PATTERN are filtered out by the planner.
+ *
+ * XXX we only support column reference in row pattern definition search
+ * condition, e.g. "price". <row pattern definition variable name>.<column
+ * reference> is not supported, e.g. "A.price".
+ */
+static List *
+transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
+ List **targetlist)
+{
+ ListCell *lc,
+ *l;
+ ResTarget *restarget,
+ *r;
+ List *restargets;
+ List *defineClause = NIL;
+ char *name;
+ List *patternVarNames = NIL;
+
+ /*
+ * If Row Definition Common Syntax exists, DEFINE clause must exist. (the
+ * raw parser should have already checked it.)
+ */
+ Assert(windef->rpCommonSyntax->rpDefs != NULL);
+
+ /* Validate PATTERN variable count (max RPR_VARID_MAX) */
+ validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
+ &patternVarNames);
+
+ /*
+ * Check for duplicate row pattern definition variables. The standard
+ * requires that no two row pattern definition variable names shall be
+ * equivalent.
+ */
+ restargets = NIL;
+ foreach(lc, windef->rpCommonSyntax->rpDefs)
+ {
+ TargetEntry *te,
+ *teDefine;
+
+ restarget = (ResTarget *) lfirst(lc);
+ name = restarget->name;
+
+ foreach(l, restargets)
+ {
+ char *n;
+
+ r = (ResTarget *) lfirst(l);
+ n = r->name;
+
+ if (!strcmp(n, name))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("row pattern definition variable name \"%s\" appears more than once in DEFINE clause",
+ name),
+ parser_errposition(pstate, exprLocation((Node *) r))));
+ }
+
+ restargets = lappend(restargets, restarget);
+
+ /*
+ * Add DEFINE expression (Restarget->val) to the targetlist as a
+ * TargetEntry if it does not exist yet. Planner will add the column
+ * ref var node to the outer plan's target list later on. This makes
+ * DEFINE expression could access the outer tuple while evaluating
+ * PATTERN.
+ *
+ * Note: findTargetlistEntrySQL99 does Expr transformation and clobber
+ * restarget->val.
+ */
+
+ /*
+ * Save the original expression location before transformation.
+ * findTargetlistEntrySQL99 may return an existing TargetEntry whose
+ * location points to where it was originally created (e.g., ORDER
+ * BY), not the DEFINE clause. We need to preserve the DEFINE location
+ * for accurate error reporting.
+ */
+ {
+ int defineExprLocation = exprLocation(restarget->val);
+
+ te = findTargetlistEntrySQL99(pstate, restarget->val,
+ targetlist, EXPR_KIND_RPR_DEFINE);
+
+ /* -------------------
+ * Copy the TargetEntry for defineClause and always set the pattern
+ * variable name. We use copyObject so the original targetlist entry
+ * is not modified.
+ *
+ * Note: We must always set resname to the pattern variable name.
+ * findTargetlistEntrySQL99 creates new TEs with resname = NULL
+ * (resjunk entries), but returns existing TEs unchanged when the
+ * expression already exists in targetlist.
+ *
+ * Example: "SELECT id, flag, ... WINDOW w AS (... DEFINE T AS flag)"
+ *
+ * 1. SELECT list processing creates: TE{resname="flag", expr=flag}
+ * 2. DEFINE T AS flag: findTargetlistEntrySQL99 finds existing TE
+ * 3. te->resname is "flag" (from SELECT), not NULL
+ * 4. Without unconditionally setting resname, teDefine->resname
+ * would remain "flag" instead of pattern variable name "T"
+ * 5. buildRPRPattern builds defineVariableList from resname, so
+ * it would contain ["flag"] instead of ["T"]
+ * 6. Pattern variable "T" not found -> Assert failure crash
+ */
+ teDefine = copyObject(te);
+ teDefine->resname = pstrdup(name);
+
+ /*
+ * Update the expression location to point to the DEFINE clause.
+ * This ensures error messages reference the correct source
+ * location.
+ */
+ if (defineExprLocation >= 0 && IsA(teDefine->expr, Var))
+ ((Var *) teDefine->expr)->location = defineExprLocation;
+ }
+
+ /* build transformed DEFINE clause (list of TargetEntry) */
+ defineClause = lappend(defineClause, teDefine);
+ }
+ list_free(restargets);
+
+ /*
+ * Make sure that the row pattern definition search condition is a boolean
+ * expression.
+ */
+ foreach_ptr(TargetEntry, te, defineClause)
+ (void) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");
+
+ /* mark column origins */
+ markTargetListOrigins(pstate, defineClause);
+
+ /* mark all nodes in the DEFINE clause tree with collation information */
+ assign_expr_collations(pstate, (Node *) defineClause);
+
+ return defineClause;
+}
diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h
index fe234611007..8aaac881f2b 100644
--- a/src/include/parser/parse_clause.h
+++ b/src/include/parser/parse_clause.h
@@ -52,6 +52,9 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle,
extern Index assignSortGroupRef(TargetEntry *tle, List *tlist);
extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList);
+extern TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node,
+ List **tlist, ParseExprKind exprKind);
+
/* functions in parse_jsontable.c */
extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt);
diff --git a/src/include/parser/parse_rpr.h b/src/include/parser/parse_rpr.h
new file mode 100644
index 00000000000..7fab6f292aa
--- /dev/null
+++ b/src/include/parser/parse_rpr.h
@@ -0,0 +1,22 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_rpr.h
+ * handle Row Pattern Recognition in parser
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/parser/parse_rpr.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PARSE_RPR_H
+#define PARSE_RPR_H
+
+#include "parser/parse_node.h"
+
+extern void transformRPR(ParseState *pstate, WindowClause *wc,
+ WindowDef *windef, List **targetlist);
+
+#endif /* PARSE_RPR_H */
--
2.43.0
[application/octet-stream] v43-0003-Row-pattern-recognition-patch-rewriter.patch (5.8K, 4-v43-0003-Row-pattern-recognition-patch-rewriter.patch)
download | inline diff:
From dac0f9a109db2366cf6abe42cbdef63285c92cfb Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sun, 15 Feb 2026 17:47:48 +0900
Subject: [PATCH v43 3/8] Row pattern recognition patch (rewriter).
---
src/backend/utils/adt/ruleutils.c | 175 ++++++++++++++++++++++++++++++
1 file changed, 175 insertions(+)
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 89cbdd3b1e7..6e008ed28a8 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -439,6 +439,12 @@ static void get_rule_groupingset(GroupingSet *gset, List *targetlist,
bool omit_parens, deparse_context *context);
static void get_rule_orderby(List *orderList, List *targetList,
bool force_colno, deparse_context *context);
+static void append_pattern_quantifier(StringInfo buf, RPRPatternNode *node);
+static void get_rule_pattern_node(RPRPatternNode *node, deparse_context *context);
+static void get_rule_pattern(RPRPatternNode *rpPattern, bool force_colno,
+ deparse_context *context);
+static void get_rule_define(List *defineClause, bool force_colno,
+ deparse_context *context);
static void get_rule_windowclause(Query *query, deparse_context *context);
static void get_rule_windowspec(WindowClause *wc, List *targetList,
deparse_context *context);
@@ -6746,6 +6752,130 @@ get_rule_orderby(List *orderList, List *targetList,
}
}
+/*
+ * Helper function to append quantifier string for pattern node
+ */
+static void
+append_pattern_quantifier(StringInfo buf, RPRPatternNode *node)
+{
+ bool has_quantifier = true;
+
+ if (node->min == 1 && node->max == 1)
+ {
+ /* {1,1} = no quantifier */
+ has_quantifier = false;
+ }
+ else if (node->min == 0 && node->max == INT_MAX)
+ appendStringInfoChar(buf, '*');
+ else if (node->min == 1 && node->max == INT_MAX)
+ appendStringInfoChar(buf, '+');
+ else if (node->min == 0 && node->max == 1)
+ appendStringInfoChar(buf, '?');
+ else if (node->max == INT_MAX)
+ appendStringInfo(buf, "{%d,}", node->min);
+ else if (node->min == node->max)
+ appendStringInfo(buf, "{%d}", node->min);
+ else
+ appendStringInfo(buf, "{%d,%d}", node->min, node->max);
+
+ if (node->reluctant >= 0)
+ {
+ if (!has_quantifier)
+ appendStringInfo(buf, "{1}"); /* make reluctant ? unambiguous */
+ appendStringInfoChar(buf, '?');
+ }
+}
+
+/*
+ * Recursive helper to display RPRPatternNode tree
+ */
+static void
+get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
+{
+ StringInfo buf = context->buf;
+ ListCell *lc;
+ const char *sep;
+
+ Assert(node != NULL);
+
+ switch (node->nodeType)
+ {
+ case RPR_PATTERN_VAR:
+ appendStringInfoString(buf, node->varName);
+ append_pattern_quantifier(buf, node);
+ break;
+
+ case RPR_PATTERN_SEQ:
+ sep = "";
+ foreach(lc, node->children)
+ {
+ appendStringInfoString(buf, sep);
+ get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+ sep = " ";
+ }
+ break;
+
+ case RPR_PATTERN_ALT:
+ sep = "";
+ foreach(lc, node->children)
+ {
+ appendStringInfoString(buf, sep);
+ get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+ sep = " | ";
+ }
+ break;
+
+ case RPR_PATTERN_GROUP:
+ appendStringInfoChar(buf, '(');
+ sep = "";
+ foreach(lc, node->children)
+ {
+ appendStringInfoString(buf, sep);
+ get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+ sep = " ";
+ }
+ appendStringInfoChar(buf, ')');
+ append_pattern_quantifier(buf, node);
+ break;
+ }
+}
+
+/*
+ * Display a PATTERN clause.
+ */
+static void
+get_rule_pattern(RPRPatternNode *rpPattern, bool force_colno,
+ deparse_context *context)
+{
+ StringInfo buf = context->buf;
+
+ appendStringInfoChar(buf, '(');
+ get_rule_pattern_node(rpPattern, context);
+ appendStringInfoChar(buf, ')');
+}
+
+/*
+ * Display a DEFINE clause.
+ */
+static void
+get_rule_define(List *defineClause, bool force_colno, deparse_context *context)
+{
+ StringInfo buf = context->buf;
+ const char *sep;
+ ListCell *lc_def;
+
+ sep = " ";
+
+ foreach(lc_def, defineClause)
+ {
+ TargetEntry *te = (TargetEntry *) lfirst(lc_def);
+
+ appendStringInfo(buf, "%s%s AS ", sep, te->resname);
+ get_rule_expr((Node *) te->expr, context, false);
+ sep = ",\n ";
+ }
+}
+
/*
* Display a WINDOW clause.
*
@@ -6826,6 +6956,7 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
get_rule_orderby(wc->orderClause, targetList, false, context);
needspace = true;
}
+
/* framing clause is never inherited, so print unless it's default */
if (wc->frameOptions & FRAMEOPTION_NONDEFAULT)
{
@@ -6834,7 +6965,51 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
get_window_frame_options(wc->frameOptions,
wc->startOffset, wc->endOffset,
context);
+ needspace = true;
+ }
+
+ /* RPR */
+ if (wc->rpSkipTo == ST_NEXT_ROW)
+ {
+ if (needspace)
+ appendStringInfoChar(buf, ' ');
+ appendStringInfoString(buf,
+ "\n AFTER MATCH SKIP TO NEXT ROW ");
+ needspace = true;
+ }
+ else if (wc->rpSkipTo == ST_PAST_LAST_ROW)
+ {
+ if (needspace)
+ appendStringInfoChar(buf, ' ');
+ appendStringInfoString(buf,
+ "\n AFTER MATCH SKIP PAST LAST ROW ");
+ needspace = true;
+ }
+ if (wc->initial)
+ {
+ if (needspace)
+ appendStringInfoChar(buf, ' ');
+ appendStringInfoString(buf, "\n INITIAL");
+ needspace = true;
}
+ if (wc->rpPattern)
+ {
+ if (needspace)
+ appendStringInfoChar(buf, ' ');
+ appendStringInfoString(buf, "\n PATTERN ");
+ get_rule_pattern(wc->rpPattern, false, context);
+ needspace = true;
+ }
+
+ if (wc->defineClause)
+ {
+ if (needspace)
+ appendStringInfoChar(buf, ' ');
+ appendStringInfoString(buf, "\n DEFINE\n");
+ get_rule_define(wc->defineClause, false, context);
+ appendStringInfoChar(buf, ' ');
+ }
+
appendStringInfoChar(buf, ')');
}
--
2.43.0
[application/octet-stream] v43-0004-Row-pattern-recognition-patch-planner.patch (65.5K, 5-v43-0004-Row-pattern-recognition-patch-planner.patch)
download | inline diff:
From d7eb969bd2da3bfd513391f4595871fb5a39c803 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sun, 15 Feb 2026 17:47:48 +0900
Subject: [PATCH v43 4/8] Row pattern recognition patch (planner).
---
src/backend/optimizer/plan/Makefile | 1 +
src/backend/optimizer/plan/createplan.c | 76 +-
src/backend/optimizer/plan/meson.build | 1 +
src/backend/optimizer/plan/planner.c | 3 +
src/backend/optimizer/plan/rpr.c | 1806 +++++++++++++++++++++
src/backend/optimizer/plan/setrefs.c | 27 +-
src/backend/optimizer/prep/prepjointree.c | 9 +
src/include/nodes/plannodes.h | 74 +
src/include/optimizer/rpr.h | 57 +
9 files changed, 2036 insertions(+), 18 deletions(-)
create mode 100644 src/backend/optimizer/plan/rpr.c
create mode 100644 src/include/optimizer/rpr.h
diff --git a/src/backend/optimizer/plan/Makefile b/src/backend/optimizer/plan/Makefile
index 80ef162e484..7e0167789d8 100644
--- a/src/backend/optimizer/plan/Makefile
+++ b/src/backend/optimizer/plan/Makefile
@@ -19,6 +19,7 @@ OBJS = \
planagg.o \
planmain.o \
planner.o \
+ rpr.o \
setrefs.o \
subselect.o
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 21f1988cf22..bbc2c7e71f4 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -35,6 +35,7 @@
#include "optimizer/prep.h"
#include "optimizer/restrictinfo.h"
#include "optimizer/subselect.h"
+#include "optimizer/rpr.h"
#include "optimizer/tlist.h"
#include "parser/parse_clause.h"
#include "parser/parsetree.h"
@@ -287,7 +288,10 @@ static Memoize *make_memoize(Plan *lefttree, Oid *hashoperators,
static WindowAgg *make_windowagg(List *tlist, WindowClause *wc,
int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
- List *runCondition, List *qual, bool topWindow,
+ List *runCondition, RPSkipTo rpSkipTo,
+ RPRPattern *compiledPattern,
+ List *defineClause,
+ List *qual, bool topWindow,
Plan *lefttree);
static Group *make_group(List *tlist, List *qual, int numGroupCols,
AttrNumber *grpColIdx, Oid *grpOperators, Oid *grpCollations,
@@ -2530,21 +2534,50 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
ordNumCols++;
}
- /* And finally we can make the WindowAgg node */
- plan = make_windowagg(tlist,
- wc,
- partNumCols,
- partColIdx,
- partOperators,
- partCollations,
- ordNumCols,
- ordColIdx,
- ordOperators,
- ordCollations,
- best_path->runCondition,
- best_path->qual,
- best_path->topwindow,
- subplan);
+ /* Build RPR pattern and filter defineClause */
+ {
+ List *defineVariableList = NIL;
+ List *filteredDefineClause = NIL;
+ RPRPattern *compiledPattern = NULL;
+
+ if (wc->rpPattern)
+ {
+ List *patternVars;
+
+ /*
+ * Filter defineClause to include only variables used in PATTERN.
+ * This eliminates unnecessary DEFINE evaluations at runtime.
+ */
+ patternVars = collectPatternVariables(wc->rpPattern);
+ filteredDefineClause = filterDefineClause(wc->defineClause,
+ patternVars,
+ &defineVariableList);
+
+ compiledPattern = buildRPRPattern(wc->rpPattern,
+ defineVariableList,
+ wc->rpSkipTo,
+ wc->frameOptions);
+ }
+
+ /* And finally we can make the WindowAgg node */
+ plan = make_windowagg(tlist,
+ wc,
+ partNumCols,
+ partColIdx,
+ partOperators,
+ partCollations,
+ ordNumCols,
+ ordColIdx,
+ ordOperators,
+ ordCollations,
+ best_path->runCondition,
+ wc->rpSkipTo,
+ compiledPattern,
+ filteredDefineClause,
+ best_path->qual,
+ best_path->topwindow,
+ subplan);
+ }
copy_generic_path_info(&plan->plan, (Path *) best_path);
@@ -6611,7 +6644,10 @@ static WindowAgg *
make_windowagg(List *tlist, WindowClause *wc,
int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
- List *runCondition, List *qual, bool topWindow, Plan *lefttree)
+ List *runCondition, RPSkipTo rpSkipTo,
+ RPRPattern *compiledPattern,
+ List *defineClause,
+ List *qual, bool topWindow, Plan *lefttree)
{
WindowAgg *node = makeNode(WindowAgg);
Plan *plan = &node->plan;
@@ -6638,6 +6674,12 @@ make_windowagg(List *tlist, WindowClause *wc,
node->inRangeAsc = wc->inRangeAsc;
node->inRangeNullsFirst = wc->inRangeNullsFirst;
node->topWindow = topWindow;
+ node->rpSkipTo = rpSkipTo;
+
+ /* Store compiled pattern for NFA execution */
+ node->rpPattern = compiledPattern;
+
+ node->defineClause = defineClause;
plan->targetlist = tlist;
plan->lefttree = lefttree;
diff --git a/src/backend/optimizer/plan/meson.build b/src/backend/optimizer/plan/meson.build
index c565b2adbcc..5b2381cd774 100644
--- a/src/backend/optimizer/plan/meson.build
+++ b/src/backend/optimizer/plan/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
'planagg.c',
'planmain.c',
'planner.c',
+ 'rpr.c',
'setrefs.c',
'subselect.c',
)
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 006b3281969..3950e0dd1af 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1014,6 +1014,9 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
EXPRKIND_LIMIT);
wc->endOffset = preprocess_expression(root, wc->endOffset,
EXPRKIND_LIMIT);
+ wc->defineClause = (List *) preprocess_expression(root,
+ (Node *) wc->defineClause,
+ EXPRKIND_TARGET);
}
parse->limitOffset = preprocess_expression(root, parse->limitOffset,
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
new file mode 100644
index 00000000000..112ed034fe2
--- /dev/null
+++ b/src/backend/optimizer/plan/rpr.c
@@ -0,0 +1,1806 @@
+/*-------------------------------------------------------------------------
+ *
+ * rpr.c
+ * Row Pattern Recognition pattern compilation for planner
+ *
+ * This file contains functions for optimizing RPR pattern AST and
+ * compiling it to bytecode for execution by WindowAgg.
+ *
+ * Key components:
+ * 1. Pattern Optimization: Simplifies patterns before compilation
+ * (e.g., flatten nested SEQ/ALT, merge consecutive vars)
+ * 2. Pattern Compilation: Converts AST to flat element array for NFA
+ * 3. Absorption Analysis: Computes flags for O(n^2)->O(n) optimization
+ *
+ * Context Absorption Optimization:
+ * When a pattern starts with a greedy unbounded element (e.g., A+ or (A B)+),
+ * newer contexts cannot produce longer matches than older contexts.
+ * By absorbing (eliminating) redundant newer contexts, we reduce
+ * complexity from O(n^2) to O(n) for patterns like A+ B.
+ *
+ * The absorption analysis uses two element flags:
+ * - RPR_ELEM_ABSORBABLE: marks WHERE to compare (judgment point)
+ * - RPR_ELEM_ABSORBABLE_BRANCH: marks the absorbable region
+ *
+ * See computeAbsorbability() and the detailed comments before
+ * isUnboundedStart() for the full design explanation.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/optimizer/plan/rpr.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "nodes/makefuncs.h"
+#include "optimizer/rpr.h"
+
+/* Forward declarations - pattern comparison */
+static bool rprPatternEqual(RPRPatternNode *a, RPRPatternNode *b);
+static bool rprPatternChildrenEqual(List *a, List *b);
+
+/* Forward declarations - pattern optimization (shared) */
+static RPRPatternNode *tryUnwrapSingleChild(RPRPatternNode *pattern);
+
+/* Forward declarations - SEQ optimization */
+static List *flattenSeqChildren(List *children);
+static List *mergeConsecutiveVars(List *children);
+static List *mergeConsecutiveGroups(List *children);
+static List *mergeConsecutiveAlts(List *children);
+static List *mergeGroupPrefixSuffix(List *children);
+static RPRPatternNode *optimizeSeqPattern(RPRPatternNode *pattern);
+
+/* Forward declarations - ALT optimization */
+static List *flattenAltChildren(List *children);
+static List *removeDuplicateAlternatives(List *children);
+static RPRPatternNode *optimizeAltPattern(RPRPatternNode *pattern);
+
+/* Forward declarations - GROUP optimization */
+static RPRPatternNode *tryMultiplyQuantifiers(RPRPatternNode *pattern);
+static RPRPatternNode *tryUnwrapGroup(RPRPatternNode *pattern);
+static RPRPatternNode *optimizeGroupPattern(RPRPatternNode *pattern);
+
+/* Forward declarations - optimization dispatcher */
+static RPRPatternNode *optimizeRPRPattern(RPRPatternNode *pattern);
+
+/* Forward declarations - pattern compilation */
+static int collectDefineVariables(List *defineVariableList, char **varNames);
+static void scanRPRPatternRecursive(RPRPatternNode *node, char **varNames,
+ int *numVars, int *numElements,
+ RPRDepth depth, RPRDepth *maxDepth);
+static void scanRPRPattern(RPRPatternNode *node, char **varNames, int *numVars,
+ int *numElements, RPRDepth *maxDepth);
+static RPRPattern *allocateRPRPattern(int numVars, int numElements,
+ RPRDepth maxDepth, char **varNamesStack);
+static RPRVarId getVarIdFromPattern(RPRPattern *pat, const char *varName);
+static void fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat,
+ int *idx, RPRDepth depth);
+static void fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat,
+ int *idx, RPRDepth depth);
+static void fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat,
+ int *idx, RPRDepth depth);
+static void fillRPRPattern(RPRPatternNode *node, RPRPattern *pat,
+ int *idx, RPRDepth depth);
+static void finalizeRPRPattern(RPRPattern *result);
+
+/* Forward declarations - context absorption */
+static bool isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx);
+static void computeAbsorbabilityRecursive(RPRPattern *pattern,
+ RPRElemIdx startIdx,
+ bool *hasAbsorbable);
+static void computeAbsorbability(RPRPattern *pattern);
+
+/*
+ * rprPatternEqual
+ * Compare two RPRPatternNode trees for equality.
+ *
+ * Returns true if the trees are structurally identical.
+ */
+static bool
+rprPatternEqual(RPRPatternNode *a, RPRPatternNode *b)
+{
+ /* Pattern nodes in children lists must never be NULL */
+ Assert(a != NULL && b != NULL);
+
+ /* Must have same node type and quantifiers */
+ if (a->nodeType != b->nodeType)
+ return false;
+ if (a->min != b->min || a->max != b->max)
+ return false;
+ if (a->reluctant != b->reluctant)
+ return false;
+
+ switch (a->nodeType)
+ {
+ case RPR_PATTERN_VAR:
+ return strcmp(a->varName, b->varName) == 0;
+
+ case RPR_PATTERN_SEQ:
+ case RPR_PATTERN_ALT:
+ case RPR_PATTERN_GROUP:
+ return rprPatternChildrenEqual(a->children, b->children);
+ }
+
+ return false; /* keep compiler quiet */
+}
+
+/*
+ * rprPatternChildrenEqual
+ * Compare children lists of two pattern nodes for equality.
+ *
+ * Returns true if the children lists are structurally identical.
+ */
+static bool
+rprPatternChildrenEqual(List *a, List *b)
+{
+ ListCell *lca,
+ *lcb;
+
+ if (list_length(a) != list_length(b))
+ return false;
+
+ forboth(lca, a, lcb, b)
+ {
+ if (!rprPatternEqual((RPRPatternNode *) lfirst(lca),
+ (RPRPatternNode *) lfirst(lcb)))
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * tryUnwrapSingleChild
+ * Try to unwrap pattern node with single child.
+ *
+ * Examples (internal node representation):
+ * SEQ[A] -> A (single-element sequence becomes the element)
+ * ALT[A] -> A (single-alternative becomes the alternative)
+ *
+ * If pattern has exactly one child, return the child directly.
+ * Otherwise returns the pattern unchanged.
+ * Used by both SEQ and ALT optimization.
+ */
+static RPRPatternNode *
+tryUnwrapSingleChild(RPRPatternNode *pattern)
+{
+ if (list_length(pattern->children) == 1)
+ return (RPRPatternNode *) linitial(pattern->children);
+
+ return pattern;
+}
+
+/*
+ * flattenSeqChildren
+ * Recursively optimize children and flatten nested SEQ.
+ *
+ * Example:
+ * SEQ(A, SEQ(B, C)) -> SEQ(A, B, C)
+ *
+ * Returns a new list with optimized children, with nested SEQ children
+ * flattened into the parent list.
+ */
+static List *
+flattenSeqChildren(List *children)
+{
+ ListCell *lc;
+ List *newChildren = NIL;
+
+ foreach(lc, children)
+ {
+ RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+ RPRPatternNode *opt = optimizeRPRPattern(child);
+
+ /* GROUP{1,1} should have been unwrapped by optimizeGroupPattern */
+ Assert(!(opt->nodeType == RPR_PATTERN_GROUP &&
+ opt->min == 1 && opt->max == 1 && opt->reluctant < 0));
+
+ if (opt->nodeType == RPR_PATTERN_SEQ)
+ {
+ newChildren = list_concat(newChildren,
+ list_copy(opt->children));
+ }
+ else
+ {
+ newChildren = lappend(newChildren, opt);
+ }
+ }
+
+ return newChildren;
+}
+
+/*
+ * mergeConsecutiveVars
+ * Merge consecutive identical VAR nodes.
+ *
+ * Examples:
+ * A{m1,M1} A{m2,M2} -> A{m1+m2, M1+M2} where INF + x = INF.
+ *
+ * Only merges non-reluctant VAR nodes with the same variable name.
+ */
+static List *
+mergeConsecutiveVars(List *children)
+{
+ ListCell *lc;
+ List *mergedChildren = NIL;
+ RPRPatternNode *prev = NULL;
+
+ foreach(lc, children)
+ {
+ RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+
+ if (child->nodeType == RPR_PATTERN_VAR && child->reluctant < 0)
+ {
+ /* ----------------------
+ * Can merge consecutive VAR nodes if:
+ * 1. Same variable name
+ * 2. No min overflow: prev->min + child->min <= INF
+ * 3. No max overflow: prev->max + child->max <= INF (or either is INF)
+ */
+ if (prev != NULL &&
+ strcmp(prev->varName, child->varName) == 0 &&
+ prev->min <= RPR_QUANTITY_INF - child->min &&
+ (prev->max <= RPR_QUANTITY_INF - child->max ||
+ prev->max == RPR_QUANTITY_INF ||
+ child->max == RPR_QUANTITY_INF))
+ {
+ /*
+ * Merge: accumulate min/max into prev. prev is guaranteed to
+ * be a non-reluctant VAR by the outer condition.
+ */
+ Assert(prev->nodeType == RPR_PATTERN_VAR && prev->reluctant < 0);
+
+ prev->min += child->min;
+
+ if (prev->max == RPR_QUANTITY_INF ||
+ child->max == RPR_QUANTITY_INF)
+ prev->max = RPR_QUANTITY_INF;
+ else
+ prev->max += child->max;
+ }
+ else
+ {
+ /* Flush previous and start new */
+ if (prev != NULL)
+ mergedChildren = lappend(mergedChildren, prev);
+ prev = child;
+ }
+ }
+ else
+ {
+ /* Non-mergeable - flush previous */
+ if (prev != NULL)
+ mergedChildren = lappend(mergedChildren, prev);
+ mergedChildren = lappend(mergedChildren, child);
+ prev = NULL;
+ }
+ }
+
+ /* Flush remaining */
+ if (prev != NULL)
+ mergedChildren = lappend(mergedChildren, prev);
+
+ return mergedChildren;
+}
+
+/*
+ * mergeConsecutiveGroups
+ * Merge consecutive identical GROUP nodes.
+ *
+ * Example:
+ * (A B)+ (A B)+ -> (A B){2,}
+ *
+ * Only merges non-reluctant GROUP nodes with identical children.
+ */
+static List *
+mergeConsecutiveGroups(List *children)
+{
+ ListCell *lc;
+ List *mergedChildren = NIL;
+ RPRPatternNode *prev = NULL;
+
+ foreach(lc, children)
+ {
+ RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+
+ if (child->nodeType == RPR_PATTERN_GROUP && child->reluctant < 0)
+ {
+ /* ----------------------
+ * Can merge consecutive GROUP nodes if:
+ * 1. Identical children
+ * 2. No min overflow: prev->min + child->min <= INF
+ * 3. No max overflow: prev->max + child->max <= INF (or either is INF)
+ */
+ if (prev != NULL &&
+ rprPatternChildrenEqual(prev->children, child->children) &&
+ prev->min <= RPR_QUANTITY_INF - child->min &&
+ (prev->max <= RPR_QUANTITY_INF - child->max ||
+ prev->max == RPR_QUANTITY_INF ||
+ child->max == RPR_QUANTITY_INF))
+ {
+ /*
+ * Merge: accumulate min/max into prev. prev is guaranteed to
+ * be a non-reluctant GROUP by the outer condition.
+ */
+ Assert(prev->nodeType == RPR_PATTERN_GROUP && prev->reluctant < 0);
+
+ prev->min += child->min;
+
+ if (prev->max == RPR_QUANTITY_INF ||
+ child->max == RPR_QUANTITY_INF)
+ prev->max = RPR_QUANTITY_INF;
+ else
+ prev->max += child->max;
+ }
+ else
+ {
+ /* Flush previous and start new */
+ if (prev != NULL)
+ mergedChildren = lappend(mergedChildren, prev);
+ prev = child;
+ }
+ }
+ else
+ {
+ /* Non-mergeable - flush previous */
+ if (prev != NULL)
+ mergedChildren = lappend(mergedChildren, prev);
+ mergedChildren = lappend(mergedChildren, child);
+ prev = NULL;
+ }
+ }
+
+ /* Flush remaining */
+ if (prev != NULL)
+ mergedChildren = lappend(mergedChildren, prev);
+
+ return mergedChildren;
+}
+
+/*
+ * mergeConsecutiveAlts
+ * Merge consecutive identical ALT nodes into a GROUP.
+ *
+ * Example:
+ * (A | B) (A | B) (A | B) -> (A | B){3}
+ *
+ * After GROUP{1,1} unwrap, bare alternations like (A | B) become ALT nodes
+ * in the SEQ. This step detects consecutive identical ALT nodes and wraps
+ * them in a GROUP with the appropriate quantifier.
+ */
+static List *
+mergeConsecutiveAlts(List *children)
+{
+ ListCell *lc;
+ List *mergedChildren = NIL;
+ RPRPatternNode *prev = NULL;
+ int count = 0;
+
+ foreach(lc, children)
+ {
+ RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+
+ if (child->nodeType == RPR_PATTERN_ALT && child->reluctant < 0)
+ {
+ if (prev != NULL &&
+ rprPatternChildrenEqual(prev->children, child->children))
+ {
+ /* Same ALT as prev - accumulate */
+ count++;
+ }
+ else
+ {
+ /* Different ALT or first ALT - flush previous */
+ if (prev != NULL)
+ {
+ if (count > 1)
+ {
+ /* Wrap in GROUP{count,count}(ALT) */
+ RPRPatternNode *group = makeNode(RPRPatternNode);
+
+ group->nodeType = RPR_PATTERN_GROUP;
+ group->min = count;
+ group->max = count;
+ group->reluctant = -1;
+ group->location = -1;
+ group->children = list_make1(prev);
+ mergedChildren = lappend(mergedChildren, group);
+ }
+ else
+ mergedChildren = lappend(mergedChildren, prev);
+ }
+ prev = child;
+ count = 1;
+ }
+ }
+ else
+ {
+ /* Non-ALT - flush previous */
+ if (prev != NULL)
+ {
+ if (count > 1)
+ {
+ RPRPatternNode *group = makeNode(RPRPatternNode);
+
+ group->nodeType = RPR_PATTERN_GROUP;
+ group->min = count;
+ group->max = count;
+ group->reluctant = -1;
+ group->location = -1;
+ group->children = list_make1(prev);
+ mergedChildren = lappend(mergedChildren, group);
+ }
+ else
+ mergedChildren = lappend(mergedChildren, prev);
+ }
+ mergedChildren = lappend(mergedChildren, child);
+ prev = NULL;
+ count = 0;
+ }
+ }
+
+ /* Flush remaining */
+ if (prev != NULL)
+ {
+ if (count > 1)
+ {
+ RPRPatternNode *group = makeNode(RPRPatternNode);
+
+ group->nodeType = RPR_PATTERN_GROUP;
+ group->min = count;
+ group->max = count;
+ group->reluctant = -1;
+ group->location = -1;
+ group->children = list_make1(prev);
+ mergedChildren = lappend(mergedChildren, group);
+ }
+ else
+ mergedChildren = lappend(mergedChildren, prev);
+ }
+
+ return mergedChildren;
+}
+
+/*
+ * mergeGroupPrefixSuffix
+ * Merge sequence prefix/suffix into GROUP with matching children.
+ *
+ * Examples:
+ * A B (A B)+ -> (A B){2,}
+ * (A B)+ A B -> (A B){2,}
+ * A B (A B)+ A B -> (A B){3,}
+ */
+static List *
+mergeGroupPrefixSuffix(List *children)
+{
+ List *result = NIL;
+ int numChildren = list_length(children);
+ int i;
+ int skipUntil = -1; /* skip suffix elements already absorbed */
+
+ for (i = 0; i < numChildren; i++)
+ {
+ RPRPatternNode *child = (RPRPatternNode *) list_nth(children, i);
+
+ /*
+ * The suffix absorption logic below adjusts i to skip absorbed
+ * elements, ensuring we never revisit them. Verify this invariant.
+ */
+ Assert(i >= skipUntil);
+
+ /*
+ * If this is a GROUP, see if preceding/following elements match its
+ * children. GROUP's content may be wrapped in a SEQ - unwrap for
+ * comparison.
+ */
+ if (child->nodeType == RPR_PATTERN_GROUP && child->reluctant < 0)
+ {
+ List *groupContent = child->children;
+ int groupChildCount;
+ int prefixLen = list_length(result);
+
+ /*
+ * If GROUP has single SEQ child, compare with SEQ's children.
+ * e.g., (A B)+ internally contains sequence A B; compare against
+ * that.
+ */
+ if (list_length(groupContent) == 1)
+ {
+ RPRPatternNode *inner = (RPRPatternNode *) linitial(groupContent);
+
+ if (inner->nodeType == RPR_PATTERN_SEQ)
+ groupContent = inner->children;
+ }
+
+ groupChildCount = list_length(groupContent);
+
+ /*
+ * PREFIX MERGE: Check if preceding elements match. Keep absorbing
+ * as long as we have matching prefixes.
+ */
+ while (prefixLen >= groupChildCount && groupChildCount > 0)
+ {
+ List *prefixElements = NIL;
+ int j;
+
+ /* Extract last groupChildCount elements from prefix */
+ for (j = prefixLen - groupChildCount; j < prefixLen; j++)
+ {
+ prefixElements = lappend(prefixElements,
+ list_nth(result, j));
+ }
+
+ /* Compare with GROUP's (possibly unwrapped) children */
+ if (rprPatternChildrenEqual(prefixElements, groupContent) &&
+ child->min < RPR_QUANTITY_INF)
+ {
+ /*
+ * Match! Merge by incrementing GROUP's min. Remove the
+ * prefix elements from output.
+ */
+ child->min += 1;
+
+ /* Rebuild result without matched prefix */
+ {
+ List *trimmed = NIL;
+
+ for (j = 0; j < prefixLen - groupChildCount; j++)
+ {
+ trimmed = lappend(trimmed,
+ list_nth(result, j));
+ }
+ result = trimmed;
+ prefixLen = list_length(result);
+ }
+ }
+ else
+ {
+ list_free(prefixElements);
+ break;
+ }
+
+ list_free(prefixElements);
+ }
+
+ /*
+ * SUFFIX MERGE: Check if following elements match. Keep absorbing
+ * as long as we have matching suffixes.
+ */
+ while (i + groupChildCount < numChildren && groupChildCount > 0)
+ {
+ List *suffixElements = NIL;
+ int j;
+ int suffixStart = i + 1;
+
+ /* suffixStart always >= skipUntil after i adjustment */
+ Assert(skipUntil <= suffixStart);
+
+ /* Extract next groupChildCount elements as suffix */
+ for (j = 0; j < groupChildCount; j++)
+ {
+ int idx = suffixStart + j;
+
+ /* while condition guarantees idx < numChildren */
+ Assert(idx < numChildren);
+ suffixElements = lappend(suffixElements,
+ list_nth(children, idx));
+ }
+
+ /* Compare with GROUP's children */
+ if (list_length(suffixElements) == groupChildCount &&
+ rprPatternChildrenEqual(suffixElements, groupContent) &&
+ child->min < RPR_QUANTITY_INF)
+ {
+ /*
+ * Match! Absorb suffix by incrementing min and skipping.
+ */
+ child->min += 1;
+ skipUntil = suffixStart + groupChildCount;
+
+ /*
+ * Update i to continue suffix check after absorbed
+ * elements
+ */
+ i = skipUntil - 1;
+ }
+ else
+ {
+ list_free(suffixElements);
+ break;
+ }
+
+ list_free(suffixElements);
+ }
+ }
+
+ result = lappend(result, child);
+ }
+
+ return result;
+}
+
+/*
+ * optimizeSeqPattern
+ * Optimize SEQ pattern node.
+ *
+ * Optimizations:
+ * 1. Flatten nested SEQ and GROUP{1,1}
+ * 2. Merge consecutive identical VAR nodes
+ * 3. Merge consecutive identical GROUP nodes
+ * 4. Merge consecutive identical ALT nodes into GROUP
+ * 5. Merge prefix/suffix into GROUP with matching children
+ * 6. Unwrap single-item SEQ
+ */
+static RPRPatternNode *
+optimizeSeqPattern(RPRPatternNode *pattern)
+{
+ /* Recursively optimize children and flatten nested SEQ/GROUP{1,1} */
+ pattern->children = flattenSeqChildren(pattern->children);
+
+ /* Merge consecutive identical VAR nodes */
+ pattern->children = mergeConsecutiveVars(pattern->children);
+
+ /* Merge consecutive identical GROUP nodes */
+ pattern->children = mergeConsecutiveGroups(pattern->children);
+
+ /* Merge consecutive identical ALT nodes into GROUP */
+ pattern->children = mergeConsecutiveAlts(pattern->children);
+
+ /* Merge prefix/suffix into GROUP with matching children */
+ pattern->children = mergeGroupPrefixSuffix(pattern->children);
+
+ /* Unwrap single-item SEQ */
+ return tryUnwrapSingleChild(pattern);
+}
+
+/*
+ * flattenAltChildren
+ * Recursively optimize children and flatten nested ALT nodes.
+ *
+ * Example:
+ * (A | (B | C)) -> (A | B | C)
+ *
+ * Returns a new list with optimized children, with nested ALT children
+ * flattened into the parent list.
+ */
+static List *
+flattenAltChildren(List *children)
+{
+ ListCell *lc;
+ List *newChildren = NIL;
+
+ foreach(lc, children)
+ {
+ RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+ RPRPatternNode *opt = optimizeRPRPattern(child);
+
+ if (opt->nodeType == RPR_PATTERN_ALT)
+ newChildren = list_concat(newChildren, list_copy(opt->children));
+ else
+ newChildren = lappend(newChildren, opt);
+ }
+
+ return newChildren;
+}
+
+/*
+ * removeDuplicateAlternatives
+ * Remove duplicate alternatives from a list.
+ *
+ * Examples:
+ * (A | B | A) -> (A | B)
+ * (X | Y | X | Z | Y) -> (X | Y | Z)
+ *
+ * Returns a new list with only unique children (first occurrence kept).
+ */
+static List *
+removeDuplicateAlternatives(List *children)
+{
+ ListCell *lc;
+ ListCell *lc2;
+ List *uniqueChildren = NIL;
+
+ foreach(lc, children)
+ {
+ RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+ bool isDuplicate = false;
+
+ foreach(lc2, uniqueChildren)
+ {
+ if (rprPatternEqual((RPRPatternNode *) lfirst(lc2), child))
+ {
+ isDuplicate = true;
+ break;
+ }
+ }
+
+ if (!isDuplicate)
+ uniqueChildren = lappend(uniqueChildren, child);
+ }
+
+ return uniqueChildren;
+}
+
+/*
+ * optimizeAltPattern
+ * Optimize ALT pattern node.
+ *
+ * Optimizations:
+ * 1. Flatten nested ALT
+ * 2. Remove duplicate alternatives
+ * 3. Unwrap single-item ALT
+ */
+static RPRPatternNode *
+optimizeAltPattern(RPRPatternNode *pattern)
+{
+ /* Recursively optimize children and flatten nested ALT */
+ pattern->children = flattenAltChildren(pattern->children);
+
+ /* Remove duplicate alternatives */
+ pattern->children = removeDuplicateAlternatives(pattern->children);
+
+ /* Unwrap single-item ALT */
+ return tryUnwrapSingleChild(pattern);
+}
+
+/*
+ * tryMultiplyQuantifiers
+ * Try to multiply quantifiers.
+ *
+ * Multiplication is SAFE when:
+ * 1. Both unbounded: (A*)* -> A*, (A+)+ -> A+
+ * 2. Outer exact: (A{m,n}){k} -> A{m*k, n*k}
+ * 3. Outer range + child {1,1}: (A){2,} -> A{2,}
+ *
+ * Multiplication is NOT safe when:
+ * - Only child unbounded: (A+){3} has different semantics
+ * - Outer range + child not {1,1}: gaps possible
+ * e.g., (A{2}){2,3} yields 4,6 only (not 4,5,6)
+ *
+ * Returns the child node with multiplied quantifiers if successful,
+ * otherwise returns the original pattern unchanged.
+ */
+static RPRPatternNode *
+tryMultiplyQuantifiers(RPRPatternNode *pattern)
+{
+ RPRPatternNode *child;
+ int64 new_min_64;
+ int64 new_max_64;
+
+ /* Parser always creates GROUP with exactly one child */
+ Assert(list_length(pattern->children) == 1);
+
+ if (pattern->reluctant >= 0)
+ return pattern;
+
+ child = (RPRPatternNode *) linitial(pattern->children);
+
+ if ((child->nodeType != RPR_PATTERN_VAR &&
+ child->nodeType != RPR_PATTERN_GROUP) ||
+ child->reluctant >= 0)
+ return pattern;
+
+ /* Case 1: Both unbounded - (A*)* -> A*, (A+)+ -> A+ */
+ if (child->max == RPR_QUANTITY_INF && pattern->max == RPR_QUANTITY_INF)
+ {
+ new_min_64 = (int64) child->min * pattern->min;
+ if (new_min_64 >= RPR_QUANTITY_INF)
+ return pattern; /* overflow, skip optimization */
+
+ child->min = (int) new_min_64;
+ child->max = RPR_QUANTITY_INF;
+ return child;
+ }
+
+ /*
+ * Case 2/3: Safe when child is finite AND (outer is exact OR child is
+ * {1,1})
+ */
+ if (child->max != RPR_QUANTITY_INF &&
+ (pattern->min == pattern->max ||
+ (child->min == 1 && child->max == 1)))
+ {
+ new_min_64 = (int64) pattern->min * child->min;
+ if (new_min_64 >= RPR_QUANTITY_INF)
+ return pattern;
+
+ if (pattern->max == RPR_QUANTITY_INF)
+ new_max_64 = RPR_QUANTITY_INF;
+ else
+ {
+ new_max_64 = (int64) pattern->max * child->max;
+
+ if (new_max_64 >= RPR_QUANTITY_INF)
+ return pattern;
+ }
+
+ child->min = (int) new_min_64;
+ child->max = (int) new_max_64;
+ return child;
+ }
+
+ /* Not safe to multiply */
+ return pattern;
+}
+
+/*
+ * tryUnwrapGroup
+ * Try to unwrap GROUP{1,1} node.
+ *
+ * Examples:
+ * (A){1,1} -> A
+ * (A B){1,1} -> SEQ(A, B) (unwraps the inner SEQ)
+ *
+ * If GROUP has min=1, max=1, and is not reluctant, return the child directly.
+ * Otherwise returns the pattern unchanged.
+ *
+ * Note: Parser always creates GROUP with exactly one child via list_make1().
+ */
+static RPRPatternNode *
+tryUnwrapGroup(RPRPatternNode *pattern)
+{
+ if (pattern->min != 1 || pattern->max != 1 || pattern->reluctant >= 0)
+ return pattern;
+
+ /* Parser always creates GROUP with single child */
+ Assert(list_length(pattern->children) == 1);
+ return (RPRPatternNode *) linitial(pattern->children);
+}
+
+/*
+ * optimizeGroupPattern
+ * Optimize GROUP pattern node.
+ *
+ * Optimizations:
+ * 1. Quantifier multiplication: (A{m}){n} -> A{m*n}
+ * 2. Unwrap GROUP{1,1}
+ */
+static RPRPatternNode *
+optimizeGroupPattern(RPRPatternNode *pattern)
+{
+ ListCell *lc;
+ List *newChildren;
+ RPRPatternNode *result;
+
+ /* Recursively optimize children */
+ newChildren = NIL;
+ foreach(lc, pattern->children)
+ {
+ RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+
+ newChildren = lappend(newChildren, optimizeRPRPattern(child));
+ }
+ pattern->children = newChildren;
+
+ /* Try quantifier multiplication */
+ result = tryMultiplyQuantifiers(pattern);
+ if (result != pattern)
+ return result;
+
+ /* Try unwrapping GROUP{1,1} */
+ return tryUnwrapGroup(pattern);
+}
+
+/*
+ * optimizeRPRPattern
+ * Optimize RPRPatternNode tree (dispatcher).
+ *
+ * Dispatches to type-specific optimization functions.
+ * Returns the optimized pattern (may be a different node).
+ */
+static RPRPatternNode *
+optimizeRPRPattern(RPRPatternNode *pattern)
+{
+ /* Pattern nodes from parser are never NULL */
+ Assert(pattern != NULL);
+
+ switch (pattern->nodeType)
+ {
+ case RPR_PATTERN_VAR:
+ return pattern;
+ case RPR_PATTERN_SEQ:
+ return optimizeSeqPattern(pattern);
+ case RPR_PATTERN_ALT:
+ return optimizeAltPattern(pattern);
+ case RPR_PATTERN_GROUP:
+ return optimizeGroupPattern(pattern);
+ }
+
+ return pattern; /* keep compiler quiet */
+}
+
+/*
+ * collectDefineVariables
+ * Collect variable names from DEFINE clause.
+ *
+ * Populates varNames array with variable names in DEFINE order.
+ * This ensures varId == defineIdx, eliminating runtime mapping.
+ * Returns the number of variables collected.
+ */
+static int
+collectDefineVariables(List *defineVariableList, char **varNames)
+{
+ ListCell *lc;
+ int numVars = 0;
+
+ foreach(lc, defineVariableList)
+ {
+ /* Parser already checked this limit in transformDefineClause */
+ Assert(numVars < RPR_VARID_MAX);
+
+ varNames[numVars++] = strVal(lfirst(lc));
+ }
+
+ return numVars;
+}
+
+/*
+ * scanRPRPatternRecursive
+ * Recursively scan pattern AST (pass 1 internal).
+ *
+ * Collects unique variable names and counts elements while tracking depth.
+ * Variables from DEFINE clause are already in varNames; this adds any
+ * additional variables found in the pattern.
+ */
+static void
+scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
+ int *numElements, RPRDepth depth, RPRDepth *maxDepth)
+{
+ ListCell *lc;
+ int i;
+
+ /* Pattern nodes from parser are never NULL */
+ Assert(node != NULL);
+
+ /* Check recursion depth limit before overflow occurs */
+ if (depth >= RPR_DEPTH_MAX)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("pattern nesting too deep"),
+ errdetail("Pattern nesting depth %d exceeds maximum %d.",
+ depth, RPR_DEPTH_MAX - 1)));
+
+ /* Track maximum depth */
+ if (depth > *maxDepth)
+ *maxDepth = depth;
+
+ switch (node->nodeType)
+ {
+ case RPR_PATTERN_VAR:
+ /* Count element */
+ (*numElements)++;
+
+ /* Collect variable name if not already present */
+ for (i = 0; i < *numVars; i++)
+ {
+ if (strcmp(varNames[i], node->varName) == 0)
+ return; /* Already have this variable */
+ }
+
+ /*
+ * Variable not in DEFINE clause - this is valid per SQL standard.
+ * Such variables are implicitly TRUE. Add to varNames so they get
+ * a varId >= defineVariableList length, which executor treats as
+ * TRUE.
+ */
+ Assert(*numVars < RPR_VARID_MAX);
+ varNames[(*numVars)++] = node->varName;
+ break;
+
+ case RPR_PATTERN_SEQ:
+ /* Sequence: just recurse into children */
+ foreach(lc, node->children)
+ {
+ scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+ numVars, numElements, depth, maxDepth);
+ }
+ break;
+
+ case RPR_PATTERN_GROUP:
+ /* Add BEGIN element if group has non-trivial quantifier */
+ if (node->min != 1 || node->max != 1)
+ (*numElements)++;
+
+ /* Recurse into children at increased depth */
+ foreach(lc, node->children)
+ {
+ scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+ numVars, numElements, depth + 1, maxDepth);
+ }
+
+ /* Add END element if group has non-trivial quantifier */
+ if (node->min != 1 || node->max != 1)
+ (*numElements)++;
+ break;
+
+ case RPR_PATTERN_ALT:
+ /* Count ALT start element */
+ (*numElements)++;
+
+ /* Recurse into children at increased depth */
+ foreach(lc, node->children)
+ {
+ scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+ numVars, numElements, depth + 1, maxDepth);
+ }
+ break;
+ }
+}
+
+/*
+ * scanRPRPattern
+ * Scan pattern AST (pass 1 entry point).
+ *
+ * Collects unique variable names (appending to those from DEFINE clause),
+ * counts total elements (including FIN marker), and tracks maximum depth.
+ * Reports error if element count exceeds RPR_ELEMIDX_MAX.
+ */
+static void
+scanRPRPattern(RPRPatternNode *node, char **varNames, int *numVars,
+ int *numElements, RPRDepth *maxDepth)
+{
+ *numElements = 0;
+ *maxDepth = 0;
+
+ scanRPRPatternRecursive(node, varNames, numVars, numElements, 0, maxDepth);
+
+ (*numElements)++; /* +1 for FIN marker */
+
+ if (*numElements > RPR_ELEMIDX_MAX)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("pattern too complex"),
+ errdetail("Pattern has %d elements, maximum is %d.",
+ *numElements, RPR_ELEMIDX_MAX)));
+}
+
+/*
+ * allocateRPRPattern
+ * Allocate and initialize RPRPattern structure.
+ *
+ * Creates the pattern structure, copies variable names, and allocates
+ * the elements array. The elements array is zero-initialized.
+ */
+static RPRPattern *
+allocateRPRPattern(int numVars, int numElements, RPRDepth maxDepth,
+ char **varNamesStack)
+{
+ RPRPattern *result;
+ int i;
+
+ result = makeNode(RPRPattern);
+ result->numVars = numVars;
+ result->maxDepth = maxDepth + 1; /* +1: depth is 0-based */
+ result->numElements = numElements;
+
+ /* Copy varNames (pattern must have at least one variable) */
+ Assert(numVars > 0);
+ result->varNames = palloc(numVars * sizeof(char *));
+ for (i = 0; i < numVars; i++)
+ result->varNames[i] = pstrdup(varNamesStack[i]);
+
+ /* Allocate elements array (zero-init for reserved fields) */
+ Assert(numElements >= 2);
+ result->elements = palloc0(numElements * sizeof(RPRPatternElement));
+
+ return result;
+}
+
+/*
+ * getVarIdFromPattern
+ * Get variable ID for a variable name from RPRPattern.
+ *
+ * Returns the index of the variable in the varNames array.
+ */
+static RPRVarId
+getVarIdFromPattern(RPRPattern *pat, const char *varName)
+{
+ for (int i = 0; i < pat->numVars; i++)
+ {
+ if (strcmp(pat->varNames[i], varName) == 0)
+ return (RPRVarId) i;
+ }
+
+ /* Should not happen - variable should already be collected */
+ elog(ERROR, "pattern variable \"%s\" not found", varName);
+ pg_unreachable();
+}
+
+/*
+ * fillRPRPatternVar
+ * Fill a VAR pattern element.
+ */
+static void
+fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
+{
+ RPRPatternElement *elem = &pat->elements[*idx];
+
+ memset(elem, 0, sizeof(RPRPatternElement));
+ elem->varId = getVarIdFromPattern(pat, node->varName);
+ elem->depth = depth;
+ elem->min = node->min;
+ elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+ elem->next = RPR_ELEMIDX_INVALID;
+ elem->jump = RPR_ELEMIDX_INVALID;
+ if (node->reluctant >= 0)
+ elem->flags |= RPR_ELEM_RELUCTANT;
+ (*idx)++;
+}
+
+/*
+ * fillRPRPatternGroup
+ * Fill a GROUP pattern and its children.
+ *
+ * Creates elements for group content at increased depth, plus an END marker
+ * if the group has a non-trivial quantifier.
+ */
+static void
+fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
+{
+ ListCell *lc;
+ int groupStartIdx = *idx;
+ int beginIdx = -1;
+
+ /* Add BEGIN marker if group has non-trivial quantifier */
+ if (node->min != 1 || node->max != 1)
+ {
+ RPRPatternElement *elem = &pat->elements[*idx];
+
+ beginIdx = *idx;
+ memset(elem, 0, sizeof(RPRPatternElement));
+ elem->varId = RPR_VARID_BEGIN;
+ elem->depth = depth;
+ elem->min = node->min;
+ elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+ elem->next = RPR_ELEMIDX_INVALID; /* set by finalize */
+ elem->jump = RPR_ELEMIDX_INVALID; /* set after END */
+ if (node->reluctant >= 0)
+ elem->flags |= RPR_ELEM_RELUCTANT;
+ (*idx)++;
+ groupStartIdx = *idx; /* children start after BEGIN */
+ }
+
+ foreach(lc, node->children)
+ {
+ fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, depth + 1);
+ }
+
+ /* Add group end marker if group has non-trivial quantifier */
+ if (node->min != 1 || node->max != 1)
+ {
+ RPRPatternElement *beginElem = &pat->elements[beginIdx];
+ RPRPatternElement *endElem = &pat->elements[*idx];
+
+ memset(endElem, 0, sizeof(RPRPatternElement));
+ endElem->varId = RPR_VARID_END;
+ endElem->depth = depth;
+ endElem->min = node->min;
+ endElem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+ endElem->next = RPR_ELEMIDX_INVALID;
+ endElem->jump = groupStartIdx; /* loop to first child */
+ if (node->reluctant >= 0)
+ endElem->flags |= RPR_ELEM_RELUCTANT;
+ (*idx)++;
+
+ /* Set BEGIN skip pointer (next is set by finalize) */
+ beginElem->jump = *idx; /* skip: go to after END */
+ }
+}
+
+/*
+ * fillRPRPatternAlt
+ * Fill an ALT pattern and its alternatives.
+ *
+ * Creates ALT_START marker, fills each alternative at increased depth,
+ * sets jump pointers for backtracking, and next pointers for successful paths.
+ */
+static void
+fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
+{
+ ListCell *lc;
+ RPRPatternElement *elem;
+ List *altBranchStarts = NIL;
+ List *altEndPositions = NIL;
+
+ /* Add alternation start marker */
+ elem = &pat->elements[*idx];
+ memset(elem, 0, sizeof(RPRPatternElement));
+ elem->varId = RPR_VARID_ALT;
+ elem->depth = depth;
+ elem->min = 1;
+ elem->max = 1;
+ elem->next = RPR_ELEMIDX_INVALID;
+ elem->jump = RPR_ELEMIDX_INVALID;
+ (*idx)++;
+
+ /* Fill each alternative */
+ foreach(lc, node->children)
+ {
+ RPRPatternNode *alt = (RPRPatternNode *) lfirst(lc);
+ int branchStart = *idx;
+
+ altBranchStarts = lappend_int(altBranchStarts, branchStart);
+ fillRPRPattern(alt, pat, idx, depth + 1);
+ altEndPositions = lappend_int(altEndPositions, *idx - 1);
+ }
+
+ /* Set jump on first element of each alternative to next alternative */
+ foreach(lc, altBranchStarts)
+ {
+ int firstElemIdx = lfirst_int(lc);
+
+ if (lnext(altBranchStarts, lc) != NULL)
+ pat->elements[firstElemIdx].jump = lfirst_int(lnext(altBranchStarts, lc));
+ }
+
+ /* Set next on last element of each alternative to after the alternation */
+ {
+ int afterAltIdx = *idx;
+ ListCell *lc2 = list_head(altBranchStarts);
+
+ foreach(lc, altEndPositions)
+ {
+ int endPos = lfirst_int(lc);
+ int branchStart = lfirst_int(lc2);
+
+ if (pat->elements[endPos].next != RPR_ELEMIDX_INVALID)
+ {
+ /*
+ * An inner ALT already set next on this element. Redirect
+ * all elements in this branch that share the same target to
+ * point to after this ALT instead.
+ */
+ int oldTarget = pat->elements[endPos].next;
+ int j;
+
+ for (j = branchStart; j <= endPos; j++)
+ {
+ if (pat->elements[j].next == oldTarget)
+ pat->elements[j].next = afterAltIdx;
+ }
+ }
+ else
+ {
+ pat->elements[endPos].next = afterAltIdx;
+ }
+
+ lc2 = lnext(altBranchStarts, lc2);
+ }
+ }
+
+ list_free(altBranchStarts);
+ list_free(altEndPositions);
+}
+
+/*
+ * fillRPRPattern
+ * Fill pattern elements array from AST (pass 2).
+ *
+ * Recursively traverses AST and populates pre-allocated elements array.
+ * Dispatches to type-specific fill functions.
+ */
+static void
+fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
+{
+ ListCell *lc;
+
+ /* Pattern nodes from parser are never NULL */
+ Assert(node != NULL);
+
+ switch (node->nodeType)
+ {
+ case RPR_PATTERN_SEQ:
+ foreach(lc, node->children)
+ {
+ fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, depth);
+ }
+ break;
+
+ case RPR_PATTERN_VAR:
+ fillRPRPatternVar(node, pat, idx, depth);
+ break;
+
+ case RPR_PATTERN_GROUP:
+ fillRPRPatternGroup(node, pat, idx, depth);
+ break;
+
+ case RPR_PATTERN_ALT:
+ fillRPRPatternAlt(node, pat, idx, depth);
+ break;
+ }
+}
+
+/*
+ * finalizeRPRPattern
+ * Finalize pattern structure after filling elements.
+ *
+ * This performs:
+ * 1. Initialize absorption flag to false
+ * 2. Set up next pointers for sequential flow
+ * 3. Add FIN marker at the end
+ */
+static void
+finalizeRPRPattern(RPRPattern *result)
+{
+ int finIdx = result->numElements - 1;
+ int i;
+ RPRPatternElement *finElem;
+
+ /* Initialize absorption flag */
+ result->isAbsorbable = false;
+
+ /* Set up next pointers for elements that don't have one */
+ for (i = 0; i < finIdx; i++)
+ {
+ RPRPatternElement *elem = &result->elements[i];
+
+ if (elem->next == RPR_ELEMIDX_INVALID)
+ elem->next = (i < finIdx - 1) ? i + 1 : finIdx;
+ }
+
+ /* Add FIN marker at the end */
+ finElem = &result->elements[finIdx];
+ memset(finElem, 0, sizeof(RPRPatternElement));
+ finElem->varId = RPR_VARID_FIN;
+ finElem->depth = 0;
+ finElem->min = 1;
+ finElem->max = 1;
+ finElem->next = RPR_ELEMIDX_INVALID;
+ finElem->jump = RPR_ELEMIDX_INVALID;
+}
+
+/*-------------------------------------------------------------------------
+ * CONTEXT ABSORPTION: TWO-FLAG DESIGN
+ *-------------------------------------------------------------------------
+ *
+ * Context absorption eliminates redundant match searches by absorbing newer
+ * contexts that cannot produce longer matches than older contexts. This
+ * achieves O(n^2) -> O(n) performance improvement for patterns like A+ B.
+ *
+ * Core Insight:
+ * For pattern A+ B, if Ctx1 starts at row 0 and Ctx2 starts at row 1,
+ * both matching A continuously, Ctx1 will always have more A matches.
+ * When B finally appears, Ctx1's match (0 to current) is always longer
+ * than Ctx2's match (1 to current). So Ctx2 can be safely eliminated.
+ *
+ * Two Flags:
+ * 1. RPR_ELEM_ABSORBABLE - "Absorption judgment point"
+ * WHERE contexts can be compared for absorption.
+ * - Simple unbounded VAR (A+): the VAR element itself
+ * - Unbounded GROUP ((A B)+): the END element only
+ *
+ * 2. RPR_ELEM_ABSORBABLE_BRANCH - "Absorbable region marker"
+ * ALL elements within the absorbable region.
+ * - Used for tracking state.isAbsorbable at runtime
+ * - States leaving this region become non-absorbable permanently
+ *
+ * Why Two Flags?
+ * For pattern "(A B)+", contexts at different positions (one at A,
+ * another at B) cannot be compared - they must synchronize at END.
+ *
+ * Example: "(A B)+" with input A B A B A B...
+ * Row 0 (A): Ctx1 starts, matches A
+ * Row 1 (B): Ctx1 matches B -> END (count=1)
+ * Row 2 (A): Ctx1 loops to A, Ctx2 starts at A
+ * Row 3 (B): Ctx1 at END (count=2), Ctx2 at END (count=1)
+ * -> Both at END, comparable! Ctx1 absorbs Ctx2.
+ *
+ * Contexts synchronize at END every group-length rows. Therefore:
+ * - ABSORBABLE marks END as judgment point (where to compare)
+ * - ABSORBABLE_BRANCH keeps state.isAbsorbable=true through A->B->END
+ *
+ * Pattern Examples:
+ *
+ * Pattern: A+ B
+ * Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH <- judgment point
+ * Element 1 (B): (none)
+ * -> Compare at A every row. When contexts move to B, absorption stops.
+ *
+ * Pattern: (A B)+ C
+ * Element 0 (A): ABSORBABLE_BRANCH
+ * Element 1 (B): ABSORBABLE_BRANCH
+ * Element 2 (END): ABSORBABLE | ABSORBABLE_BRANCH <- judgment point
+ * Element 3 (C): (none)
+ * -> Compare at END every 2 rows. When contexts move to C, absorption stops.
+ *
+ * Pattern: (A+ B+)+ C
+ * Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH <- only first A+ flagged
+ * Element 1 (B): (none)
+ * Element 2 (END): (none)
+ * Element 3 (C): (none)
+ * -> Only first unbounded portion (A+) gets flags. Absorption happens
+ * at A during first iteration. After moving to B+, absorption stops.
+ *
+ * First Unbounded Portion Strategy:
+ * The algorithm only flags the FIRST unbounded portion starting from
+ * element 0. This is sufficient because:
+ * - Absorption in first portion already achieves O(n) complexity
+ * - Later portions have different synchronization characteristics
+ * - Nested unbounded patterns are too complex for simple absorption
+ * - Complex patterns (nested groups, etc.) naturally die from mismatch
+ *
+ * Runtime Usage (in nodeWindowAgg.c):
+ * - state.isAbsorbable = (previous && elem.ABSORBABLE_BRANCH)
+ * - Monotonic: once false, stays false (cannot re-enter region)
+ * - context.hasAbsorbableState: can absorb others (>=1 absorbable state)
+ * - context.allStatesAbsorbable: can be absorbed (ALL states absorbable)
+ * - Absorption check: if Ctx1.hasAbsorbable && Ctx2.allAbsorbable,
+ * compare counts at same elemIdx, absorb if Ctx1.count >= Ctx2.count
+ *
+ *-------------------------------------------------------------------------
+ */
+
+/*
+ * isUnboundedStart
+ * Check if the element at idx starts an unbounded greedy sequence.
+ *
+ * For context absorption to work, the sequence starting at idx must be:
+ * - Unbounded (max = infinity)
+ * - Greedy (not reluctant)
+ * - At the start of current scope
+ *
+ * Algorithm:
+ * - Traverse elements within current scope (parentDepth to startDepth)
+ * - For GROUP: must be unbounded greedy AND contain only simple {1,1} VARs
+ * - Sets ABSORBABLE and ABSORBABLE_BRANCH flags on matching elements
+ *
+ * Two cases are handled:
+ * 1. Simple VAR: A+ B C - A has max=INF, gets both flags
+ * 2. Group: (A B)+ C - END has max=INF, all children are {1,1} VARs
+ * A,B,END get ABSORBABLE_BRANCH, only END gets ABSORBABLE
+ *
+ * Returns false for patterns where absorption cannot work:
+ * - A B+ (unbounded not at start)
+ * - A+? B (reluctant quantifier)
+ * - (A | B)+ (ALT inside group)
+ * - (A B+)+ (unbounded element inside group)
+ * - ((A B)+ C)+ (nested unbounded groups)
+ */
+static bool
+isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
+{
+ RPRPatternElement *elem = &pattern->elements[idx];
+ RPRDepth startDepth = elem->depth;
+ RPRPatternElement *nextElem;
+ RPRPatternElement *e;
+
+ /* Case 1: Simple unbounded VAR at start (greedy only) */
+ if (RPRElemIsVar(elem) && elem->max == RPR_QUANTITY_INF &&
+ !RPRElemIsReluctant(elem))
+ {
+ /* Set both flags on first element */
+ elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH | RPR_ELEM_ABSORBABLE;
+ return true;
+ }
+
+ /*
+ * Case 2: Unbounded GROUP - traverse siblings at startDepth and check if
+ * they're all simple {1,1} VARs, then check if END at startDepth - 1 is
+ * unbounded greedy.
+ */
+ for (e = elem; e->depth == startDepth; e = nextElem)
+ {
+ /* Must be simple {1,1} VAR */
+ if (!RPRElemIsVar(e) || e->min != 1 || e->max != 1)
+ return false;
+
+ Assert(e->next != RPR_ELEMIDX_INVALID);
+ nextElem = &pattern->elements[e->next];
+ }
+
+ /* Now e should be END at startDepth - 1 */
+ if (e->depth == startDepth - 1 &&
+ RPRElemIsEnd(e) && e->max == RPR_QUANTITY_INF &&
+ !RPRElemIsReluctant(e))
+ {
+ Assert(e->jump == idx); /* END points back to first child */
+
+ /* Set ABSORBABLE_BRANCH on all children, ABSORBABLE on END only */
+ for (e = elem; !RPRElemIsEnd(e); e = &pattern->elements[e->next])
+ e->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+ e->flags |= RPR_ELEM_ABSORBABLE_BRANCH | RPR_ELEM_ABSORBABLE;
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * computeAbsorbabilityRecursive
+ * Recursively check absorbability starting from given index.
+ *
+ * If the element at startIdx is ALT, recursively checks each branch independently.
+ * Each branch gets its own absorbability status, and if any branch is absorbable,
+ * the ALT element itself is marked with RPR_ELEM_ABSORBABLE_BRANCH.
+ *
+ * If BEGIN, skips to first child.
+ *
+ * Otherwise (VAR), checks if the element starts an unbounded sequence via
+ * isUnboundedStart.
+ */
+static void
+computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
+ bool *hasAbsorbable)
+{
+ RPRPatternElement *elem = &pattern->elements[startIdx];
+
+ if (RPRElemIsAlt(elem))
+ {
+ /* ALT: recursively check each branch */
+ RPRElemIdx branchIdx = elem->next;
+ RPRPatternElement *branchFirst;
+ bool branchAbsorbable;
+
+ while (branchIdx != RPR_ELEMIDX_INVALID)
+ {
+ branchAbsorbable = false;
+
+ Assert(branchIdx < pattern->numElements);
+ branchFirst = &pattern->elements[branchIdx];
+
+ /* Stop if element is outside ALT scope (not a branch) */
+ if (branchFirst->depth <= elem->depth)
+ break;
+
+ /* Recursively check this branch */
+ computeAbsorbabilityRecursive(pattern, branchIdx, &branchAbsorbable);
+ if (branchAbsorbable)
+ {
+ *hasAbsorbable = true;
+ }
+
+ branchIdx = branchFirst->jump;
+ }
+
+ /* Mark ALT element if any branch is absorbable */
+ if (*hasAbsorbable)
+ elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+ }
+ else if (RPRElemIsBegin(elem))
+ {
+ /* BEGIN: skip to first child and check that */
+ computeAbsorbabilityRecursive(pattern, elem->next, hasAbsorbable);
+
+ /* Mark BEGIN element if contents are absorbable */
+ if (*hasAbsorbable)
+ elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+ }
+ else
+ {
+ /* Should never reach END - structural invariant of pattern AST */
+ Assert(!RPRElemIsEnd(elem));
+
+ /* Non-ALT, non-BEGIN: check if unbounded start */
+ if (isUnboundedStart(pattern, startIdx))
+ {
+ *hasAbsorbable = true;
+ }
+ }
+}
+
+/*
+ * computeAbsorbability
+ * Determine if pattern supports context absorption optimization.
+ *
+ * Context absorption eliminates redundant match searches by absorbing
+ * newer contexts that cannot produce longer matches than older contexts.
+ * This achieves O(n^2) -> O(n) performance improvement.
+ *
+ * Only greedy unbounded quantifiers at pattern start can be absorbable.
+ * Reluctant quantifiers are excluded because they don't maintain monotonic
+ * decrease property required for safe absorption.
+ *
+ * This function sets two flags:
+ * RPR_ELEM_ABSORBABLE: Absorption judgment point
+ * - Simple unbounded VAR: the VAR itself (e.g., A in A+)
+ * - Unbounded GROUP: the END element (e.g., END in (A B)+)
+ * RPR_ELEM_ABSORBABLE_BRANCH: All elements in absorbable region
+ * - All elements within the same scope as unbounded start
+ *
+ * Examples:
+ * A+ B C - absorbable (A gets both flags)
+ * (A B)+ C - absorbable (A,B,END get BRANCH, END gets ABSORBABLE)
+ * A B+ - NOT absorbable (unbounded not at start)
+ * A+? B C - NOT absorbable (reluctant quantifier)
+ * (A+ B+)+ - only first A+ on first iteration (nested unbounded not supported)
+ * A+ | B+ - both branches absorbable independently
+ * A+ | C D - only A+ branch absorbable (C D branch not absorbable)
+ * ((A+ B) | C) D - nested ALT: A+ branch is absorbable
+ */
+static void
+computeAbsorbability(RPRPattern *pattern)
+{
+ bool hasAbsorbable = false;
+
+ /* Parser always produces at least one element + FIN */
+ Assert(pattern->numElements >= 2);
+
+ /* Start recursion from first element */
+ computeAbsorbabilityRecursive(pattern, 0, &hasAbsorbable);
+ pattern->isAbsorbable = hasAbsorbable;
+}
+
+/*
+ * collectPatternVariablesRecursive
+ * Recursively collect variable names from pattern AST.
+ */
+static void
+collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
+{
+ ListCell *lc;
+
+ Assert(node != NULL);
+
+ switch (node->nodeType)
+ {
+ case RPR_PATTERN_VAR:
+ /* Add variable if not already in list */
+ foreach(lc, *varNames)
+ {
+ if (strcmp(strVal(lfirst(lc)), node->varName) == 0)
+ return; /* Already collected */
+ }
+ *varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
+ break;
+
+ case RPR_PATTERN_SEQ:
+ case RPR_PATTERN_ALT:
+ case RPR_PATTERN_GROUP:
+ foreach(lc, node->children)
+ {
+ collectPatternVariablesRecursive((RPRPatternNode *) lfirst(lc),
+ varNames);
+ }
+ break;
+ }
+}
+
+/*
+ * collectPatternVariables
+ * Collect unique variable names used in PATTERN clause.
+ *
+ * Returns a list of String nodes containing variable names.
+ * Used to filter defineClause before varId assignment.
+ */
+List *
+collectPatternVariables(RPRPatternNode *pattern)
+{
+ List *varNames = NIL;
+
+ /* Caller ensures pattern is not NULL */
+ Assert(pattern != NULL);
+
+ collectPatternVariablesRecursive(pattern, &varNames);
+ return varNames;
+}
+
+/*
+ * filterDefineClause
+ * Filter defineClause to include only variables used in PATTERN.
+ *
+ * This eliminates unnecessary DEFINE evaluations at runtime.
+ * Also builds defineVariableList from the filtered result.
+ *
+ * Returns filtered defineClause (list of TargetEntry).
+ * Sets *defineVariableList to list of variable names (String nodes).
+ */
+List *
+filterDefineClause(List *defineClause, List *patternVars,
+ List **defineVariableList)
+{
+ List *filteredDefineClause = NIL;
+ ListCell *lc;
+ ListCell *lc2;
+
+ *defineVariableList = NIL;
+
+ /* Filter defineClause: keep only variables used in PATTERN */
+ foreach(lc, defineClause)
+ {
+ TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+ foreach(lc2, patternVars)
+ {
+ if (strcmp(strVal(lfirst(lc2)), te->resname) == 0)
+ {
+ filteredDefineClause = lappend(filteredDefineClause, te);
+ break;
+ }
+ }
+ }
+
+ /* Build defineVariableList from filtered defineClause */
+ foreach(lc, filteredDefineClause)
+ {
+ TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+ *defineVariableList = lappend(*defineVariableList,
+ makeString(pstrdup(te->resname)));
+ }
+
+ return filteredDefineClause;
+}
+
+/*
+ * buildRPRPattern
+ * Compile pattern AST to flat bytecode array.
+ *
+ * Compilation phases:
+ * 1. Optimize AST (flatten, merge, deduplicate)
+ * 2. Scan: collect variables, count elements (pass 1)
+ * 3. Allocate result structure
+ * 4. Fill elements from AST (pass 2)
+ * 5. Finalize pattern structure
+ * 6. Compute context absorption eligibility
+ *
+ * Called from createplan.c during plan creation.
+ */
+RPRPattern *
+buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
+ RPSkipTo rpSkipTo, int frameOptions)
+{
+ RPRPattern *result;
+ RPRPatternNode *optimized;
+ char *varNamesStack[RPR_VARID_MAX];
+ int numVars;
+ int numElements;
+ RPRDepth maxDepth;
+ int idx;
+ bool hasLimitedFrame;
+
+ /* Caller must check for NULL pattern before calling */
+ Assert(pattern != NULL);
+
+ /* Optimize the pattern tree */
+ optimized = optimizeRPRPattern(copyObject(pattern));
+
+ /* Collect variable names from DEFINE clause */
+ numVars = collectDefineVariables(defineVariableList, varNamesStack);
+
+ /* Scan pattern: collect variables, count elements, validate limits */
+ scanRPRPattern(optimized, varNamesStack, &numVars, &numElements, &maxDepth);
+
+ /* Allocate result structure */
+ result = allocateRPRPattern(numVars, numElements, maxDepth, varNamesStack);
+
+ /* Fill elements (pass 2) */
+ idx = 0;
+ fillRPRPattern(optimized, result, &idx, 0);
+
+ /* Finalize: set up next pointers, flags, and add FIN marker */
+ finalizeRPRPattern(result);
+
+ /*
+ * Compute context absorption eligibility. Absorption requires both
+ * structural absorbability and runtime conditions. Check runtime
+ * conditions first to avoid unnecessary pattern analysis.
+ *
+ * Runtime conditions for absorption:
+ *
+ * 1. SKIP TO PAST LAST ROW required (not SKIP TO NEXT ROW): With NEXT
+ * ROW, after each match the search resumes from the next row, so contexts
+ * are immediately discarded. No redundant contexts accumulate, making
+ * absorption unnecessary.
+ *
+ * 2. Unbounded frame end required (not ROWS with bounded end): With a
+ * bounded frame (e.g., ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING),
+ * matches may be truncated at frame boundaries. This changes the
+ * absorption semantics - older contexts don't necessarily produce longer
+ * matches when frame limits apply differently to each context.
+ */
+ hasLimitedFrame = (frameOptions & FRAMEOPTION_ROWS) &&
+ !(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING);
+
+ if (rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame)
+ {
+ /* Runtime conditions met - check structural absorbability */
+ computeAbsorbability(result);
+ }
+
+ return result;
+}
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 1b5b9b5ed9c..14367afcc7c 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -214,7 +214,6 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
NodeTag elided_type, Bitmapset *relids);
-
/*****************************************************************************
*
* SUBPLAN REFERENCES
@@ -2627,6 +2626,32 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
NRM_EQUAL,
NUM_EXEC_QUAL(plan));
+ /*
+ * Modifies an expression tree in each DEFINE clause so that all Var
+ * nodes's varno refers to OUTER_VAR.
+ */
+ if (IsA(plan, WindowAgg))
+ {
+ WindowAgg *wplan = (WindowAgg *) plan;
+
+ if (wplan->defineClause != NIL)
+ {
+ foreach(l, wplan->defineClause)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(l);
+
+ tle->expr = (Expr *)
+ fix_upper_expr(root,
+ (Node *) tle->expr,
+ subplan_itlist,
+ OUTER_VAR,
+ rtoffset,
+ NRM_EQUAL,
+ NUM_EXEC_QUAL(plan));
+ }
+ }
+ }
+
pfree(subplan_itlist);
}
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index c90f4b32733..bcedc5b849e 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -2514,6 +2514,15 @@ perform_pullup_replace_vars(PlannerInfo *root,
parse->returningList = (List *)
pullup_replace_vars((Node *) parse->returningList, rvcontext);
+ foreach(lc, parse->windowClause)
+ {
+ WindowClause *wc = lfirst_node(WindowClause, lc);
+
+ if (wc->defineClause != NIL)
+ wc->defineClause = (List *)
+ pullup_replace_vars((Node *) wc->defineClause, rvcontext);
+ }
+
if (parse->onConflict)
{
parse->onConflict->onConflictSet = (List *)
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 8c9321aab8c..9c09848d4ae 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -20,6 +20,7 @@
#include "lib/stringinfo.h"
#include "nodes/bitmapset.h"
#include "nodes/lockoptions.h"
+#include "nodes/parsenodes.h"
#include "nodes/primnodes.h"
@@ -1242,6 +1243,70 @@ typedef struct Agg
List *chain;
} Agg;
+/* ----------------
+ * Row Pattern Recognition compiled pattern types
+ * ----------------
+ */
+
+/* Type definitions for RPR pattern elements */
+typedef uint8 RPRVarId; /* pattern variable ID */
+typedef uint8 RPRElemFlags; /* element flags */
+typedef uint8 RPRDepth; /* group nesting depth */
+typedef int32 RPRQuantity; /* quantifier min/max */
+typedef int16 RPRElemIdx; /* element array index */
+
+/*
+ * RPRPatternElement - flat element for NFA pattern matching (16 bytes)
+ *
+ * Layout optimized for alignment (no padding holes):
+ * varId(1) + depth(1) + flags(1) + reserved(1) + min(4) + max(4) + next(2) + jump(2)
+ */
+typedef struct RPRPatternElement
+{
+ RPRVarId varId; /* variable ID, or special value for control */
+ RPRDepth depth; /* group nesting depth */
+ RPRElemFlags flags; /* flags (reluctant, etc.) */
+ uint8 reserved; /* reserved padding byte */
+ RPRQuantity min; /* quantifier minimum */
+ RPRQuantity max; /* quantifier maximum */
+ RPRElemIdx next; /* next element index */
+ RPRElemIdx jump; /* jump target (for ALT/GROUP) */
+} RPRPatternElement;
+
+/*
+ * RPRPattern - compiled pattern for NFA execution
+ *
+ * Requires custom copy/out/read functions due to elements array.
+ */
+typedef struct RPRPattern
+{
+ pg_node_attr(custom_copy_equal, custom_read_write)
+
+ NodeTag type; /* T_RPRPattern */
+ int numVars; /* number of pattern variables */
+ char **varNames; /* array of variable names (DEFINE order
+ * first) */
+ RPRDepth maxDepth; /* maximum group nesting depth */
+ int numElements; /* number of elements */
+ RPRPatternElement *elements; /* array of pattern elements */
+
+ /*----------------
+ * Context absorption optimization.
+ *
+ * Absorption is only safe when later matches are guaranteed to be
+ * suffixes of earlier matches. This requires simple pattern structure:
+ *
+ * Case 1: No ALT, single unbounded element (A+, (A B)+)
+ * Case 2: Top-level ALT with each branch being single unbounded (A+ | B+)
+ *
+ * Complex patterns like A B (A B)+ could theoretically be transformed to
+ * (A B){2,} for absorption, but this changes lexical order and is not
+ * implemented. Similarly, (A|B)+ cannot be absorbed because different
+ * start positions produce different match contents (not suffix relation).
+ */
+ bool isAbsorbable; /* true if pattern supports context absorption */
+} RPRPattern;
+
/* ----------------
* window aggregate node
* ----------------
@@ -1312,6 +1377,15 @@ typedef struct WindowAgg
/* nulls sort first for in_range tests? */
bool inRangeNullsFirst;
+ /* Row Pattern Recognition AFTER MATCH SKIP clause */
+ RPSkipTo rpSkipTo; /* Row Pattern Skip To type */
+
+ /* Compiled Row Pattern for NFA execution */
+ struct RPRPattern *rpPattern;
+
+ /* Row Pattern DEFINE clause (list of TargetEntry) */
+ List *defineClause;
+
/*
* false for all apart from the WindowAgg that's closest to the root of
* the plan
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
new file mode 100644
index 00000000000..7a8938dbebd
--- /dev/null
+++ b/src/include/optimizer/rpr.h
@@ -0,0 +1,57 @@
+/*-------------------------------------------------------------------------
+ *
+ * rpr.h
+ * Row Pattern Recognition pattern compilation for planner
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/optimizer/rpr.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef OPTIMIZER_RPR_H
+#define OPTIMIZER_RPR_H
+
+#include "nodes/parsenodes.h"
+#include "nodes/plannodes.h"
+
+/* Limits and special values */
+#define RPR_VARID_MAX 251 /* max pattern variables: 251 */
+#define RPR_QUANTITY_INF INT32_MAX /* unbounded quantifier */
+#define RPR_COUNT_MAX INT32_MAX /* max runtime count (NFA state) */
+#define RPR_ELEMIDX_MAX INT16_MAX /* max pattern elements */
+#define RPR_ELEMIDX_INVALID ((RPRElemIdx) -1) /* invalid index */
+#define RPR_DEPTH_MAX (UINT8_MAX - 1) /* max pattern nesting depth: 254 */
+#define RPR_DEPTH_NONE UINT8_MAX /* no enclosing group (top-level) */
+
+/* Special varId values for control elements (252-255) */
+#define RPR_VARID_BEGIN ((RPRVarId) 252) /* group begin */
+#define RPR_VARID_END ((RPRVarId) 253) /* group end */
+#define RPR_VARID_ALT ((RPRVarId) 254) /* alternation start */
+#define RPR_VARID_FIN ((RPRVarId) 255) /* pattern finish */
+
+/* Element flags */
+#define RPR_ELEM_RELUCTANT 0x01 /* reluctant (non-greedy)
+ * quantifier */
+#define RPR_ELEM_ABSORBABLE_BRANCH 0x02 /* element in absorbable region */
+#define RPR_ELEM_ABSORBABLE 0x04 /* absorption judgment point */
+
+/* Accessor macros for RPRPatternElement */
+#define RPRElemIsReluctant(e) ((e)->flags & RPR_ELEM_RELUCTANT)
+#define RPRElemIsAbsorbableBranch(e) ((e)->flags & RPR_ELEM_ABSORBABLE_BRANCH)
+#define RPRElemIsAbsorbable(e) ((e)->flags & RPR_ELEM_ABSORBABLE)
+#define RPRElemIsVar(e) ((e)->varId <= RPR_VARID_MAX)
+#define RPRElemIsBegin(e) ((e)->varId == RPR_VARID_BEGIN)
+#define RPRElemIsEnd(e) ((e)->varId == RPR_VARID_END)
+#define RPRElemIsAlt(e) ((e)->varId == RPR_VARID_ALT)
+#define RPRElemIsFin(e) ((e)->varId == RPR_VARID_FIN)
+#define RPRElemCanSkip(e) ((e)->min == 0)
+
+extern List *collectPatternVariables(RPRPatternNode *pattern);
+extern List *filterDefineClause(List *defineClause, List *patternVars,
+ List **defineVariableList);
+extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
+ RPSkipTo rpSkipTo, int frameOptions);
+
+#endif /* OPTIMIZER_RPR_H */
--
2.43.0
[application/octet-stream] v43-0005-Row-pattern-recognition-patch-executor-and-comma.patch (102.5K, 6-v43-0005-Row-pattern-recognition-patch-executor-and-comma.patch)
download | inline diff:
From 9ac8600d3ef3e91f7bebb5a2862b0f357278eacd Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sun, 15 Feb 2026 17:47:49 +0900
Subject: [PATCH v43 5/8] Row pattern recognition patch (executor and
commands).
---
src/backend/commands/explain.c | 461 +++++
src/backend/executor/nodeWindowAgg.c | 2343 +++++++++++++++++++++++++-
src/backend/utils/adt/windowfuncs.c | 25 +-
src/include/catalog/pg_proc.dat | 6 +
src/include/nodes/execnodes.h | 119 ++
5 files changed, 2943 insertions(+), 11 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index b9587983f88..575236f0bc6 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -29,6 +29,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"
@@ -117,6 +118,20 @@ 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_elements(RPRPattern *pattern, int *idx,
+ StringInfoData *buf, RPRDepth groupDepth,
+ RPRDepth *prevDepth, bool *needSpace);
+static void deparse_rpr_group(RPRPattern *pattern, int *idx,
+ StringInfoData *buf, RPRDepth *prevDepth,
+ bool *needSpace);
+static void deparse_rpr_alt(RPRPattern *pattern, int *idx,
+ StringInfoData *buf, RPRDepth *prevDepth,
+ bool *needSpace, List **altSeps);
+static void deparse_rpr_var(RPRPattern *pattern, int *idx,
+ StringInfoData *buf, RPRDepth *prevDepth,
+ bool *needSpace, List **altSeps);
static void show_storage_info(char *maxStorageType, int64 maxSpaceUsed,
ExplainState *es);
static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
@@ -127,6 +142,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);
@@ -2889,6 +2905,284 @@ 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);
+
+ if (RPRElemIsReluctant(elem))
+ {
+ if (elem->min == 1 && elem->max == 1)
+ appendStringInfo(buf, "{1}"); /* make reluctant ? unambiguous */
+ appendStringInfoChar(buf, '?');
+ }
+
+ /* Append absorption markers: " for judgment point, ' for branch only */
+ if (RPRElemIsAbsorbable(elem))
+ {
+ Assert(elem->max == RPR_QUANTITY_INF);
+ appendStringInfoChar(buf, '"');
+ }
+ else if (RPRElemIsAbsorbableBranch(elem))
+ appendStringInfoChar(buf, '\'');
+}
+
+/*
+ * Deparse a compiled RPRPattern (bytecode) back to pattern string.
+ *
+ * Walks the flat bytecode array using mutual recursion: deparse_rpr_elements
+ * processes sequential elements, and deparse_rpr_group handles BEGIN...END
+ * groups by recursing back into deparse_rpr_elements for the group content.
+ */
+static char *
+deparse_rpr_pattern(RPRPattern *pattern)
+{
+ StringInfoData buf;
+ int idx = 0;
+ RPRDepth prevDepth = 0;
+ bool needSpace = false;
+
+ Assert(pattern != NULL && pattern->numElements >= 2);
+
+ initStringInfo(&buf);
+
+ deparse_rpr_elements(pattern, &idx, &buf, RPR_DEPTH_NONE,
+ &prevDepth, &needSpace);
+
+ /* Close remaining open parens */
+ while (prevDepth > 0)
+ {
+ appendStringInfoChar(&buf, ')');
+ prevDepth--;
+ }
+
+ return buf.data;
+}
+
+/*
+ * Process pattern elements sequentially until FIN or END at groupDepth.
+ *
+ * When groupDepth >= 0, stops at the matching END element (leaving idx
+ * pointing to it) so the caller (deparse_rpr_group) can consume it.
+ * When groupDepth < 0, processes until FIN (top-level call).
+ */
+static void
+deparse_rpr_elements(RPRPattern *pattern, int *idx, StringInfoData *buf,
+ RPRDepth groupDepth, RPRDepth *prevDepth,
+ bool *needSpace)
+{
+ List *altSeps = NIL; /* pending alternation separator indices */
+
+ while (*idx < pattern->numElements)
+ {
+ RPRPatternElement *elem = &pattern->elements[*idx];
+
+ if (RPRElemIsFin(elem))
+ break;
+
+ /* Stop at END matching our group depth; caller handles it */
+ if (RPRElemIsEnd(elem) && elem->depth == groupDepth)
+ break;
+
+ /* Alternation separator */
+ if (list_member_int(altSeps, *idx))
+ {
+ /* Close parens to match separator depth first */
+ while (*prevDepth > elem->depth)
+ {
+ appendStringInfoChar(buf, ')');
+ (*prevDepth)--;
+ }
+ appendStringInfoString(buf, " | ");
+ *needSpace = false;
+ altSeps = list_delete_int(altSeps, *idx);
+ }
+
+ /* Dispatch to element-type handlers */
+ if (RPRElemIsAlt(elem))
+ deparse_rpr_alt(pattern, idx, buf, prevDepth,
+ needSpace, &altSeps);
+ else if (RPRElemIsBegin(elem))
+ deparse_rpr_group(pattern, idx, buf, prevDepth,
+ needSpace);
+ else if (RPRElemIsVar(elem))
+ deparse_rpr_var(pattern, idx, buf, prevDepth,
+ needSpace, &altSeps);
+ }
+ list_free(altSeps);
+}
+
+/*
+ * Process a BEGIN...END group.
+ *
+ * Consumes BEGIN, recurses into deparse_rpr_elements for group content,
+ * then consumes END and outputs the group quantifier.
+ *
+ * When the group wraps a single ALT with no siblings, the group-level
+ * parenthesis is suppressed since the ALT-to-children depth transition
+ * already provides it (avoids double parens like "((a | b))+").
+ */
+static void
+deparse_rpr_group(RPRPattern *pattern, int *idx, StringInfoData *buf,
+ RPRDepth *prevDepth, bool *needSpace)
+{
+ RPRPatternElement *begin = &pattern->elements[*idx];
+ RPRDepth childDepth = begin->depth + 1;
+ bool singleAlt = false;
+ RPRPatternElement *end;
+
+ /*
+ * Check if this group wraps a single ALT with no siblings. Scan from
+ * after ALT to END: if no element at childDepth exists, the ALT is the
+ * sole child.
+ */
+ if (*idx + 1 < pattern->numElements &&
+ RPRElemIsAlt(&pattern->elements[*idx + 1]))
+ {
+ int j;
+
+ singleAlt = true;
+ for (j = *idx + 2; j < pattern->numElements; j++)
+ {
+ RPRPatternElement *e = &pattern->elements[j];
+
+ if (RPRElemIsEnd(e) && e->depth == begin->depth)
+ break;
+ if (e->depth <= childDepth)
+ {
+ singleAlt = false;
+ break;
+ }
+ }
+ }
+
+ /* Open group paren (unless single ALT provides it) */
+ if (!singleAlt)
+ {
+ if (*needSpace)
+ appendStringInfoChar(buf, ' ');
+ appendStringInfoChar(buf, '(');
+ *needSpace = false;
+ }
+ *prevDepth = childDepth;
+ (*idx)++; /* consume BEGIN */
+
+ /* Process group children; stops at matching END */
+ deparse_rpr_elements(pattern, idx, buf, begin->depth,
+ prevDepth, needSpace);
+
+ /* Consume END and output quantifier */
+ Assert(*idx < pattern->numElements);
+ end = &pattern->elements[*idx];
+ Assert(RPRElemIsEnd(end) && end->depth == begin->depth);
+
+ while (*prevDepth > end->depth + 1)
+ {
+ appendStringInfoChar(buf, ')');
+ (*prevDepth)--;
+ }
+ if (!singleAlt)
+ appendStringInfoChar(buf, ')');
+ append_rpr_quantifier(buf, end);
+ *prevDepth = end->depth;
+ *needSpace = true;
+ (*idx)++; /* consume END */
+}
+
+/*
+ * Process an ALT element: adjust depth parens and register separator positions.
+ */
+static void
+deparse_rpr_alt(RPRPattern *pattern, int *idx, StringInfoData *buf,
+ RPRDepth *prevDepth, bool *needSpace, List **altSeps)
+{
+ RPRPatternElement *elem = &pattern->elements[*idx];
+
+ /* Close parens for depth decrease */
+ while (*prevDepth > elem->depth)
+ {
+ appendStringInfoChar(buf, ')');
+ (*prevDepth)--;
+ *needSpace = true;
+ }
+
+ /* Open parens up to ALT's depth */
+ while (*prevDepth < elem->depth)
+ {
+ if (*needSpace)
+ appendStringInfoChar(buf, ' ');
+ appendStringInfoChar(buf, '(');
+ (*prevDepth)++;
+ *needSpace = false;
+ }
+
+ /* Register next alternation separator position */
+ if (elem->next != RPR_ELEMIDX_INVALID)
+ {
+ RPRPatternElement *firstElem = &pattern->elements[elem->next];
+
+ if (firstElem->jump != RPR_ELEMIDX_INVALID)
+ *altSeps = lappend_int(*altSeps, firstElem->jump);
+ }
+ if (elem->jump != RPR_ELEMIDX_INVALID)
+ *altSeps = lappend_int(*altSeps, elem->jump);
+ (*idx)++;
+}
+
+/*
+ * Process a VAR element: adjust depth parens and output variable name.
+ */
+static void
+deparse_rpr_var(RPRPattern *pattern, int *idx, StringInfoData *buf,
+ RPRDepth *prevDepth, bool *needSpace, List **altSeps)
+{
+ RPRPatternElement *elem = &pattern->elements[*idx];
+
+ /* Open parens for depth increase */
+ while (*prevDepth < elem->depth)
+ {
+ if (*needSpace)
+ appendStringInfoChar(buf, ' ');
+ appendStringInfoChar(buf, '(');
+ (*prevDepth)++;
+ *needSpace = false;
+ }
+
+ /* Close parens for depth decrease */
+ while (*prevDepth > elem->depth)
+ {
+ appendStringInfoChar(buf, ')');
+ (*prevDepth)--;
+ }
+
+ if (*needSpace)
+ appendStringInfoChar(buf, ' ');
+
+ Assert(elem->varId < pattern->numVars);
+ appendStringInfoString(buf, pattern->varNames[elem->varId]);
+ append_rpr_quantifier(buf, elem);
+ *needSpace = true;
+
+ if (elem->jump != RPR_ELEMIDX_INVALID)
+ *altSeps = lappend_int(*altSeps, elem->jump);
+ (*idx)++;
+}
+
/*
* Show the window definition for a WindowAgg node.
*/
@@ -2947,6 +3241,18 @@ 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);
+
+ if (patternStr != NULL)
+ {
+ ExplainPropertyText("Pattern", patternStr, es);
+ pfree(patternStr);
+ }
+ }
}
/*
@@ -3499,6 +3805,7 @@ show_windowagg_info(WindowAggState *winstate, ExplainState *es)
{
char *maxStorageType;
int64 maxSpaceUsed;
+ WindowAgg *wagg = (WindowAgg *) winstate->ss.ps.plan;
Tuplestorestate *tupstore = winstate->buffer;
@@ -3511,6 +3818,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);
+ appendStringInfo(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
+ {
+ appendStringInfo(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
+ {
+ appendStringInfo(es->str, ", 0 mismatched");
+ }
+ appendStringInfoChar(es->str, '\n');
+
+ /* Absorbed/skipped context length statistics */
+ if (winstate->nfaContextsAbsorbed > 0 || winstate->nfaContextsSkipped > 0)
+ {
+ ExplainIndentText(es);
+ appendStringInfo(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
+ {
+ appendStringInfo(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
+ {
+ appendStringInfo(es->str, ", 0 skipped");
+ }
+
+ appendStringInfoChar(es->str, '\n');
+ }
+ }
}
/*
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index d9b64b0f465..9b2c4b6a1d7 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -36,18 +36,23 @@
#include "access/htup_details.h"
#include "catalog/objectaccess.h"
#include "catalog/pg_aggregate.h"
+#include "catalog/pg_collation_d.h"
#include "catalog/pg_proc.h"
#include "executor/executor.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 "optimizer/rpr.h"
#include "parser/parse_agg.h"
#include "parser/parse_coerce.h"
+#include "regex/regex.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/datum.h"
+#include "utils/fmgroids.h"
#include "utils/expandeddatum.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
@@ -170,6 +175,15 @@ typedef struct WindowStatePerAggData
bool restart; /* need to restart this agg in this cycle? */
} WindowStatePerAggData;
+/*
+ * Structure used by check_rpr_navigation() and rpr_navigation_walker().
+ */
+typedef struct NavigationInfo
+{
+ bool is_prev; /* true if PREV */
+ int num_vars; /* number of var nodes */
+} NavigationInfo;
+
static void initialize_windowaggregate(WindowAggState *winstate,
WindowStatePerFunc perfuncstate,
WindowStatePerAgg peraggstate);
@@ -206,6 +220,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);
@@ -224,6 +241,91 @@ 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 void attno_map(Node *node);
+static bool attno_map_walker(Node *node, void *context);
+static int row_is_in_reduced_frame(WindowObject winobj, int64 pos);
+static bool rpr_is_defined(WindowAggState *winstate);
+
+static void create_reduced_frame_map(WindowAggState *winstate);
+static int get_reduced_frame_map(WindowAggState *winstate, int64 pos);
+static void register_reduced_frame_map(WindowAggState *winstate, int64 pos,
+ int val);
+static void clear_reduced_frame_map(WindowAggState *winstate);
+static void update_reduced_frame(WindowObject winobj, int64 pos);
+
+static void check_rpr_navigation(Node *node, bool is_prev);
+static bool rpr_navigation_walker(Node *node, void *context);
+
+/* Forward declarations - NFA row processing */
+static void nfa_process_row(WindowAggState *winstate, int64 currentPos,
+ bool hasLimitedFrame, int64 frameOffset);
+
+/* Forward declarations - NFA state management */
+static RPRNFAState *nfa_state_alloc(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_create(WindowAggState *winstate, int16 elemIdx,
+ int16 altPriority, int32 *counts,
+ bool sourceAbsorbable);
+static bool nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1,
+ RPRNFAState *s2);
+static bool nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
+ RPRNFAState *state);
+static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
+ RPRNFAState *state, int64 matchEndRow);
+
+/* Forward declarations - NFA context management */
+static RPRNFAContext *nfa_context_alloc(WindowAggState *winstate);
+static void nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx);
+static void nfa_context_free(WindowAggState *winstate, RPRNFAContext *ctx);
+static RPRNFAContext *nfa_start_context(WindowAggState *winstate, int64 startPos);
+static RPRNFAContext *nfa_get_head_context(WindowAggState *winstate, int64 pos);
+
+/* Forward declarations - NFA statistics */
+static void nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen);
+static void nfa_record_context_success(WindowAggState *winstate, int64 matchLen);
+static void nfa_record_context_failure(WindowAggState *winstate, int64 failedLen);
+static void nfa_record_context_skipped(WindowAggState *winstate, int64 skippedLen);
+static void nfa_record_context_absorbed(WindowAggState *winstate, int64 absorbedLen);
+
+/* Forward declarations - NFA row evaluation */
+static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
+
+/* Forward declarations - NFA context lifecycle */
+static void nfa_cleanup_dead_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx);
+static void nfa_finalize_all_contexts(WindowAggState *winstate, int64 lastPos);
+
+/* Forward declarations - NFA absorption */
+static void nfa_update_absorption_flags(RPRNFAContext *ctx);
+static bool nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older,
+ RPRNFAContext *newer);
+static bool nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx);
+static void nfa_absorb_contexts(WindowAggState *winstate);
+
+/* Forward declarations - NFA match and advance */
+static inline bool nfa_eval_var_match(WindowAggState *winstate,
+ RPRPatternElement *elem, bool *varMatched);
+static void nfa_match(WindowAggState *winstate, RPRNFAContext *ctx,
+ bool *varMatched);
+static void nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
+ RPRNFAState *state, int64 currentPos, bool initialAdvance);
+static void nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
+ RPRNFAState *state, RPRPatternElement *nextElem,
+ int64 currentPos, bool initialAdvance);
+static void nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
+ RPRNFAState *state, RPRPatternElement *elem,
+ int64 currentPos, bool initialAdvance);
+static void nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
+ RPRNFAState *state, RPRPatternElement *elem,
+ int64 currentPos, bool initialAdvance);
+static void nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
+ RPRNFAState *state, RPRPatternElement *elem,
+ int64 currentPos, bool initialAdvance);
+static void nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
+ RPRNFAState *state, RPRPatternElement *elem,
+ int64 currentPos, bool initialAdvance);
+static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
+ int64 currentPos, bool initialAdvance);
/*
* Not null info bit array consists of 2-bit items
@@ -817,6 +919,7 @@ 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 is enabled
*
* 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
@@ -831,7 +934,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++;
@@ -905,7 +1009,22 @@ eval_windowaggregates(WindowAggState *winstate)
* head, so that tuplestore can discard unnecessary rows.
*/
if (agg_winobj->markptr >= 0)
- WinSetMarkPosition(agg_winobj, winstate->frameheadpos);
+ {
+ int64 markpos = winstate->frameheadpos;
+
+ if (rpr_is_defined(winstate))
+ {
+ /*
+ * If RPR is used, it is possible PREV wants to look at the
+ * previous row. So the mark pos should be frameheadpos - 1
+ * unless it is below 0.
+ */
+ markpos -= 1;
+ if (markpos < 0)
+ markpos = 0;
+ }
+ WinSetMarkPosition(agg_winobj, markpos);
+ }
/*
* Now restart the aggregates that require it.
@@ -960,6 +1079,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;
}
/*
@@ -973,6 +1100,12 @@ eval_windowaggregates(WindowAggState *winstate)
{
int ret;
+#ifdef RPR_DEBUG
+ printf("===== loop in frame starts: aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT "\n",
+ winstate->aggregatedupto,
+ winstate->aggregatedbase);
+#endif
+
/* Fetch next row if we didn't already */
if (TupIsNull(agg_row_slot))
{
@@ -989,9 +1122,53 @@ eval_windowaggregates(WindowAggState *winstate)
agg_row_slot, false);
if (ret < 0)
break;
+
if (ret == 0)
goto next_tuple;
+ if (rpr_is_defined(winstate))
+ {
+#ifdef RPR_DEBUG
+ printf("reduced_frame_map: %d aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT "\n",
+ get_reduced_frame_map(winstate,
+ winstate->aggregatedupto),
+ winstate->aggregatedupto,
+ winstate->aggregatedbase);
+#endif
+
+ /*
+ * If the row status at currentpos is already decided and current
+ * row status is not decided yet, it means we passed the last
+ * reduced frame. Time to break the loop.
+ */
+ if (get_reduced_frame_map(winstate, winstate->currentpos)
+ != RF_NOT_DETERMINED &&
+ get_reduced_frame_map(winstate, winstate->aggregatedupto)
+ == RF_NOT_DETERMINED)
+ break;
+
+ /*
+ * Otherwise we need to calculate the reduced frame.
+ */
+ ret = row_is_in_reduced_frame(winstate->agg_winobj,
+ winstate->aggregatedupto);
+ if (ret == -1) /* unmatched row */
+ break;
+
+ /*
+ * Check if current row needs to be skipped due to no match.
+ */
+ if (get_reduced_frame_map(winstate,
+ winstate->aggregatedupto) == RF_SKIPPED &&
+ winstate->aggregatedupto == winstate->aggregatedbase)
+ {
+#ifdef RPR_DEBUG
+ printf("skip current row for aggregation\n");
+#endif
+ break;
+ }
+ }
+
/* Set tuple context for evaluation of aggregate arguments */
winstate->tmpcontext->ecxt_outertuple = agg_row_slot;
@@ -1020,6 +1197,7 @@ next_tuple:
ExecClearTuple(agg_row_slot);
}
+
/* The frame's end is not supposed to move backwards, ever */
Assert(aggregatedupto_nonrestarted <= winstate->aggregatedupto);
@@ -1243,6 +1421,7 @@ begin_partition(WindowAggState *winstate)
winstate->framehead_valid = false;
winstate->frametail_valid = false;
winstate->grouptail_valid = false;
+ create_reduced_frame_map(winstate);
winstate->spooled_rows = 0;
winstate->currentpos = 0;
winstate->frameheadpos = 0;
@@ -1464,6 +1643,15 @@ release_partition(WindowAggState *winstate)
tuplestore_clear(winstate->buffer);
winstate->partition_spooled = false;
winstate->next_partition = true;
+
+ /* 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;
}
/*
@@ -2237,6 +2425,11 @@ ExecWindowAgg(PlanState *pstate)
CHECK_FOR_INTERRUPTS();
+#ifdef RPR_DEBUG
+ printf("ExecWindowAgg called. pos: " INT64_FORMAT "\n",
+ winstate->currentpos);
+#endif
+
if (winstate->status == WINDOWAGG_DONE)
return NULL;
@@ -2345,6 +2538,17 @@ 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, we need to clear
+ * existing reduced frame info so that we newly calculate the info
+ * starting from current row.
+ */
+ if (rpr_is_defined(winstate))
+ {
+ if (winstate->rpSkipTo == ST_NEXT_ROW)
+ clear_reduced_frame_map(winstate);
+ }
+
/*
* Evaluate true window functions
*/
@@ -2511,6 +2715,9 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
TupleDesc scanDesc;
ListCell *l;
+ TargetEntry *te;
+ Expr *expr;
+
/* check for unsupported flags */
Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
@@ -2609,6 +2816,16 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
winstate->temp_slot_2 = ExecInitExtraTupleSlot(estate, scanDesc,
&TTSOpsMinimalTuple);
+ winstate->prev_slot = ExecInitExtraTupleSlot(estate, scanDesc,
+ &TTSOpsMinimalTuple);
+
+ winstate->next_slot = ExecInitExtraTupleSlot(estate, scanDesc,
+ &TTSOpsMinimalTuple);
+
+ winstate->null_slot = ExecInitExtraTupleSlot(estate, scanDesc,
+ &TTSOpsMinimalTuple);
+ winstate->null_slot = ExecStoreAllNullTuple(winstate->null_slot);
+
/*
* create frame head and tail slots only if needed (must create slots in
* exactly the same cases that update_frameheadpos and update_frametailpos
@@ -2795,6 +3012,67 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
winstate->inRangeAsc = node->inRangeAsc;
winstate->inRangeNullsFirst = node->inRangeNullsFirst;
+ /* Set up SKIP TO type */
+ winstate->rpSkipTo = node->rpSkipTo;
+ /* Set up row pattern recognition PATTERN clause (compiled NFA) */
+ winstate->rpPattern = node->rpPattern;
+
+ /* Calculate NFA state size for allocation */
+ if (node->rpPattern != NULL)
+ {
+ winstate->nfaStateSize = offsetof(RPRNFAState, counts) +
+ sizeof(int32) * node->rpPattern->maxDepth;
+ }
+
+ /* Set up row pattern recognition DEFINE clause */
+ winstate->defineVariableList = NIL;
+ winstate->defineClauseList = NIL;
+ if (node->defineClause != NIL)
+ {
+ /*
+ * Tweak arg var of PREV/NEXT so that it refers to scan/inner slot.
+ */
+ foreach(l, node->defineClause)
+ {
+ char *name;
+ ExprState *exps;
+
+ te = lfirst(l);
+ name = te->resname;
+ expr = te->expr;
+
+#ifdef RPR_DEBUG
+ printf("defineVariable name: %s\n", name);
+#endif
+ winstate->defineVariableList =
+ lappend(winstate->defineVariableList,
+ makeString(pstrdup(name)));
+ attno_map((Node *) expr);
+ exps = ExecInitExpr(expr, (PlanState *) winstate);
+ winstate->defineClauseList =
+ lappend(winstate->defineClauseList, exps);
+ }
+ }
+
+ /* Initialize NFA free lists for row pattern matching */
+ winstate->nfaContext = NULL;
+ winstate->nfaContextTail = NULL;
+ winstate->nfaContextFree = NULL;
+ winstate->nfaStateFree = NULL;
+ winstate->nfaLastProcessedRow = -1;
+ winstate->nfaStatesActive = 0;
+ winstate->nfaContextsActive = 0;
+
+ /*
+ * Allocate varMatched array for NFA evaluation. With the new varNames
+ * ordering (DEFINE order first), varId == defineIdx for all defined
+ * variables, so no mapping is needed.
+ */
+ if (list_length(winstate->defineVariableList) > 0)
+ winstate->nfaVarMatched = palloc0(sizeof(bool) *
+ list_length(winstate->defineVariableList));
+ else
+ winstate->nfaVarMatched = NULL;
winstate->all_first = true;
winstate->partition_spooled = false;
winstate->more_partitions = false;
@@ -2803,6 +3081,111 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
return winstate;
}
+/*
+ * Rewrite varno of Var nodes that are the argument of PREV/NET so that they
+ * see scan tuple (PREV) or inner tuple (NEXT). Also we check the arguments
+ * of PREV/NEXT include at least 1 column reference. This is required by the
+ * SQL standard.
+ */
+static void
+attno_map(Node *node)
+{
+ (void) expression_tree_walker(node, attno_map_walker, NULL);
+}
+
+static bool
+attno_map_walker(Node *node, void *context)
+{
+ FuncExpr *func;
+ int nargs;
+ bool is_prev;
+
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, FuncExpr))
+ {
+ func = (FuncExpr *) node;
+
+ if (func->funcid == F_PREV || func->funcid == F_NEXT)
+ {
+ /*
+ * The SQL standard allows to have two more arguments form of
+ * PREV/NEXT. But currently we allow only 1 argument form.
+ */
+ nargs = list_length(func->args);
+ if (list_length(func->args) != 1)
+ elog(ERROR, "PREV/NEXT must have 1 argument but function %d has %d args",
+ func->funcid, nargs);
+
+ /*
+ * Check expr of PREV/NEXT aruguments and replace varno.
+ */
+ is_prev = (func->funcid == F_PREV) ? true : false;
+ check_rpr_navigation(node, is_prev);
+ }
+ }
+ return expression_tree_walker(node, attno_map_walker, NULL);
+}
+
+/*
+ * Rewrite varno of Var of RPR navigation operations (PREV/NEXT).
+ * If is_prev is true, we take care PREV, otherwise NEXT.
+ */
+static void
+check_rpr_navigation(Node *node, bool is_prev)
+{
+ NavigationInfo context;
+
+ context.is_prev = is_prev;
+ context.num_vars = 0;
+ (void) expression_tree_walker(node, rpr_navigation_walker, &context);
+ if (context.num_vars < 1)
+ ereport(ERROR,
+ errmsg("row pattern navigation operation's argument must include at least one column reference"));
+}
+
+static bool
+rpr_navigation_walker(Node *node, void *context)
+{
+ NavigationInfo *nav = (NavigationInfo *) context;
+
+ if (node == NULL)
+ return false;
+
+ switch (nodeTag(node))
+ {
+ case T_Var:
+ {
+ Var *var = (Var *) node;
+
+ nav->num_vars++;
+
+ if (nav->is_prev)
+ {
+ /*
+ * Rewrite varno from OUTER_VAR to regular var no so that
+ * the var references scan tuple.
+ */
+ var->varno = var->varnosyn;
+ }
+ else
+ var->varno = INNER_VAR;
+ }
+ break;
+ case T_Const:
+ case T_FuncExpr:
+ case T_OpExpr:
+ break;
+
+ default:
+ ereport(ERROR,
+ errmsg("row pattern navigation operation's argument includes unsupported expression"));
+ }
+ return expression_tree_walker(node, rpr_navigation_walker, context);
+}
+
+
/* -----------------
* ExecEndWindowAgg
* -----------------
@@ -2860,6 +3243,8 @@ ExecReScanWindowAgg(WindowAggState *node)
ExecClearTuple(node->agg_row_slot);
ExecClearTuple(node->temp_slot_1);
ExecClearTuple(node->temp_slot_2);
+ ExecClearTuple(node->prev_slot);
+ ExecClearTuple(node->next_slot);
if (node->framehead_slot)
ExecClearTuple(node->framehead_slot);
if (node->frametail_slot)
@@ -3220,7 +3605,8 @@ window_gettupleslot(WindowObject winobj, int64 pos, TupleTableSlot *slot)
return false;
if (pos < winobj->markpos)
- elog(ERROR, "cannot fetch row before WindowObject's mark position");
+ elog(ERROR, "cannot fetch row: " INT64_FORMAT " before WindowObject's mark position: " INT64_FORMAT,
+ pos, winobj->markpos);
oldcontext = MemoryContextSwitchTo(winstate->ss.ps.ps_ExprContext->ecxt_per_query_memory);
@@ -3337,6 +3723,7 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno,
int notnull_offset;
int notnull_relpos;
int forward;
+ int num_reduced_frame;
Assert(WindowObjectIsValid(winobj));
winstate = winobj->winstate;
@@ -3365,6 +3752,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 */
@@ -3380,6 +3774,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, EXCUDE clause cannot be specified and the frame is always
+ * contiguous. So we can do the check followings safely. 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 lopp.
+ */
+ 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;
@@ -3441,6 +3864,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)
@@ -3922,8 +4355,6 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
WindowAggState *winstate;
ExprContext *econtext;
TupleTableSlot *slot;
- int64 abs_pos;
- int64 mark_pos;
Assert(WindowObjectIsValid(winobj));
winstate = winobj->winstate;
@@ -3934,6 +4365,48 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
return ignorenulls_getfuncarginframe(winobj, argno, relpos, seektype,
set_mark, isnull, isout);
+ if (WinGetSlotInFrame(winobj, slot,
+ relpos, seektype, set_mark,
+ isnull, isout) == 0)
+ {
+ econtext->ecxt_outertuple = slot;
+ return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno),
+ econtext, isnull);
+ }
+
+ if (isout)
+ *isout = true;
+ *isnull = true;
+ return (Datum) 0;
+}
+
+/*
+ * 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 successfullt got the slot. false if out of frame.
+ * (also isout is set)
+ */
+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;
+ int num_reduced_frame;
+
+ Assert(WindowObjectIsValid(winobj));
+ winstate = winobj->winstate;
+
switch (seektype)
{
case WINDOW_SEEK_CURRENT:
@@ -4000,11 +4473,25 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
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;
@@ -4071,6 +4558,14 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
mark_pos = 0; /* keep compiler quiet */
break;
}
+
+ num_reduced_frame = row_is_in_reduced_frame(winobj,
+ winstate->frameheadpos + relpos);
+ if (num_reduced_frame < 0)
+ goto out_of_frame;
+ else if (num_reduced_frame > 0)
+ abs_pos = winstate->frameheadpos + relpos +
+ num_reduced_frame - 1;
break;
default:
elog(ERROR, "unrecognized window seek type: %d", seektype);
@@ -4089,15 +4584,13 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
*isout = false;
if (set_mark)
WinSetMarkPosition(winobj, mark_pos);
- econtext->ecxt_outertuple = slot;
- return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno),
- econtext, isnull);
+ return 0;
out_of_frame:
if (isout)
*isout = true;
*isnull = true;
- return (Datum) 0;
+ return -1;
}
/*
@@ -4128,3 +4621,1835 @@ WinGetFuncArgCurrent(WindowObject winobj, int argno, bool *isnull)
return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno),
econtext, isnull);
}
+
+/*
+ * 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 has been already determined that it is in a full window frame
+ * and fetched it into 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 unmatched row
+ * -2, if the row is in the reduced frame but needed to be skipped because of
+ * AFTER MATCH SKIP PAST LAST ROW
+ * -----------------
+ */
+static int
+row_is_in_reduced_frame(WindowObject winobj, int64 pos)
+{
+ WindowAggState *winstate = winobj->winstate;
+ int state;
+ int rtn;
+
+ if (!rpr_is_defined(winstate))
+ {
+ /*
+ * RPR is not defined. Assume that we are always in the the reduced
+ * window frame.
+ */
+ rtn = 0;
+#ifdef RPR_DEBUG
+ printf("row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT "\n",
+ rtn, pos);
+#endif
+ return rtn;
+ }
+
+ state = get_reduced_frame_map(winstate, pos);
+
+ if (state == RF_NOT_DETERMINED)
+ {
+ update_frameheadpos(winstate);
+ update_reduced_frame(winobj, pos);
+ }
+
+ state = get_reduced_frame_map(winstate, pos);
+
+ switch (state)
+ {
+ int64 i;
+ int num_reduced_rows;
+
+ case RF_FRAME_HEAD:
+ num_reduced_rows = 1;
+ for (i = pos + 1;
+ get_reduced_frame_map(winstate, i) == RF_SKIPPED; i++)
+ num_reduced_rows++;
+ rtn = num_reduced_rows;
+ break;
+
+ case RF_SKIPPED:
+ rtn = -2;
+ break;
+
+ case RF_UNMATCHED:
+ rtn = -1;
+ break;
+
+ default:
+ elog(ERROR, "Unrecognized state: %d at: " INT64_FORMAT,
+ state, pos);
+ break;
+ }
+
+#ifdef RPR_DEBUG
+ printf("row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT "\n",
+ rtn, pos);
+#endif
+ return rtn;
+}
+
+#define REDUCED_FRAME_MAP_INIT_SIZE 1024L
+
+/*
+ * create_reduced_frame_map
+ * Create reduced frame map
+ */
+static void
+create_reduced_frame_map(WindowAggState *winstate)
+{
+ winstate->reduced_frame_map =
+ MemoryContextAlloc(winstate->partcontext,
+ REDUCED_FRAME_MAP_INIT_SIZE);
+ winstate->alloc_sz = REDUCED_FRAME_MAP_INIT_SIZE;
+ clear_reduced_frame_map(winstate);
+}
+
+/*
+ * clear_reduced_frame_map
+ * Clear reduced frame map
+ */
+static void
+clear_reduced_frame_map(WindowAggState *winstate)
+{
+ Assert(winstate->reduced_frame_map != NULL);
+ MemSet(winstate->reduced_frame_map, RF_NOT_DETERMINED,
+ winstate->alloc_sz);
+}
+
+/*
+ * get_reduced_frame_map
+ * Get reduced frame map specified by pos
+ */
+static int
+get_reduced_frame_map(WindowAggState *winstate, int64 pos)
+{
+ Assert(winstate->reduced_frame_map != NULL);
+ Assert(pos >= 0);
+
+ /*
+ * If pos is not in the reduced frame map, it means that any info
+ * regarding the pos has not been registered yet. So we return
+ * RF_NOT_DETERMINED.
+ */
+ if (pos >= winstate->alloc_sz)
+ return RF_NOT_DETERMINED;
+
+ return winstate->reduced_frame_map[pos];
+}
+
+/*
+ * register_reduced_frame_map
+ * Add/replace reduced frame map member at pos.
+ * If there's no enough space, expand the map.
+ */
+static void
+register_reduced_frame_map(WindowAggState *winstate, int64 pos, int val)
+{
+ int64 realloc_sz;
+
+ Assert(winstate->reduced_frame_map != NULL);
+
+ if (pos < 0)
+ elog(ERROR, "wrong pos: " INT64_FORMAT, pos);
+
+ while (pos > winstate->alloc_sz - 1)
+ {
+ realloc_sz = winstate->alloc_sz * 2;
+
+ winstate->reduced_frame_map =
+ repalloc(winstate->reduced_frame_map, realloc_sz);
+
+ MemSet(winstate->reduced_frame_map + winstate->alloc_sz,
+ RF_NOT_DETERMINED, realloc_sz - winstate->alloc_sz);
+
+ winstate->alloc_sz = realloc_sz;
+ }
+
+ winstate->reduced_frame_map[pos] = val;
+}
+
+/*
+ * 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;
+ int64 currentPos;
+ int64 startPos;
+ 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 && winstate->endOffsetValue != 0)
+ 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)
+ {
+ register_reduced_frame_map(winstate, pos, RF_UNMATCHED);
+ return;
+ }
+
+ /*
+ * Case 2: Find existing context for this pos, or create new one.
+ */
+ targetCtx = nfa_get_head_context(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)
+ {
+ register_reduced_frame_map(winstate, pos, RF_UNMATCHED);
+ return;
+ }
+ /* Not yet processed - create new context and start fresh */
+ targetCtx = nfa_start_context(winstate, pos);
+ }
+ else if (targetCtx->states == NULL)
+ {
+ /* Context already completed - skip to result registration */
+ goto register_result;
+ }
+
+ /*
+ * 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.
+ */
+ for (currentPos = startPos; targetCtx->states != NULL; currentPos++)
+ {
+ bool rowExists;
+
+ /*
+ * Evaluate variables for this row - done only once, shared by all
+ * contexts
+ */
+ rowExists = nfa_evaluate_row(winobj, currentPos, winstate->nfaVarMatched);
+
+ /* No more rows in partition? Finalize all contexts */
+ if (!rowExists)
+ {
+ nfa_finalize_all_contexts(winstate, currentPos - 1);
+ /* Clean up dead contexts from finalization */
+ nfa_cleanup_dead_contexts(winstate, targetCtx);
+ /* Absorb contexts at partition boundary */
+ if (winstate->rpPattern->isAbsorbable)
+ {
+ nfa_absorb_contexts(winstate);
+ }
+ 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)
+ */
+ nfa_process_row(winstate, currentPos, hasLimitedFrame, frameOffset);
+
+ /*
+ * Create a new context for the next potential start position. This
+ * enables overlapping match detection for SKIP TO NEXT ROW.
+ */
+ nfa_start_context(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.
+ */
+ nfa_cleanup_dead_contexts(winstate, targetCtx);
+ }
+
+register_result:
+ Assert(pos == targetCtx->matchStartRow);
+
+ /*
+ * Register reduced frame map based on match result.
+ */
+ if (targetCtx->matchEndRow < targetCtx->matchStartRow)
+ {
+ matchLen = targetCtx->lastProcessedRow - targetCtx->matchStartRow + 1;
+
+ register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_UNMATCHED);
+ nfa_record_context_failure(winstate, matchLen);
+ nfa_context_free(winstate, targetCtx);
+ return;
+ }
+
+ /* Match succeeded - register frame map and record statistics */
+ matchLen = targetCtx->matchEndRow - targetCtx->matchStartRow + 1;
+
+ register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_FRAME_HEAD);
+ for (int64 i = targetCtx->matchStartRow + 1; i <= targetCtx->matchEndRow; i++)
+ {
+ register_reduced_frame_map(winstate, i, RF_SKIPPED);
+ }
+ nfa_record_context_success(winstate, matchLen);
+
+ /* Remove the matched context */
+ nfa_context_free(winstate, targetCtx);
+}
+
+/*
+ * NFA-based pattern matching implementation
+ *
+ * These functions implement direct NFA execution using the compiled
+ * RPRPattern structure, avoiding regex compilation overhead.
+ *
+ * Execution Flow: match -> absorb -> advance
+ * -----------------------------------------
+ * The NFA execution follows a three-phase cycle for each row:
+ *
+ * 1. MATCH (convergence): Evaluate all waiting states against current row.
+ * States on VAR elements are checked against their defining conditions.
+ * Failed matches are removed, successful ones may transition forward.
+ * This is a "convergence" phase - the number of states tends to decrease.
+ *
+ * 2. ABSORB: After matching, check if any context can absorb another.
+ * Context absorption is an optimization that merges equivalent contexts.
+ * A context can only be absorbed if ALL its states are absorbable.
+ *
+ * 3. ADVANCE (divergence): Expand states through epsilon transitions.
+ * States advance through ALT (alternation), END (group end), and
+ * optional elements until reaching VAR or FIN elements where they wait.
+ * This is a "divergence" phase - ALT creates multiple branch states.
+ *
+ * Key Design Decisions:
+ * ---------------------
+ * - VAR->END transition in match phase: When a simple VAR (max=1) matches
+ * and the next element is END, we transition immediately in the match
+ * phase rather than waiting for advance. This is necessary for correct
+ * absorption: states must be at END to be marked absorbable before the
+ * absorption check occurs.
+ *
+ * - Optional VAR skip paths: When advance lands on a VAR with min=0,
+ * we create both a waiting state AND a skip state (like ALT branches).
+ * This ensures patterns like "A B? C" work correctly - we need a state
+ * waiting for B AND a state that has already skipped to C.
+ *
+ * - END->END count increment: When transitioning from one END to another
+ * END within advance, we must increment the outer END's count. This
+ * handles nested groups like "((A|B)+)+" correctly - exiting the inner
+ * group counts as one iteration of the outer group.
+ *
+ * - initialAdvance flag: The first advance after context creation must
+ * skip FIN recording. Reaching FIN without evaluating any VAR would
+ * create a zero-length match, which is invalid.
+ *
+ * Context Absorption Runtime:
+ * ---------------------------
+ * Absorption uses flags computed at planning time (in rpr.c) and two
+ * context-level flags maintained at runtime:
+ *
+ * State-level:
+ * state.isAbsorbable: true if state is in the absorbable region.
+ * - Set at creation: elem->flags & RPR_ELEM_ABSORBABLE_BRANCH
+ * - At transition: prevAbsorbable && (newElem->flags & ABSORBABLE_BRANCH)
+ * - Monotonic: once false, stays false forever
+ *
+ * Context-level:
+ * ctx.hasAbsorbableState: can this context absorb others?
+ * - True if at least one state has isAbsorbable=true
+ * - Monotonic: true->false only (optimization: skip recalc when false)
+ *
+ * ctx.allStatesAbsorbable: can this context be absorbed?
+ * - True if ALL states have isAbsorbable=true
+ * - Dynamic: can change false->true (when non-absorbable states die)
+ *
+ * Absorption Algorithm:
+ * For each pair (older Ctx1, newer Ctx2):
+ * 1. Pre-check: Ctx1.hasAbsorbableState && Ctx2.allStatesAbsorbable
+ * -> If false, skip (fast filter)
+ * 2. Coverage check: For each Ctx2 state with isAbsorbable=true,
+ * find Ctx1 state with same elemIdx and count >= Ctx2.count
+ * 3. If all Ctx2 absorbable states are covered, absorb Ctx2
+ *
+ * Example: Pattern A+ B
+ * Row 1: Ctx1 at A (count=1)
+ * Row 2: Ctx1 at A (count=2), Ctx2 at A (count=1)
+ * -> Both at same elemIdx (A), Ctx1.count >= Ctx2.count
+ * -> Ctx2 absorbed
+ *
+ * The asymmetric design (Ctx1 needs hasAbsorbable, Ctx2 needs allAbsorbable)
+ * allows absorption even when Ctx1 has extra non-absorbable states.
+ */
+
+/*
+ * nfa_process_row
+ *
+ * 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
+ */
+static void
+nfa_process_row(WindowAggState *winstate, int64 currentPos,
+ bool hasLimitedFrame, int64 frameOffset)
+{
+ RPRNFAContext *ctx;
+ bool *varMatched = winstate->nfaVarMatched;
+
+ /*
+ * 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 if exceeded */
+ if (hasLimitedFrame)
+ {
+ int64 ctxFrameEnd = ctx->matchStartRow + frameOffset + 1;
+
+ if (currentPos >= ctxFrameEnd)
+ {
+ /* Frame boundary exceeded: force mismatch */
+ nfa_match(winstate, ctx, NULL);
+ continue;
+ }
+ }
+
+ nfa_match(winstate, ctx, varMatched);
+ 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.
+ */
+ Assert(!hasLimitedFrame ||
+ currentPos < ctx->matchStartRow + frameOffset + 1);
+
+ nfa_advance(winstate, ctx, currentPos, false);
+ }
+}
+
+/*
+ * nfa_state_alloc
+ *
+ * Allocate an NFA state, reusing from freeList if available.
+ * freeList is stored in WindowAggState for reuse across match attempts.
+ * Uses flexible array member for counts[].
+ */
+static RPRNFAState *
+nfa_state_alloc(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++;
+ if (winstate->nfaStatesActive > winstate->nfaStatesMax)
+ 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--;
+ state->next = winstate->nfaStateFree;
+ winstate->nfaStateFree = state;
+}
+
+/*
+ * 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_create
+ *
+ * Create a new state with given elemIdx, altPriority 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_create(WindowAggState *winstate, int16 elemIdx, int16 altPriority,
+ int32 *counts, bool sourceAbsorbable)
+{
+ RPRPattern *pattern = winstate->rpPattern;
+ int maxDepth = pattern->maxDepth;
+ RPRNFAState *state = nfa_state_alloc(winstate);
+ RPRPatternElement *elem = &pattern->elements[elemIdx];
+
+ state->elemIdx = elemIdx;
+ state->altPriority = altPriority;
+ if (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_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 */
+ elem = &pattern->elements[s1->elemIdx];
+ compareDepth = elem->depth + 1; /* depth 0 needs 1 count, etc. */
+
+ if (compareDepth > 0 &&
+ memcmp(s1->counts, s2->counts, sizeof(int32) * compareDepth) != 0)
+ return false;
+
+ return true;
+}
+
+/*
+ * nfa_add_state_unique
+ *
+ * Add a state to ctx->states at the END, only if no duplicate exists.
+ * Returns true if state was added, false if duplicate found (state is freed).
+ * Earlier states have lower altPriority (lexical order), so existing wins.
+ */
+static bool
+nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *state)
+{
+ RPRNFAState *s;
+ RPRNFAState *tail = NULL;
+
+ /* Check for duplicate and find tail */
+ for (s = ctx->states; s != NULL; s = s->next)
+ {
+ if (nfa_states_equal(winstate, s, state))
+ {
+ /*
+ * Duplicate found - existing has better lexical order, discard
+ * new
+ */
+ nfa_state_free(winstate, state);
+ winstate->nfaStatesMerged++;
+ return false;
+ }
+ tail = s;
+ }
+
+ /* No duplicate, add at end */
+ state->next = NULL;
+ if (tail == NULL)
+ ctx->states = state;
+ else
+ tail->next = state;
+
+ return true;
+}
+
+/*
+ * nfa_add_matched_state
+ *
+ * Record a matched state following SQL standard semantics.
+ * Lexical order (lower altPriority) wins first. Among same lexical order,
+ * longer match wins (greedy).
+ *
+ * FIXME: altPriority is a single value that only tracks the last ALT choice.
+ * For patterns with repeated or nested ALTs like (A|B)+, this cannot correctly
+ * implement SQL standard lexical order, which requires comparing the full path
+ * from left to right. For example:
+ * Pattern: (A | B)+
+ * Path "A B A" vs "B A B"
+ * Current: compares last choice (A vs B) → altPriority 10 vs 20
+ * Correct: should compare first choice (A < B) → "A B A" wins
+ *
+ * A classifier structure tracking the entire ALT path is required for correct
+ * implementation. Without it, patterns with repeated or nested ALTs will
+ * produce incorrect match selection.
+ */
+static void
+nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
+ RPRNFAState *state, int64 matchEndRow)
+{
+ bool shouldUpdate = false;
+
+ if (ctx->matchedState == NULL)
+ shouldUpdate = true;
+ else if (state->altPriority < ctx->matchedState->altPriority)
+ shouldUpdate = true; /* Better lexical order wins */
+ else if (state->altPriority == ctx->matchedState->altPriority &&
+ matchEndRow > ctx->matchEndRow)
+ shouldUpdate = true; /* Same lexical order, longer wins */
+
+ if (shouldUpdate)
+ {
+ /* Free old matchedState if exists */
+ if (ctx->matchedState != NULL)
+ nfa_state_free(winstate, ctx->matchedState);
+
+ /* Take ownership of the new state */
+ ctx->matchedState = state;
+ state->next = NULL;
+ ctx->matchEndRow = matchEndRow;
+
+ /*----------
+ * SKIP PAST LAST ROW: eagerly prune contexts within match range.
+ *
+ * This function is called whenever a FIN state is reached, including
+ * during greedy matching when intermediate (shorter) matches are
+ * found. Each time we update matchEndRow (whether extending a greedy
+ * match or finding a new match), we can prune pending contexts that
+ * started within the current match range.
+ *
+ * SKIP PAST LAST ROW uses lexical order (matchStartRow). Therefore,
+ * any pending context that started at or before matchEndRow can never
+ * produce a valid output row - it would be skipped anyway per SQL
+ * standard.
+ *
+ * Example (greedy matching in progress):
+ * Pattern: START UP+
+ * Rows: 1 2 3 4 5
+ * Context A starts at row 1:
+ * - Matches START UP (rows 1-2) → matchEndRow=2 → prune Context B(row 2)
+ * - Matches START UP UP (rows 1-3) → matchEndRow=3 → prune Context C(row 3)
+ * - Continues greedy extension while pruning incrementally
+ *----------
+ */
+ if (winstate->rpSkipTo == ST_PAST_LAST_ROW)
+ {
+ RPRNFAContext *nextCtx;
+ int64 skippedLen;
+
+ while (ctx->next != NULL &&
+ ctx->next->matchStartRow <= matchEndRow)
+ {
+ nextCtx = ctx->next;
+ ctx->next = ctx->next->next;
+
+ Assert(nextCtx->lastProcessedRow >= nextCtx->matchStartRow);
+ skippedLen = nextCtx->lastProcessedRow - nextCtx->matchStartRow + 1;
+ nfa_record_context_skipped(winstate, skippedLen);
+
+ nfa_context_free(winstate, nextCtx);
+ }
+ if (ctx->next == NULL)
+ winstate->nfaContextTail = ctx;
+ }
+ }
+ else
+ {
+ /* This state didn't win, free it */
+ nfa_state_free(winstate, state);
+ }
+}
+
+/*
+ * nfa_context_alloc
+ *
+ * Allocate an NFA context, reusing from free list if available.
+ */
+static RPRNFAContext *
+nfa_context_alloc(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;
+ /* Initialize two-flag absorption design based on pattern */
+ ctx->hasAbsorbableState = (winstate->rpPattern != NULL &&
+ winstate->rpPattern->isAbsorbable);
+ ctx->allStatesAbsorbable = (winstate->rpPattern != NULL &&
+ winstate->rpPattern->isAbsorbable);
+
+ /* Update statistics */
+ winstate->nfaContextsActive++;
+ winstate->nfaContextsTotalCreated++;
+ if (winstate->nfaContextsActive > winstate->nfaContextsMax)
+ 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_context_free
+ *
+ * Unlink context from active list and return it to free list.
+ * Also frees any states in the context.
+ */
+static void
+nfa_context_free(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;
+}
+
+/*
+ * nfa_start_context
+ *
+ * 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.
+ */
+static RPRNFAContext *
+nfa_start_context(WindowAggState *winstate, int64 startPos)
+{
+ RPRNFAContext *ctx;
+ RPRPattern *pattern = winstate->rpPattern;
+
+ ctx = nfa_context_alloc(winstate);
+ ctx->matchStartRow = startPos;
+ ctx->states = nfa_state_alloc(winstate); /* initial state at elem 0 */
+
+ /*--------------------------
+ * Initialize two-flag absorption design:
+ * hasAbsorbableState: can this context absorb others? (>= 1 absorbable state)
+ * allStatesAbsorbable: can this context be absorbed? (ALL states absorbable)
+ * Both initialized from pattern->isAbsorbable at context start.
+ */
+ ctx->hasAbsorbableState = (pattern != NULL && pattern->isAbsorbable);
+ ctx->allStatesAbsorbable = (pattern != NULL && pattern->isAbsorbable);
+
+ if (ctx->states != NULL && pattern != NULL && pattern->numElements > 0)
+ {
+ RPRPatternElement *elem = &pattern->elements[0];
+
+ /*
+ * Initial state at element 0. Check if element 0 is in absorbable
+ * branch.
+ */
+ if (RPRElemIsAbsorbableBranch(elem))
+ {
+ /* Element 0 is in absorbable branch - flags stay true */
+ ctx->states->isAbsorbable = true;
+ }
+ else
+ {
+ /* Element 0 is NOT in absorbable branch - turn flags OFF */
+ ctx->hasAbsorbableState = false;
+ ctx->allStatesAbsorbable = false;
+ ctx->states->isAbsorbable = false;
+ }
+ }
+
+ /* Add to tail of active context list (doubly-linked, oldest-first) */
+ 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.
+ *
+ * Pass initialAdvance=true to prevent recording zero-length matches when
+ * optional patterns can skip all VARs to reach FIN immediately.
+ */
+ nfa_advance(winstate, ctx, startPos, true);
+
+ return ctx;
+}
+
+/*
+ * nfa_get_head_context
+ *
+ * Return the head context if its start position matches pos.
+ * Returns NULL if no context exists or head doesn't match pos.
+ */
+static RPRNFAContext *
+nfa_get_head_context(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;
+}
+
+/*
+ * 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
+ {
+ if (newLen < stats->min)
+ stats->min = newLen;
+ if (newLen > stats->max)
+ stats->max = newLen;
+ }
+ stats->total += newLen;
+}
+
+/*
+ * nfa_record_context_success
+ *
+ * Record a successful context in statistics.
+ */
+static void
+nfa_record_context_success(WindowAggState *winstate, int64 matchLen)
+{
+ winstate->nfaMatchesSucceeded++;
+ nfa_update_length_stats(winstate->nfaMatchesSucceeded,
+ &winstate->nfaMatchLen,
+ matchLen);
+}
+
+/*
+ * nfa_record_context_failure
+ *
+ * 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.
+ */
+static void
+nfa_record_context_failure(WindowAggState *winstate, int64 failedLen)
+{
+ if (failedLen == 1)
+ {
+ winstate->nfaContextsPruned++;
+ }
+ else
+ {
+ winstate->nfaMatchesFailed++;
+ nfa_update_length_stats(winstate->nfaMatchesFailed,
+ &winstate->nfaFailLen,
+ failedLen);
+ }
+}
+
+/*
+ * 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_evaluate_row
+ *
+ * Evaluate all DEFINE variables for current row.
+ * Returns true if the row exists, false if out of partition.
+ * If row exists, fills varMatched array.
+ * varMatched[i] = true if variable i matched at current row.
+ */
+static bool
+nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
+{
+ WindowAggState *winstate = winobj->winstate;
+ ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+ int numDefineVars = list_length(winstate->defineVariableList);
+ ListCell *lc;
+ int varIdx = 0;
+ TupleTableSlot *slot;
+
+ /*
+ * Set up slots for current, previous, and next rows. We don't call
+ * get_slots() here to avoid recursion through row_is_in_frame ->
+ * update_reduced_frame -> nfa_process_row.
+ */
+
+ /* Current row -> ecxt_outertuple */
+ slot = winstate->temp_slot_1;
+ if (!window_gettupleslot(winobj, pos, slot))
+ return false; /* No row exists */
+ econtext->ecxt_outertuple = slot;
+
+ /* Previous row -> ecxt_scantuple (for PREV) */
+ if (pos > 0)
+ {
+ slot = winstate->prev_slot;
+ if (!window_gettupleslot(winobj, pos - 1, slot))
+ econtext->ecxt_scantuple = winstate->null_slot;
+ else
+ econtext->ecxt_scantuple = slot;
+ }
+ else
+ econtext->ecxt_scantuple = winstate->null_slot;
+
+ /* Next row -> ecxt_innertuple (for NEXT) */
+ slot = winstate->next_slot;
+ if (!window_gettupleslot(winobj, pos + 1, slot))
+ econtext->ecxt_innertuple = winstate->null_slot;
+ else
+ econtext->ecxt_innertuple = slot;
+
+ foreach(lc, winstate->defineClauseList)
+ {
+ ExprState *exprState = (ExprState *) lfirst(lc);
+ Datum result;
+ bool isnull;
+
+ /* Evaluate DEFINE expression */
+ result = ExecEvalExpr(exprState, econtext, &isnull);
+
+ varMatched[varIdx] = (!isnull && DatumGetBool(result));
+
+ varIdx++;
+ if (varIdx >= numDefineVars)
+ break;
+ }
+
+ return true; /* Row exists */
+}
+
+/*
+ * nfa_cleanup_dead_contexts
+ *
+ * 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).
+ */
+static void
+nfa_cleanup_dead_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
+{
+ RPRNFAContext *ctx;
+ RPRNFAContext *next;
+
+ for (ctx = winstate->nfaContext; ctx != NULL; ctx = next)
+ {
+ next = ctx->next;
+
+ /* Skip the target context and contexts still processing */
+ if (ctx == excludeCtx || ctx->states != NULL)
+ continue;
+
+ /* Skip successfully matched contexts (will be handled by SKIP logic) */
+ if (ctx->matchEndRow >= ctx->matchStartRow)
+ continue;
+
+ /*
+ * This is a failed context - count and remove it. Only count if it
+ * actually processed its start row. Contexts created for
+ * beyond-partition rows are silently removed.
+ */
+ if (ctx->lastProcessedRow >= ctx->matchStartRow)
+ {
+ int64 failedLen = ctx->lastProcessedRow - ctx->matchStartRow + 1;
+
+ nfa_record_context_failure(winstate, failedLen);
+ }
+ /* else: context was never processed (beyond-partition), just remove */
+
+ nfa_context_free(winstate, ctx);
+ }
+}
+
+/*
+ * nfa_finalize_all_contexts
+ *
+ * Finalize all active contexts when partition ends.
+ * Match with NULL to force mismatch, then advance to process epsilon transitions.
+ */
+static void
+nfa_finalize_all_contexts(WindowAggState *winstate, int64 lastPos)
+{
+ RPRNFAContext *ctx;
+
+ for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
+ {
+ if (ctx->states != NULL)
+ {
+ nfa_match(winstate, ctx, NULL);
+ nfa_advance(winstate, ctx, lastPos, false);
+ }
+ }
+}
+
+/*
+ * 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.
+ * This flag is dynamic and can change false -> true when non-absorbable
+ * states die off.
+ *
+ * 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 judgment
+ * point.
+ */
+ for (state = ctx->states; state != NULL; state = state->next)
+ {
+ if (state->isAbsorbable)
+ hasAbsorbable = true;
+ else
+ 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;
+
+ for (olderState = older->states; olderState != NULL; olderState = olderState->next)
+ {
+ /* 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.
+ * Returns true if ctx was absorbed and freed.
+ *
+ * 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 bool
+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 false;
+
+ for (older = ctx->prev; older != NULL; older = older->prev)
+ {
+ /*
+ * 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;
+
+ nfa_context_free(winstate, ctx);
+ nfa_record_context_absorbed(winstate, absorbedLen);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ * 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.
+ * Undefined variables (varId >= defineVariableList length) default to TRUE.
+ */
+static inline bool
+nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
+ bool *varMatched)
+{
+ /* This function should only be called for VAR elements */
+ Assert(RPRElemIsVar(elem));
+
+ if (varMatched == NULL)
+ return false;
+ if (elem->varId >= list_length(winstate->defineVariableList))
+ return true;
+ return varMatched[elem->varId];
+}
+
+/*
+ * 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++, keep state (unless count > max)
+ * - not matched: remove state (exit alternatives already exist from
+ * previous advance when count >= min was satisfied)
+ *
+ * For simple VARs (min=max=1) followed by END:
+ * - Advance to END and update group count before absorb phase
+ * - This ensures absorption can compare states by group completion
+ *
+ * Non-VAR elements (ALT, END, FIN) are kept as-is for advance phase.
+ */
+static void
+nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
+{
+ RPRPattern *pattern = winstate->rpPattern;
+ RPRPatternElement *elements = pattern->elements;
+ RPRNFAState **prevPtr = &ctx->states;
+ RPRNFAState *state;
+ RPRNFAState *nextState;
+
+ /*
+ * Evaluate VAR elements against current row. For simple VARs with END
+ * next, advance to END and update group count inline so absorb phase can
+ * compare states properly.
+ */
+ for (state = ctx->states; state != NULL; state = nextState)
+ {
+ RPRPatternElement *elem = &elements[state->elemIdx];
+
+ 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 */
+ if (count < RPR_COUNT_MAX)
+ count++;
+
+ /* Max constraint should not be exceeded */
+ Assert(elem->max == RPR_QUANTITY_INF || count <= elem->max);
+
+ state->counts[depth] = count;
+
+ /*
+ * For simple VAR (min=max=1) with END next, advance to END
+ * and update group count inline. This keeps state in place,
+ * preserving lexical order.
+ */
+ if (elem->min == 1 && elem->max == 1 &&
+ RPRElemIsEnd(&elements[elem->next]))
+ {
+ RPRPatternElement *endElem = &elements[elem->next];
+ int endDepth = endElem->depth;
+ int32 endCount = state->counts[endDepth];
+
+ Assert(count == 1);
+
+ /* Increment group count with overflow protection */
+ if (endCount < RPR_COUNT_MAX)
+ endCount++;
+
+ /*
+ * END's max can never be exceeded here because
+ * nfa_advance_end only loops when count < max, so
+ * endCount entering inline advance is at most max-1, and
+ * incrementing yields at most max.
+ */
+ Assert(endElem->max == RPR_QUANTITY_INF ||
+ endCount <= endElem->max);
+
+ state->elemIdx = elem->next;
+ state->counts[endDepth] = endCount;
+ }
+ /* 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, bool initialAdvance)
+{
+ if (RPRElemIsVar(nextElem))
+ {
+ nfa_add_state_unique(winstate, ctx, state);
+ if (RPRElemCanSkip(nextElem))
+ {
+ RPRNFAState *skipState;
+
+ skipState = nfa_state_create(winstate, nextElem->next,
+ state->altPriority, state->counts,
+ state->isAbsorbable);
+ nfa_advance_state(winstate, ctx, skipState, currentPos, initialAdvance);
+ }
+ }
+ else
+ {
+ nfa_advance_state(winstate, ctx, state, currentPos, initialAdvance);
+ }
+}
+
+/*
+ * nfa_advance_alt
+ *
+ * Handle ALT element: expand all branches in lexical order (DFS).
+ * Sets altPriority to element index to preserve lexical order for match selection.
+ */
+static void
+nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
+ RPRNFAState *state, RPRPatternElement *elem,
+ int64 currentPos, bool initialAdvance)
+{
+ RPRPattern *pattern = winstate->rpPattern;
+ RPRPatternElement *elements = pattern->elements;
+ RPRElemIdx altIdx = elem->next;
+ bool first = true;
+
+ while (altIdx >= 0 && altIdx < pattern->numElements)
+ {
+ RPRPatternElement *altElem = &elements[altIdx];
+ RPRNFAState *newState;
+
+ /* Stop if element is outside ALT scope (not a branch) */
+ if (altElem->depth <= elem->depth)
+ break;
+
+ if (first)
+ {
+ state->elemIdx = altIdx;
+ state->altPriority = altIdx;
+ newState = state;
+ first = false;
+ }
+ else
+ {
+ newState = nfa_state_create(winstate, altIdx, altIdx,
+ state->counts, state->isAbsorbable);
+ }
+
+ /* Recursively process this branch before next */
+ nfa_advance_state(winstate, ctx, newState, currentPos, initialAdvance);
+ altIdx = altElem->jump;
+ }
+
+ /* ALT must have at least one branch */
+ Assert(!first);
+}
+
+/*
+ * nfa_advance_begin
+ *
+ * Handle BEGIN element: group entry logic.
+ * BEGIN is only visited at initial group entry (count is always 0).
+ * If min=0, creates a skip path past the group.
+ * Loop-back from END goes directly to first child, bypassing BEGIN.
+ */
+static void
+nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
+ RPRNFAState *state, RPRPatternElement *elem,
+ int64 currentPos, bool initialAdvance)
+{
+ RPRPattern *pattern = winstate->rpPattern;
+ RPRPatternElement *elements = pattern->elements;
+ RPRNFAState *skipState = NULL;
+
+ state->counts[elem->depth] = 0;
+
+ /* Optional group: create skip path (but don't route yet) */
+ if (elem->min == 0)
+ {
+ skipState = nfa_state_create(winstate, elem->jump, state->altPriority,
+ state->counts, state->isAbsorbable);
+ }
+
+ /* Enter group: route to first child (lexically first) */
+ state->elemIdx = elem->next;
+ nfa_route_to_elem(winstate, ctx, state,
+ &elements[state->elemIdx], currentPos, initialAdvance);
+
+ /* Now route skip path (lexically second) */
+ if (skipState != NULL)
+ {
+ nfa_route_to_elem(winstate, ctx, skipState,
+ &elements[elem->jump], currentPos, initialAdvance);
+ }
+}
+
+/*
+ * 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, bool initialAdvance)
+{
+ RPRPattern *pattern = winstate->rpPattern;
+ RPRPatternElement *elements = pattern->elements;
+ int depth = elem->depth;
+ int32 count = state->counts[depth];
+
+ if (count < elem->min)
+ {
+ /* Must loop back */
+ RPRPatternElement *jumpElem;
+
+ for (int d = depth + 1; d < pattern->maxDepth; d++)
+ state->counts[d] = 0;
+ state->elemIdx = elem->jump;
+ jumpElem = &elements[state->elemIdx];
+
+ nfa_route_to_elem(winstate, ctx, state, jumpElem, currentPos, initialAdvance);
+ }
+ else if ((elem->max != RPR_QUANTITY_INF && count >= elem->max) ||
+ (count == 0 && elem->min == 0))
+ {
+ /*----------
+ * Must exit: either reached max iterations, or group matched empty.
+ *
+ * FIXME: The (count == 0 && min == 0) condition is insufficient for
+ * cycle prevention. Cycles can occur at any count value when loop back
+ * happens without consuming rows. For example:
+ * Pattern: (A*)*
+ * After matching 3 A's (count=3), loop back at a B row
+ * Inner A* matches 0 times (skip path) → same (elemIdx, count=3)
+ * Infinite cycle at count=3, not count=0
+ *
+ * Currently, cycles are silently prevented by nfa_add_state_unique
+ * detecting duplicate states, but this is implicit and not guaranteed
+ * for all code paths. Explicit cycle detection is needed.
+ *----------
+ */
+ RPRPatternElement *nextElem;
+
+ state->counts[depth] = 0;
+ state->elemIdx = elem->next;
+ nextElem = &elements[state->elemIdx];
+
+ /* END->END: increment outer END's count */
+ if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
+ state->counts[nextElem->depth]++;
+
+ nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos, initialAdvance);
+ }
+ else
+ {
+ /*
+ * Between min and max (with at least one iteration) - can exit or
+ * loop
+ */
+ RPRElemIdx exitAltPriority;
+ RPRNFAState *exitState;
+ RPRPatternElement *jumpElem;
+ RPRPatternElement *nextElem;
+
+ /* Preserve altPriority for greedy extension */
+ exitAltPriority = state->altPriority;
+ if (ctx->matchedState != NULL)
+ exitAltPriority = ctx->matchedState->altPriority;
+
+ /*
+ * Create exit state first (need original counts before modifying
+ * state)
+ */
+ exitState = nfa_state_create(winstate, elem->next, exitAltPriority,
+ state->counts, state->isAbsorbable);
+ exitState->counts[depth] = 0;
+ nextElem = &elements[exitState->elemIdx];
+
+ /* END->END: increment outer END's count */
+ if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_MAX)
+ exitState->counts[nextElem->depth]++;
+
+ /* Route loop state first (earlier in pattern = lexical order) */
+ for (int d = depth + 1; d < pattern->maxDepth; d++)
+ state->counts[d] = 0;
+ state->elemIdx = elem->jump;
+ jumpElem = &elements[state->elemIdx];
+
+ nfa_route_to_elem(winstate, ctx, state, jumpElem, currentPos, initialAdvance);
+
+ /* Then route exit state */
+ nfa_route_to_elem(winstate, ctx, exitState, nextElem, currentPos, initialAdvance);
+ }
+}
+
+/*
+ * 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, bool initialAdvance)
+{
+ RPRPattern *pattern = winstate->rpPattern;
+ RPRPatternElement *elements = pattern->elements;
+ int depth = elem->depth;
+ int32 count = state->counts[depth];
+ bool canLoop = (elem->max == RPR_QUANTITY_INF || count < elem->max);
+ bool canExit = (count >= elem->min);
+
+ /* After a successful match, count >= 1, so at least one must be true */
+ Assert(canLoop || canExit);
+
+ if (canLoop && canExit)
+ {
+ /* Both: clone for loop, modify original for exit */
+ RPRNFAState *loopState;
+ RPRPatternElement *nextElem;
+
+ loopState = nfa_state_create(winstate, state->elemIdx, state->altPriority,
+ state->counts, state->isAbsorbable);
+ nfa_add_state_unique(winstate, ctx, loopState);
+
+ /* Exit: advance to next element */
+ state->counts[depth] = 0;
+ state->elemIdx = elem->next;
+ nextElem = &elements[state->elemIdx];
+
+ nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos, initialAdvance);
+ }
+ else if (canLoop)
+ {
+ /* Loop only: keep state as-is */
+ nfa_add_state_unique(winstate, ctx, state);
+ }
+ else if (canExit)
+ {
+ /* Exit only: advance to next element */
+ RPRPatternElement *nextElem;
+
+ state->counts[depth] = 0;
+ state->elemIdx = elem->next;
+ nextElem = &elements[state->elemIdx];
+
+ nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos, initialAdvance);
+ }
+}
+
+/*
+ * nfa_advance_state
+ *
+ * Recursively process a single state through epsilon transitions.
+ * Uses DFS traversal to maintain lexical order: lower altPriority paths
+ * are fully processed before higher altPriority paths, ensuring states
+ * are added to ctx->states in lexical order.
+ */
+static void
+nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
+ RPRNFAState *state, int64 currentPos, bool initialAdvance)
+{
+ RPRPattern *pattern = winstate->rpPattern;
+ RPRPatternElement *elem;
+
+ Assert(state->elemIdx >= 0 && state->elemIdx < pattern->numElements);
+ elem = &pattern->elements[state->elemIdx];
+
+ switch (elem->varId)
+ {
+ case RPR_VARID_FIN:
+ /* FIN: record match (skip for initial advance) */
+ if (!initialAdvance)
+ nfa_add_matched_state(winstate, ctx, state, currentPos);
+ else
+ nfa_state_free(winstate, state);
+ break;
+
+ case RPR_VARID_ALT:
+ nfa_advance_alt(winstate, ctx, state, elem, currentPos, initialAdvance);
+ break;
+
+ case RPR_VARID_BEGIN:
+ nfa_advance_begin(winstate, ctx, state, elem, currentPos, initialAdvance);
+ break;
+
+ case RPR_VARID_END:
+ nfa_advance_end(winstate, ctx, state, elem, currentPos, initialAdvance);
+ break;
+
+ default:
+ /* VAR element */
+ nfa_advance_var(winstate, ctx, state, elem, currentPos, initialAdvance);
+ 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 (initialAdvance=true skips FIN matches).
+ *
+ * Processes states in order, using recursive DFS to maintain lexical order.
+ */
+static void
+nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos,
+ bool initialAdvance)
+{
+ RPRNFAState *states = ctx->states;
+ RPRNFAState *state;
+
+ ctx->states = NULL; /* Will rebuild */
+
+ /* Process each state in order */
+ while (states != NULL)
+ {
+ state = states;
+ states = states->next;
+ state->next = NULL;
+
+ nfa_advance_state(winstate, ctx, state, currentPos, initialAdvance);
+ }
+}
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 78b7f05aba2..efb60c99052 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -41,7 +41,6 @@ static bool rank_up(WindowObject winobj);
static Datum leadlag_common(FunctionCallInfo fcinfo,
bool forward, bool withoffset, bool withdefault);
-
/*
* utility routine for *_rank functions.
*/
@@ -683,7 +682,7 @@ window_last_value(PG_FUNCTION_ARGS)
WinCheckAndInitializeNullTreatment(winobj, true, fcinfo);
result = WinGetFuncArgInFrame(winobj, 0,
- 0, WINDOW_SEEK_TAIL, true,
+ 0, WINDOW_SEEK_TAIL, false,
&isnull, NULL);
if (isnull)
PG_RETURN_NULL();
@@ -724,3 +723,25 @@ window_nth_value(PG_FUNCTION_ARGS)
PG_RETURN_DATUM(result);
}
+
+/*
+ * prev
+ * Dummy function to invoke RPR's navigation operator "PREV".
+ * This is *not* a window function.
+ */
+Datum
+window_prev(PG_FUNCTION_ARGS)
+{
+ PG_RETURN_DATUM(PG_GETARG_DATUM(0));
+}
+
+/*
+ * next
+ * Dummy function to invoke RPR's navigation operation "NEXT".
+ * This is *not* a window function.
+ */
+Datum
+window_next(PG_FUNCTION_ARGS)
+{
+ PG_RETURN_DATUM(PG_GETARG_DATUM(0));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 83f6501df38..ea35b9cb1de 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10853,6 +10853,12 @@
{ oid => '3114', descr => 'fetch the Nth row value',
proname => 'nth_value', prokind => 'w', prorettype => 'anyelement',
proargtypes => 'anyelement int4', prosrc => 'window_nth_value' },
+{ oid => '8126', descr => 'previous value',
+ proname => 'prev', provolatile => 's', prorettype => 'anyelement',
+ proargtypes => 'anyelement', prosrc => 'window_prev' },
+{ oid => '8127', descr => 'next value',
+ proname => 'next', provolatile => 's', prorettype => 'anyelement',
+ proargtypes => 'anyelement', prosrc => 'window_next' },
# functions for range types
{ oid => '3832', descr => 'I/O',
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 63c067d5aae..cd6f794f62b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -41,6 +41,7 @@
#include "nodes/plannodes.h"
#include "nodes/tidbitmap.h"
#include "partitioning/partdefs.h"
+#include "regex/regex.h"
#include "storage/condition_variable.h"
#include "utils/hsearch.h"
#include "utils/queryenvironment.h"
@@ -2513,6 +2514,76 @@ typedef enum WindowAggStatus
* tuples during spool */
} WindowAggStatus;
+#define RF_NOT_DETERMINED 0
+#define RF_FRAME_HEAD 1
+#define RF_SKIPPED 2
+#define RF_UNMATCHED 3
+
+/*
+ * RPRNFAState - single NFA state for pattern matching
+ *
+ * counts[] tracks repetition counts at each nesting depth.
+ * altPriority tracks lexical order for alternation (lower = earlier alternative).
+ *
+ * isAbsorbable tracks if state is in absorbable region (ABSORBABLE_BRANCH).
+ * Monotonic property: once false, stays false (can't re-enter region).
+ *
+ * Absorption comparison uses elemIdx and counts[depth] directly because:
+ * - VAR elements consume a row, forcing states to wait for next row
+ * - END loop puts states at group start with iteration count preserved
+ * - At row end, comparable states are at the same position (elemIdx)
+ */
+typedef struct RPRNFAState
+{
+ struct RPRNFAState *next; /* next state in linked list */
+ int16 elemIdx; /* current pattern element index */
+ int16 altPriority; /* lexical order priority (lower = preferred) */
+ 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)
+ * - Dynamic: can change false->true (when non-absorbable states die)
+ * - 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; /* row where match ended (-1 = no match) */
+ int64 lastProcessedRow; /* last row processed (for fail depth) */
+ RPRNFAState *matchedState; /* FIN state for greedy fallback (cloned) */
+
+ /* 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;
+
typedef struct WindowAggState
{
ScanState ss; /* its first field is NodeTag */
@@ -2572,6 +2643,42 @@ 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 *defineVariableList; /* list of row pattern definition
+ * variables (list of String) */
+ List *defineClauseList; /* expression for row pattern definition
+ * search conditions ExprState list */
+ 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 */
+ bool *nfaVarMatched; /* per-row cache: varMatched[varId] for varId
+ * < numDefines */
+ 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 */
@@ -2599,6 +2706,18 @@ typedef struct WindowAggState
TupleTableSlot *agg_row_slot;
TupleTableSlot *temp_slot_1;
TupleTableSlot *temp_slot_2;
+
+ /* temporary slots for RPR */
+ TupleTableSlot *prev_slot; /* PREV row navigation operator */
+ TupleTableSlot *next_slot; /* NEXT row navigation operator */
+ TupleTableSlot *null_slot; /* all NULL slot */
+
+ /*
+ * Each byte corresponds to a row positioned at absolute its pos in
+ * partition. See above definition for RF_*. Used for RPR.
+ */
+ char *reduced_frame_map;
+ int64 alloc_sz; /* size of the map */
} WindowAggState;
/* ----------------
--
2.43.0
[application/octet-stream] v43-0006-Row-pattern-recognition-patch-docs.patch (16.0K, 7-v43-0006-Row-pattern-recognition-patch-docs.patch)
download | inline diff:
From 160b6367df968c3247ad8a35a26175b01acd8a9d Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sun, 15 Feb 2026 17:47:49 +0900
Subject: [PATCH v43 6/8] Row pattern recognition patch (docs).
---
doc/src/sgml/advanced.sgml | 143 ++++++++++++++++++++++++++++-
doc/src/sgml/func/func-window.sgml | 53 +++++++++++
doc/src/sgml/ref/select.sgml | 88 +++++++++++++++++-
3 files changed, 277 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 451bcb202ec..a76fb263a94 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -540,13 +540,148 @@ WHERE pos < 3;
two rows for each department).
</para>
+ <para>
+ Row pattern common syntax can be used to perform row pattern recognition
+ in a query. The row pattern common syntax includes two sub
+ clauses: <literal>DEFINE</literal>
+ and <literal>PATTERN</literal>. <literal>DEFINE</literal> defines
+ definition variables along with an expression. The expression must be a
+ logical expression, which means it must
+ return <literal>TRUE</literal>, <literal>FALSE</literal>
+ or <literal>NULL</literal>. The expression may comprise column references
+ and functions. Window functions, aggregate functions and subqueries are
+ not allowed. An example of <literal>DEFINE</literal> is as follows.
+
+<programlisting>
+DEFINE
+ LOWPRICE AS price <= 100,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+</programlisting>
+
+ Note that <function>PREV</function> returns the <literal>price</literal>
+ column in the previous row if it's called in a context of row pattern
+ recognition. Thus in the second line the definition variable "UP"
+ is <literal>TRUE</literal> when the price column in the current row is
+ greater than the price column in the previous row. Likewise, "DOWN"
+ is <literal>TRUE</literal> when the
+ <literal>price</literal> column in the current row is lower than
+ the <literal>price</literal> column in the previous row.
+ </para>
+ <para>
+ Once <literal>DEFINE</literal> exists, <literal>PATTERN</literal> can be
+ used. <literal>PATTERN</literal> defines a sequence of rows that satisfies
+ conditions defined in the <literal>DEFINE</literal> clause. For example
+ following <literal>PATTERN</literal> defines a sequence of rows starting
+ with the a row satisfying "LOWPRICE", then one or more rows satisfying
+ "UP" and finally one or more rows satisfying "DOWN". Pattern variables
+ can be followed by quantifiers: "+" means one or more matches,
+ "*" means zero or more matches, "?" means zero or one match (optional),
+ "{n}" means exactly n matches, "{n,}" means at least n matches,
+ "{,m}" means at most m matches, and "{n,m}" means between n and m matches.
+ Patterns can be grouped using parentheses and combined using alternation
+ (the vertical bar "|" for OR). For example, "(UP DOWN)+" matches one or
+ more repetitions of UP followed by DOWN.
+ If a sequence of rows which satisfies the PATTERN is found, in
+ the starting row all columns or functions are shown in the target
+ list. Note that aggregations only look into the matched rows, rather than
+ the whole frame. On the second or subsequent rows all window functions are
+ shown as NULL. Aggregates are NULL or 0 depending on its aggregation
+ definition. A count() aggregate shows 0. For rows that do not match on the
+ PATTERN, columns are shown AS NULL too. Example of
+ a <literal>SELECT</literal> using the <literal>DEFINE</literal>
+ and <literal>PATTERN</literal> clause is as follows.
+
+<programlisting>
+SELECT company, tdate, price,
+ first_value(price) OVER w,
+ max(price) OVER w,
+ count(price) OVER w
+FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (LOWPRICE UP+ DOWN+)
+ DEFINE
+ LOWPRICE AS price <= 100,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+</programlisting>
+<screen>
+ company | tdate | price | first_value | max | count
+----------+------------+-------+-------------+-----+-------
+ company1 | 2023-07-01 | 100 | 100 | 200 | 4
+ company1 | 2023-07-02 | 200 | | | 0
+ company1 | 2023-07-03 | 150 | | | 0
+ company1 | 2023-07-04 | 140 | | | 0
+ company1 | 2023-07-05 | 150 | | | 0
+ company1 | 2023-07-06 | 90 | 90 | 130 | 4
+ company1 | 2023-07-07 | 110 | | | 0
+ company1 | 2023-07-08 | 130 | | | 0
+ company1 | 2023-07-09 | 120 | | | 0
+ company1 | 2023-07-10 | 130 | | | 0
+(10 rows)
+</screen>
+ </para>
+
+ <para>
+ Row pattern recognition internally uses a nondeterministic finite
+ automaton (NFA) to match patterns. For patterns with unbounded
+ quantifiers (e.g., <literal>A+</literal> or <literal>(A B)+</literal>),
+ the NFA may need to track many active matching contexts simultaneously,
+ which could potentially lead to O(n<superscript>2</superscript>)
+ complexity as the number of rows increases.
+ </para>
+
+ <para>
+ Before execution, <productname>PostgreSQL</productname> automatically
+ optimizes patterns to simplify their structure. This includes flattening
+ nested sequences and alternations, merging consecutive identical variables
+ (e.g., <literal>A{2,3} A{1,2}</literal> becomes <literal>A{3,5}</literal>),
+ removing duplicate alternatives
+ (e.g., <literal>(A | B | A)</literal> becomes <literal>(A | B)</literal>),
+ and simplifying nested quantifiers
+ (e.g., <literal>(A*)*</literal> becomes <literal>A*</literal>).
+ These optimizations reduce pattern complexity and also decrease
+ nesting depth, making the 253-level depth limit rarely encountered.
+ They are applied transparently and can be observed
+ in <command>EXPLAIN</command> output.
+ </para>
+
+ <para>
+ To mitigate this, <productname>PostgreSQL</productname> employs
+ a context absorption optimization. When a pattern starts with a greedy
+ unbounded element, newer matching contexts cannot produce longer matches
+ than older contexts. By detecting and eliminating these redundant
+ contexts, the matching complexity is reduced from
+ O(n<superscript>2</superscript>) to O(n) for many common patterns.
+ </para>
+
+ <para>
+ When examining query plans for row pattern recognition with
+ <command>EXPLAIN</command>, the pattern output may include special
+ markers that indicate optimization opportunities. A double quote
+ <literal>"</literal> marks where pattern absorption can occur,
+ and a single quote <literal>'</literal> marks absorbable elements
+ within a branch. For example, <literal>a+"</literal> indicates that
+ repeated matches of <literal>a</literal> can be absorbed, while
+ <literal>(a' b')+"</literal> shows that both <literal>a</literal>
+ and <literal>b</literal> within the group are absorbable.
+ These markers are primarily useful for understanding internal
+ optimization behavior.
+ </para>
+
<para>
When a query involves multiple window functions, it is possible to write
out each one with a separate <literal>OVER</literal> clause, but this is
- duplicative and error-prone if the same windowing behavior is wanted
- for several functions. Instead, each windowing behavior can be named
- in a <literal>WINDOW</literal> clause and then referenced in <literal>OVER</literal>.
- For example:
+ duplicative and error-prone if the same windowing behavior is wanted for
+ several functions. Instead, each windowing behavior can be named in
+ a <literal>WINDOW</literal> clause and then referenced
+ in <literal>OVER</literal>. For example:
<programlisting>
SELECT sum(salary) OVER w, avg(salary) OVER w
diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml
index bcf755c9ebc..ae36e0f3135 100644
--- a/doc/src/sgml/func/func-window.sgml
+++ b/doc/src/sgml/func/func-window.sgml
@@ -278,6 +278,59 @@
<function>nth_value</function>.
</para>
+ <para>
+ Row pattern recognition navigation functions are listed in
+ <xref linkend="functions-rpr-navigation-table"/>. These functions
+ can be used to describe DEFINE clause of Row pattern recognition.
+ </para>
+
+ <table id="functions-rpr-navigation-table">
+ <title>Row Pattern Navigation Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>prev</primary>
+ </indexterm>
+ <function>prev</function> ( <parameter>value</parameter> <type>anyelement</type> )
+ <returnvalue>anyelement</returnvalue>
+ </para>
+ <para>
+ Returns the column value at the previous row;
+ returns NULL if there is no previous row in the window frame.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>next</primary>
+ </indexterm>
+ <function>next</function> ( <parameter>value</parameter> <type>anyelement</type> )
+ <returnvalue>anyelement</returnvalue>
+ </para>
+ <para>
+ Returns the column value at the next row;
+ returns NULL if there is no next row in the window frame.
+ </para></entry>
+ </row>
+
+ </tbody>
+ </tgroup>
+ </table>
+
<note>
<para>
The SQL standard defines a <literal>FROM FIRST</literal> or <literal>FROM LAST</literal>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index ca5dd14d627..f0676bf6f2c 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -979,8 +979,8 @@ WINDOW <replaceable class="parameter">window_name</replaceable> AS ( <replaceabl
The <replaceable class="parameter">frame_clause</replaceable> can be one of
<synopsis>
-{ RANGE | ROWS | GROUPS } <replaceable>frame_start</replaceable> [ <replaceable>frame_exclusion</replaceable> ]
-{ RANGE | ROWS | GROUPS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable>frame_exclusion</replaceable> ]
+{ RANGE | ROWS | GROUPS } <replaceable>frame_start</replaceable> [ <replaceable>frame_exclusion</replaceable> ] [ <replaceable>row_pattern_common_syntax</replaceable> ]
+{ RANGE | ROWS | GROUPS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable>frame_exclusion</replaceable> ] [ <replaceable>row_pattern_common_syntax</replaceable> ]
</synopsis>
where <replaceable>frame_start</replaceable>
@@ -1087,9 +1087,91 @@ EXCLUDE NO OTHERS
a given peer group will be in the frame or excluded from it.
</para>
+ <para>
+ The
+ optional <replaceable class="parameter">row_pattern_common_syntax</replaceable>
+ defines the <firstterm>row pattern recognition condition</firstterm> for
+ this
+ window. <replaceable class="parameter">row_pattern_common_syntax</replaceable>
+ includes following subclauses.
+
+<synopsis>
+[ { AFTER MATCH SKIP PAST LAST ROW | AFTER MATCH SKIP TO NEXT ROW } ]
+[ INITIAL | SEEK ]
+PATTERN ( <replaceable class="parameter">pattern_variable_name</replaceable> [ <replaceable>quantifier</replaceable> ] [, ...] )
+DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS <replaceable class="parameter">expression</replaceable> [, ...]
+</synopsis>
+ <literal>AFTER MATCH SKIP PAST LAST ROW</literal> or <literal>AFTER MATCH
+ SKIP TO NEXT ROW</literal> controls how to proceed to next row position
+ after a match found. With <literal>AFTER MATCH SKIP PAST LAST
+ ROW</literal> (the default) next row position is next to the last row of
+ previous match. On the other hand, with <literal>AFTER MATCH SKIP TO NEXT
+ ROW</literal> next row position is next to the first row of previous
+ match. <literal>INITIAL</literal> or <literal>SEEK</literal> defines how a
+ successful pattern matching starts from which row in a
+ frame. If <literal>INITIAL</literal> is specified, the match must start
+ from the first row in the frame. If <literal>SEEK</literal> is specified,
+ the set of matching rows do not necessarily start from the first row. The
+ default is <literal>INITIAL</literal>. Currently
+ only <literal>INITIAL</literal> is supported. <literal>DEFINE</literal>
+ defines definition variables along with a boolean
+ expression. <literal>PATTERN</literal> defines a sequence of rows that
+ satisfies certain conditions using variables defined
+ in <literal>DEFINE</literal> clause. Each pattern variable can be
+ followed by a quantifier to specify how many times it should match:
+ <literal>*</literal> (zero or more),
+ <literal>+</literal> (one or more),
+ <literal>?</literal> (zero or one),
+ <literal>{</literal><replaceable>n</replaceable><literal>}</literal> (exactly <replaceable>n</replaceable> times),
+ <literal>{</literal><replaceable>n</replaceable><literal>,}</literal> (at least <replaceable>n</replaceable> times),
+ <literal>{,</literal><replaceable>m</replaceable><literal>}</literal> (at most <replaceable>m</replaceable> times), or
+ <literal>{</literal><replaceable>n</replaceable><literal>,</literal><replaceable>m</replaceable><literal>}</literal>
+ (between <replaceable>n</replaceable> and <replaceable>m</replaceable> times).
+ Reluctant quantifiers (e.g., <literal>*?</literal>, <literal>+?</literal>,
+ <literal>??</literal>, <literal>{</literal><replaceable>n</replaceable><literal>,</literal><replaceable>m</replaceable><literal>}?</literal>)
+ are not supported.
+ Patterns can be grouped using parentheses, and alternation (OR) can be
+ expressed using the vertical bar <literal>|</literal>.
+ For example, <literal>(A B)+</literal> matches one or more repetitions
+ of the sequence A followed by B, and <literal>A | B</literal> matches
+ either A or B.
+ If a pattern variable is not defined in
+ the <literal>DEFINE</literal> clause, it is not automatically added
+ to the <literal>DEFINE</literal> clause. Instead, the executor evaluates
+ the variable as <literal>TRUE</literal> at execution time, behaving as if
+ the following definition existed.
+
+<synopsis>
+<literal>variable_name</literal> AS TRUE
+</synopsis>
+
+ Conversely, variables defined in the <literal>DEFINE</literal> clause
+ but not used in the <literal>PATTERN</literal> clause are filtered out
+ during query planning.
+ </para>
+
+ <para>
+ Note that the maximum number of unique pattern variables
+ used in <literal>PATTERN</literal> clause is 251.
+ If this limit is exceeded, an error will be raised.
+ Additionally, the maximum nesting depth of pattern groups
+ (parentheses) is 253 levels.
+ However, pattern optimizations such as flattening nested sequences
+ and simplifying nested quantifiers may reduce the effective depth,
+ so this limit is rarely reached in practice.
+ </para>
+
+ <para>
+ The SQL standard defines more subclauses: <literal>MEASURES</literal>
+ and <literal>SUBSET</literal>. They are not currently supported
+ in <productname>PostgreSQL</productname>. Also in the standard there are
+ more variations in <literal>AFTER MATCH</literal> clause.
+ </para>
+
<para>
The purpose of a <literal>WINDOW</literal> clause is to specify the
- behavior of <firstterm>window functions</firstterm> appearing in the query's
+ behavior of <firstterm>window functions</firstterm> appearing in the
+ query's
<link linkend="sql-select-list"><command>SELECT</command> list</link> or
<link linkend="sql-orderby"><literal>ORDER BY</literal></link> clause.
These functions
--
2.43.0
[application/octet-stream] v43-0007-Row-pattern-recognition-patch-tests.patch (868.1K, 8-v43-0007-Row-pattern-recognition-patch-tests.patch)
download | inline diff:
From 68c747abd95561da1c48b4d492d599df89757821 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sun, 15 Feb 2026 17:47:49 +0900
Subject: [PATCH v43 7/8] Row pattern recognition patch (tests).
---
src/test/regress/expected/rpr.out | 4006 +++++++++++++++
src/test/regress/expected/rpr_base.out | 5538 +++++++++++++++++++++
src/test/regress/expected/rpr_explain.out | 3863 ++++++++++++++
src/test/regress/expected/rpr_nfa.out | 2524 ++++++++++
src/test/regress/parallel_schedule | 5 +
src/test/regress/sql/rpr.sql | 2180 ++++++++
src/test/regress/sql/rpr_base.sql | 3658 ++++++++++++++
src/test/regress/sql/rpr_explain.sql | 2254 +++++++++
src/test/regress/sql/rpr_nfa.sql | 1865 +++++++
9 files changed, 25893 insertions(+)
create mode 100644 src/test/regress/expected/rpr.out
create mode 100644 src/test/regress/expected/rpr_base.out
create mode 100644 src/test/regress/expected/rpr_explain.out
create mode 100644 src/test/regress/expected/rpr_nfa.out
create mode 100644 src/test/regress/sql/rpr.sql
create mode 100644 src/test/regress/sql/rpr_base.sql
create mode 100644 src/test/regress/sql/rpr_explain.sql
create mode 100644 src/test/regress/sql/rpr_nfa.sql
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
new file mode 100644
index 00000000000..c921badb006
--- /dev/null
+++ b/src/test/regress/expected/rpr.out
@@ -0,0 +1,4006 @@
+--
+-- Test for row pattern definition clause
+--
+CREATE TEMP TABLE stock (
+ company TEXT,
+ tdate DATE,
+ price INTEGER
+);
+INSERT INTO stock VALUES ('company1', '2023-07-01', 100);
+INSERT INTO stock VALUES ('company1', '2023-07-02', 200);
+INSERT INTO stock VALUES ('company1', '2023-07-03', 150);
+INSERT INTO stock VALUES ('company1', '2023-07-04', 140);
+INSERT INTO stock VALUES ('company1', '2023-07-05', 150);
+INSERT INTO stock VALUES ('company1', '2023-07-06', 90);
+INSERT INTO stock VALUES ('company1', '2023-07-07', 110);
+INSERT INTO stock VALUES ('company1', '2023-07-08', 130);
+INSERT INTO stock VALUES ('company1', '2023-07-09', 120);
+INSERT INTO stock VALUES ('company1', '2023-07-10', 130);
+INSERT INTO stock VALUES ('company2', '2023-07-01', 50);
+INSERT INTO stock VALUES ('company2', '2023-07-02', 2000);
+INSERT INTO stock VALUES ('company2', '2023-07-03', 1500);
+INSERT INTO stock VALUES ('company2', '2023-07-04', 1400);
+INSERT INTO stock VALUES ('company2', '2023-07-05', 1500);
+INSERT INTO stock VALUES ('company2', '2023-07-06', 60);
+INSERT INTO stock VALUES ('company2', '2023-07-07', 1100);
+INSERT INTO stock VALUES ('company2', '2023-07-08', 1300);
+INSERT INTO stock VALUES ('company2', '2023-07-09', 1200);
+INSERT INTO stock VALUES ('company2', '2023-07-10', 1300);
+SELECT * FROM stock;
+ company | tdate | price
+----------+------------+-------
+ company1 | 07-01-2023 | 100
+ company1 | 07-02-2023 | 200
+ company1 | 07-03-2023 | 150
+ company1 | 07-04-2023 | 140
+ company1 | 07-05-2023 | 150
+ company1 | 07-06-2023 | 90
+ company1 | 07-07-2023 | 110
+ company1 | 07-08-2023 | 130
+ company1 | 07-09-2023 | 120
+ company1 | 07-10-2023 | 130
+ company2 | 07-01-2023 | 50
+ company2 | 07-02-2023 | 2000
+ company2 | 07-03-2023 | 1500
+ company2 | 07-04-2023 | 1400
+ company2 | 07-05-2023 | 1500
+ company2 | 07-06-2023 | 60
+ company2 | 07-07-2023 | 1100
+ company2 | 07-08-2023 | 1300
+ company2 | 07-09-2023 | 1200
+ company2 | 07-10-2023 | 1300
+(20 rows)
+
+-- basic test using PREV
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
+ nth_value(tdate, 2) OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value | nth_second
+----------+------------+-------+-------------+------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 140 | 07-02-2023
+ company1 | 07-02-2023 | 200 | | |
+ company1 | 07-03-2023 | 150 | | |
+ company1 | 07-04-2023 | 140 | | |
+ company1 | 07-05-2023 | 150 | | |
+ company1 | 07-06-2023 | 90 | 90 | 120 | 07-07-2023
+ company1 | 07-07-2023 | 110 | | |
+ company1 | 07-08-2023 | 130 | | |
+ company1 | 07-09-2023 | 120 | | |
+ company1 | 07-10-2023 | 130 | | |
+ company2 | 07-01-2023 | 50 | 50 | 1400 | 07-02-2023
+ company2 | 07-02-2023 | 2000 | | |
+ company2 | 07-03-2023 | 1500 | | |
+ company2 | 07-04-2023 | 1400 | | |
+ company2 | 07-05-2023 | 1500 | | |
+ company2 | 07-06-2023 | 60 | 60 | 1200 | 07-07-2023
+ company2 | 07-07-2023 | 1100 | | |
+ company2 | 07-08-2023 | 1300 | | |
+ company2 | 07-09-2023 | 1200 | | |
+ company2 | 07-10-2023 | 1300 | | |
+(20 rows)
+
+-- basic test using PREV. UP appears twice
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
+ nth_value(tdate, 2) OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+ UP+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value | nth_second
+----------+------------+-------+-------------+------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 150 | 07-02-2023
+ company1 | 07-02-2023 | 200 | | |
+ company1 | 07-03-2023 | 150 | | |
+ company1 | 07-04-2023 | 140 | | |
+ company1 | 07-05-2023 | 150 | | |
+ company1 | 07-06-2023 | 90 | 90 | 130 | 07-07-2023
+ company1 | 07-07-2023 | 110 | | |
+ company1 | 07-08-2023 | 130 | | |
+ company1 | 07-09-2023 | 120 | | |
+ company1 | 07-10-2023 | 130 | | |
+ company2 | 07-01-2023 | 50 | 50 | 1500 | 07-02-2023
+ company2 | 07-02-2023 | 2000 | | |
+ company2 | 07-03-2023 | 1500 | | |
+ company2 | 07-04-2023 | 1400 | | |
+ company2 | 07-05-2023 | 1500 | | |
+ company2 | 07-06-2023 | 60 | 60 | 1300 | 07-07-2023
+ company2 | 07-07-2023 | 1100 | | |
+ company2 | 07-08-2023 | 1300 | | |
+ company2 | 07-09-2023 | 1200 | | |
+ company2 | 07-10-2023 | 1300 | | |
+(20 rows)
+
+-- basic test using PREV. Use '*'
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
+ nth_value(tdate, 2) OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP* DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value | nth_second
+----------+------------+-------+-------------+------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 140 | 07-02-2023
+ company1 | 07-02-2023 | 200 | | |
+ company1 | 07-03-2023 | 150 | | |
+ company1 | 07-04-2023 | 140 | | |
+ company1 | 07-05-2023 | 150 | 150 | 90 | 07-06-2023
+ company1 | 07-06-2023 | 90 | | |
+ company1 | 07-07-2023 | 110 | 110 | 120 | 07-08-2023
+ company1 | 07-08-2023 | 130 | | |
+ company1 | 07-09-2023 | 120 | | |
+ company1 | 07-10-2023 | 130 | | |
+ company2 | 07-01-2023 | 50 | 50 | 1400 | 07-02-2023
+ company2 | 07-02-2023 | 2000 | | |
+ company2 | 07-03-2023 | 1500 | | |
+ company2 | 07-04-2023 | 1400 | | |
+ company2 | 07-05-2023 | 1500 | 1500 | 60 | 07-06-2023
+ company2 | 07-06-2023 | 60 | | |
+ company2 | 07-07-2023 | 1100 | 1100 | 1200 | 07-08-2023
+ company2 | 07-08-2023 | 1300 | | |
+ company2 | 07-09-2023 | 1200 | | |
+ company2 | 07-10-2023 | 1300 | | |
+(20 rows)
+
+-- basic test using PREV. Use '?'
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
+ nth_value(tdate, 2) OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP? DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value | nth_second
+----------+------------+-------+-------------+------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 140 | 07-02-2023
+ company1 | 07-02-2023 | 200 | | |
+ company1 | 07-03-2023 | 150 | | |
+ company1 | 07-04-2023 | 140 | | |
+ company1 | 07-05-2023 | 150 | 150 | 90 | 07-06-2023
+ company1 | 07-06-2023 | 90 | | |
+ company1 | 07-07-2023 | 110 | 110 | 120 | 07-08-2023
+ company1 | 07-08-2023 | 130 | | |
+ company1 | 07-09-2023 | 120 | | |
+ company1 | 07-10-2023 | 130 | | |
+ company2 | 07-01-2023 | 50 | 50 | 1400 | 07-02-2023
+ company2 | 07-02-2023 | 2000 | | |
+ company2 | 07-03-2023 | 1500 | | |
+ company2 | 07-04-2023 | 1400 | | |
+ company2 | 07-05-2023 | 1500 | 1500 | 60 | 07-06-2023
+ company2 | 07-06-2023 | 60 | | |
+ company2 | 07-07-2023 | 1100 | 1100 | 1200 | 07-08-2023
+ company2 | 07-08-2023 | 1300 | | |
+ company2 | 07-09-2023 | 1200 | | |
+ company2 | 07-10-2023 | 1300 | | |
+(20 rows)
+
+-- test using alternation (|) with sequence
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START (UP | DOWN))
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 200
+ company1 | 07-02-2023 | 200 | |
+ company1 | 07-03-2023 | 150 | 150 | 140
+ company1 | 07-04-2023 | 140 | |
+ company1 | 07-05-2023 | 150 | 150 | 90
+ company1 | 07-06-2023 | 90 | |
+ company1 | 07-07-2023 | 110 | 110 | 130
+ company1 | 07-08-2023 | 130 | |
+ company1 | 07-09-2023 | 120 | 120 | 130
+ company1 | 07-10-2023 | 130 | |
+ company2 | 07-01-2023 | 50 | 50 | 2000
+ company2 | 07-02-2023 | 2000 | |
+ company2 | 07-03-2023 | 1500 | 1500 | 1400
+ company2 | 07-04-2023 | 1400 | |
+ company2 | 07-05-2023 | 1500 | 1500 | 60
+ company2 | 07-06-2023 | 60 | |
+ company2 | 07-07-2023 | 1100 | 1100 | 1300
+ company2 | 07-08-2023 | 1300 | |
+ company2 | 07-09-2023 | 1200 | 1200 | 1300
+ company2 | 07-10-2023 | 1300 | |
+(20 rows)
+
+-- test using alternation (|) with group quantifier
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START (UP | DOWN)+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 130
+ company1 | 07-02-2023 | 200 | |
+ company1 | 07-03-2023 | 150 | |
+ company1 | 07-04-2023 | 140 | |
+ company1 | 07-05-2023 | 150 | |
+ company1 | 07-06-2023 | 90 | |
+ company1 | 07-07-2023 | 110 | |
+ company1 | 07-08-2023 | 130 | |
+ company1 | 07-09-2023 | 120 | |
+ company1 | 07-10-2023 | 130 | |
+ company2 | 07-01-2023 | 50 | 50 | 1300
+ company2 | 07-02-2023 | 2000 | |
+ company2 | 07-03-2023 | 1500 | |
+ company2 | 07-04-2023 | 1400 | |
+ company2 | 07-05-2023 | 1500 | |
+ company2 | 07-06-2023 | 60 | |
+ company2 | 07-07-2023 | 1100 | |
+ company2 | 07-08-2023 | 1300 | |
+ company2 | 07-09-2023 | 1200 | |
+ company2 | 07-10-2023 | 1300 | |
+(20 rows)
+
+-- test using nested alternation
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START ((UP DOWN) | FLAT)+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price),
+ FLAT AS price = PREV(price)
+);
+ company | tdate | price | first_value | last_value
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 150
+ company1 | 07-02-2023 | 200 | |
+ company1 | 07-03-2023 | 150 | |
+ company1 | 07-04-2023 | 140 | 140 | 90
+ company1 | 07-05-2023 | 150 | |
+ company1 | 07-06-2023 | 90 | |
+ company1 | 07-07-2023 | 110 | 110 | 120
+ company1 | 07-08-2023 | 130 | |
+ company1 | 07-09-2023 | 120 | |
+ company1 | 07-10-2023 | 130 | |
+ company2 | 07-01-2023 | 50 | 50 | 1500
+ company2 | 07-02-2023 | 2000 | |
+ company2 | 07-03-2023 | 1500 | |
+ company2 | 07-04-2023 | 1400 | 1400 | 60
+ company2 | 07-05-2023 | 1500 | |
+ company2 | 07-06-2023 | 60 | |
+ company2 | 07-07-2023 | 1100 | 1100 | 1200
+ company2 | 07-08-2023 | 1300 | |
+ company2 | 07-09-2023 | 1200 | |
+ company2 | 07-10-2023 | 1300 | |
+(20 rows)
+
+-- test using group with quantifier
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((UP DOWN)+)
+ DEFINE
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 | 100 | |
+ company1 | 07-02-2023 | 200 | 200 | 150
+ company1 | 07-03-2023 | 150 | |
+ company1 | 07-04-2023 | 140 | |
+ company1 | 07-05-2023 | 150 | 150 | 90
+ company1 | 07-06-2023 | 90 | |
+ company1 | 07-07-2023 | 110 | |
+ company1 | 07-08-2023 | 130 | 130 | 120
+ company1 | 07-09-2023 | 120 | |
+ company1 | 07-10-2023 | 130 | |
+ company2 | 07-01-2023 | 50 | |
+ company2 | 07-02-2023 | 2000 | 2000 | 1500
+ company2 | 07-03-2023 | 1500 | |
+ company2 | 07-04-2023 | 1400 | |
+ company2 | 07-05-2023 | 1500 | 1500 | 60
+ company2 | 07-06-2023 | 60 | |
+ company2 | 07-07-2023 | 1100 | |
+ company2 | 07-08-2023 | 1300 | 1300 | 1200
+ company2 | 07-09-2023 | 1200 | |
+ company2 | 07-10-2023 | 1300 | |
+(20 rows)
+
+-- test using absolute threshold values (not relative PREV)
+-- HIGH: price > 150, LOW: price < 100, MID: neutral range
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOW MID* HIGH)
+ DEFINE
+ LOW AS price < 100,
+ MID AS price >= 100 AND price <= 150,
+ HIGH AS price > 150
+);
+ company | tdate | price | first_value | last_value
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 | 100 | |
+ company1 | 07-02-2023 | 200 | |
+ company1 | 07-03-2023 | 150 | |
+ company1 | 07-04-2023 | 140 | |
+ company1 | 07-05-2023 | 150 | |
+ company1 | 07-06-2023 | 90 | |
+ company1 | 07-07-2023 | 110 | |
+ company1 | 07-08-2023 | 130 | |
+ company1 | 07-09-2023 | 120 | |
+ company1 | 07-10-2023 | 130 | |
+ company2 | 07-01-2023 | 50 | 50 | 2000
+ company2 | 07-02-2023 | 2000 | |
+ company2 | 07-03-2023 | 1500 | |
+ company2 | 07-04-2023 | 1400 | |
+ company2 | 07-05-2023 | 1500 | |
+ company2 | 07-06-2023 | 60 | 60 | 1100
+ company2 | 07-07-2023 | 1100 | |
+ company2 | 07-08-2023 | 1300 | |
+ company2 | 07-09-2023 | 1200 | |
+ company2 | 07-10-2023 | 1300 | |
+(20 rows)
+
+-- test threshold-based pattern with alternation
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOW (MID | HIGH)+)
+ DEFINE
+ LOW AS price < 100,
+ MID AS price >= 100 AND price <= 150,
+ HIGH AS price > 150
+);
+ company | tdate | price | first_value | last_value
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 | 100 | |
+ company1 | 07-02-2023 | 200 | |
+ company1 | 07-03-2023 | 150 | |
+ company1 | 07-04-2023 | 140 | |
+ company1 | 07-05-2023 | 150 | |
+ company1 | 07-06-2023 | 90 | 90 | 130
+ company1 | 07-07-2023 | 110 | |
+ company1 | 07-08-2023 | 130 | |
+ company1 | 07-09-2023 | 120 | |
+ company1 | 07-10-2023 | 130 | |
+ company2 | 07-01-2023 | 50 | 50 | 1500
+ company2 | 07-02-2023 | 2000 | |
+ company2 | 07-03-2023 | 1500 | |
+ company2 | 07-04-2023 | 1400 | |
+ company2 | 07-05-2023 | 1500 | |
+ company2 | 07-06-2023 | 60 | 60 | 1300
+ company2 | 07-07-2023 | 1100 | |
+ company2 | 07-08-2023 | 1300 | |
+ company2 | 07-09-2023 | 1200 | |
+ company2 | 07-10-2023 | 1300 | |
+(20 rows)
+
+-- basic test with none-greedy pattern
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A A A)
+ DEFINE
+ A AS price >= 140 AND price <= 150
+);
+ company | tdate | price | count
+----------+------------+-------+-------
+ company1 | 07-01-2023 | 100 | 0
+ company1 | 07-02-2023 | 200 | 0
+ company1 | 07-03-2023 | 150 | 3
+ company1 | 07-04-2023 | 140 | 0
+ company1 | 07-05-2023 | 150 | 0
+ company1 | 07-06-2023 | 90 | 0
+ company1 | 07-07-2023 | 110 | 0
+ company1 | 07-08-2023 | 130 | 0
+ company1 | 07-09-2023 | 120 | 0
+ company1 | 07-10-2023 | 130 | 0
+ company2 | 07-01-2023 | 50 | 0
+ company2 | 07-02-2023 | 2000 | 0
+ company2 | 07-03-2023 | 1500 | 0
+ company2 | 07-04-2023 | 1400 | 0
+ company2 | 07-05-2023 | 1500 | 0
+ company2 | 07-06-2023 | 60 | 0
+ company2 | 07-07-2023 | 1100 | 0
+ company2 | 07-08-2023 | 1300 | 0
+ company2 | 07-09-2023 | 1200 | 0
+ company2 | 07-10-2023 | 1300 | 0
+(20 rows)
+
+-- test using {n} quantifier (A A A should be optimized to A{3})
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A{3})
+ DEFINE
+ A AS price >= 140 AND price <= 150
+);
+ company | tdate | price | count
+----------+------------+-------+-------
+ company1 | 07-01-2023 | 100 | 0
+ company1 | 07-02-2023 | 200 | 0
+ company1 | 07-03-2023 | 150 | 3
+ company1 | 07-04-2023 | 140 | 0
+ company1 | 07-05-2023 | 150 | 0
+ company1 | 07-06-2023 | 90 | 0
+ company1 | 07-07-2023 | 110 | 0
+ company1 | 07-08-2023 | 130 | 0
+ company1 | 07-09-2023 | 120 | 0
+ company1 | 07-10-2023 | 130 | 0
+ company2 | 07-01-2023 | 50 | 0
+ company2 | 07-02-2023 | 2000 | 0
+ company2 | 07-03-2023 | 1500 | 0
+ company2 | 07-04-2023 | 1400 | 0
+ company2 | 07-05-2023 | 1500 | 0
+ company2 | 07-06-2023 | 60 | 0
+ company2 | 07-07-2023 | 1100 | 0
+ company2 | 07-08-2023 | 1300 | 0
+ company2 | 07-09-2023 | 1200 | 0
+ company2 | 07-10-2023 | 1300 | 0
+(20 rows)
+
+-- test using {n,} quantifier (2 or more)
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A{2,})
+ DEFINE
+ A AS price > 100
+);
+ company | tdate | price | count
+----------+------------+-------+-------
+ company1 | 07-01-2023 | 100 | 0
+ company1 | 07-02-2023 | 200 | 4
+ company1 | 07-03-2023 | 150 | 0
+ company1 | 07-04-2023 | 140 | 0
+ company1 | 07-05-2023 | 150 | 0
+ company1 | 07-06-2023 | 90 | 0
+ company1 | 07-07-2023 | 110 | 4
+ company1 | 07-08-2023 | 130 | 0
+ company1 | 07-09-2023 | 120 | 0
+ company1 | 07-10-2023 | 130 | 0
+ company2 | 07-01-2023 | 50 | 0
+ company2 | 07-02-2023 | 2000 | 4
+ company2 | 07-03-2023 | 1500 | 0
+ company2 | 07-04-2023 | 1400 | 0
+ company2 | 07-05-2023 | 1500 | 0
+ company2 | 07-06-2023 | 60 | 0
+ company2 | 07-07-2023 | 1100 | 4
+ company2 | 07-08-2023 | 1300 | 0
+ company2 | 07-09-2023 | 1200 | 0
+ company2 | 07-10-2023 | 1300 | 0
+(20 rows)
+
+-- test using {n,m} quantifier (2 to 4)
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A{2,4})
+ DEFINE
+ A AS price > 100
+);
+ company | tdate | price | count
+----------+------------+-------+-------
+ company1 | 07-01-2023 | 100 | 0
+ company1 | 07-02-2023 | 200 | 4
+ company1 | 07-03-2023 | 150 | 0
+ company1 | 07-04-2023 | 140 | 0
+ company1 | 07-05-2023 | 150 | 0
+ company1 | 07-06-2023 | 90 | 0
+ company1 | 07-07-2023 | 110 | 4
+ company1 | 07-08-2023 | 130 | 0
+ company1 | 07-09-2023 | 120 | 0
+ company1 | 07-10-2023 | 130 | 0
+ company2 | 07-01-2023 | 50 | 0
+ company2 | 07-02-2023 | 2000 | 4
+ company2 | 07-03-2023 | 1500 | 0
+ company2 | 07-04-2023 | 1400 | 0
+ company2 | 07-05-2023 | 1500 | 0
+ company2 | 07-06-2023 | 60 | 0
+ company2 | 07-07-2023 | 1100 | 4
+ company2 | 07-08-2023 | 1300 | 0
+ company2 | 07-09-2023 | 1200 | 0
+ company2 | 07-10-2023 | 1300 | 0
+(20 rows)
+
+-- last_value() should remain consistent
+SELECT company, tdate, price, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | last_value
+----------+------------+-------+------------
+ company1 | 07-01-2023 | 100 | 140
+ company1 | 07-02-2023 | 200 |
+ company1 | 07-03-2023 | 150 |
+ company1 | 07-04-2023 | 140 |
+ company1 | 07-05-2023 | 150 |
+ company1 | 07-06-2023 | 90 | 120
+ company1 | 07-07-2023 | 110 |
+ company1 | 07-08-2023 | 130 |
+ company1 | 07-09-2023 | 120 |
+ company1 | 07-10-2023 | 130 |
+ company2 | 07-01-2023 | 50 | 1400
+ company2 | 07-02-2023 | 2000 |
+ company2 | 07-03-2023 | 1500 |
+ company2 | 07-04-2023 | 1400 |
+ company2 | 07-05-2023 | 1500 |
+ company2 | 07-06-2023 | 60 | 1200
+ company2 | 07-07-2023 | 1100 |
+ company2 | 07-08-2023 | 1300 |
+ company2 | 07-09-2023 | 1200 |
+ company2 | 07-10-2023 | 1300 |
+(20 rows)
+
+-- omit "START" in DEFINE but it is ok because "START AS TRUE" is
+-- implicitly defined. per spec.
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
+ nth_value(tdate, 2) OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value | nth_second
+----------+------------+-------+-------------+------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 140 | 07-02-2023
+ company1 | 07-02-2023 | 200 | | |
+ company1 | 07-03-2023 | 150 | | |
+ company1 | 07-04-2023 | 140 | | |
+ company1 | 07-05-2023 | 150 | | |
+ company1 | 07-06-2023 | 90 | 90 | 120 | 07-07-2023
+ company1 | 07-07-2023 | 110 | | |
+ company1 | 07-08-2023 | 130 | | |
+ company1 | 07-09-2023 | 120 | | |
+ company1 | 07-10-2023 | 130 | | |
+ company2 | 07-01-2023 | 50 | 50 | 1400 | 07-02-2023
+ company2 | 07-02-2023 | 2000 | | |
+ company2 | 07-03-2023 | 1500 | | |
+ company2 | 07-04-2023 | 1400 | | |
+ company2 | 07-05-2023 | 1500 | | |
+ company2 | 07-06-2023 | 60 | 60 | 1200 | 07-07-2023
+ company2 | 07-07-2023 | 1100 | | |
+ company2 | 07-08-2023 | 1300 | | |
+ company2 | 07-09-2023 | 1200 | | |
+ company2 | 07-10-2023 | 1300 | | |
+(20 rows)
+
+-- the first row start with less than or equal to 100
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOWPRICE UP+ DOWN+)
+ DEFINE
+ LOWPRICE AS price <= 100,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 140
+ company1 | 07-02-2023 | 200 | |
+ company1 | 07-03-2023 | 150 | |
+ company1 | 07-04-2023 | 140 | |
+ company1 | 07-05-2023 | 150 | |
+ company1 | 07-06-2023 | 90 | 90 | 120
+ company1 | 07-07-2023 | 110 | |
+ company1 | 07-08-2023 | 130 | |
+ company1 | 07-09-2023 | 120 | |
+ company1 | 07-10-2023 | 130 | |
+ company2 | 07-01-2023 | 50 | 50 | 1400
+ company2 | 07-02-2023 | 2000 | |
+ company2 | 07-03-2023 | 1500 | |
+ company2 | 07-04-2023 | 1400 | |
+ company2 | 07-05-2023 | 1500 | |
+ company2 | 07-06-2023 | 60 | 60 | 1200
+ company2 | 07-07-2023 | 1100 | |
+ company2 | 07-08-2023 | 1300 | |
+ company2 | 07-09-2023 | 1200 | |
+ company2 | 07-10-2023 | 1300 | |
+(20 rows)
+
+-- second row raises 120%
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOWPRICE UP+ DOWN+)
+ DEFINE
+ LOWPRICE AS price <= 100,
+ UP AS price > PREV(price) * 1.2,
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 140
+ company1 | 07-02-2023 | 200 | |
+ company1 | 07-03-2023 | 150 | |
+ company1 | 07-04-2023 | 140 | |
+ company1 | 07-05-2023 | 150 | |
+ company1 | 07-06-2023 | 90 | |
+ company1 | 07-07-2023 | 110 | |
+ company1 | 07-08-2023 | 130 | |
+ company1 | 07-09-2023 | 120 | |
+ company1 | 07-10-2023 | 130 | |
+ company2 | 07-01-2023 | 50 | 50 | 1400
+ company2 | 07-02-2023 | 2000 | |
+ company2 | 07-03-2023 | 1500 | |
+ company2 | 07-04-2023 | 1400 | |
+ company2 | 07-05-2023 | 1500 | |
+ company2 | 07-06-2023 | 60 | |
+ company2 | 07-07-2023 | 1100 | |
+ company2 | 07-08-2023 | 1300 | |
+ company2 | 07-09-2023 | 1200 | |
+ company2 | 07-10-2023 | 1300 | |
+(20 rows)
+
+-- using NEXT
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UPDOWN)
+ DEFINE
+ START AS TRUE,
+ UPDOWN AS price > PREV(price) AND price > NEXT(price)
+);
+ company | tdate | price | first_value | last_value
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 200
+ company1 | 07-02-2023 | 200 | |
+ company1 | 07-03-2023 | 150 | |
+ company1 | 07-04-2023 | 140 | 140 | 150
+ company1 | 07-05-2023 | 150 | |
+ company1 | 07-06-2023 | 90 | |
+ company1 | 07-07-2023 | 110 | 110 | 130
+ company1 | 07-08-2023 | 130 | |
+ company1 | 07-09-2023 | 120 | |
+ company1 | 07-10-2023 | 130 | |
+ company2 | 07-01-2023 | 50 | 50 | 2000
+ company2 | 07-02-2023 | 2000 | |
+ company2 | 07-03-2023 | 1500 | |
+ company2 | 07-04-2023 | 1400 | 1400 | 1500
+ company2 | 07-05-2023 | 1500 | |
+ company2 | 07-06-2023 | 60 | |
+ company2 | 07-07-2023 | 1100 | 1100 | 1300
+ company2 | 07-08-2023 | 1300 | |
+ company2 | 07-09-2023 | 1200 | |
+ company2 | 07-10-2023 | 1300 | |
+(20 rows)
+
+-- using AFTER MATCH SKIP TO NEXT ROW
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (START UPDOWN)
+ DEFINE
+ START AS TRUE,
+ UPDOWN AS price > PREV(price) AND price > NEXT(price)
+);
+ company | tdate | price | first_value | last_value
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 200
+ company1 | 07-02-2023 | 200 | |
+ company1 | 07-03-2023 | 150 | |
+ company1 | 07-04-2023 | 140 | 140 | 150
+ company1 | 07-05-2023 | 150 | |
+ company1 | 07-06-2023 | 90 | |
+ company1 | 07-07-2023 | 110 | 110 | 130
+ company1 | 07-08-2023 | 130 | |
+ company1 | 07-09-2023 | 120 | |
+ company1 | 07-10-2023 | 130 | |
+ company2 | 07-01-2023 | 50 | 50 | 2000
+ company2 | 07-02-2023 | 2000 | |
+ company2 | 07-03-2023 | 1500 | |
+ company2 | 07-04-2023 | 1400 | 1400 | 1500
+ company2 | 07-05-2023 | 1500 | |
+ company2 | 07-06-2023 | 60 | |
+ company2 | 07-07-2023 | 1100 | 1100 | 1300
+ company2 | 07-08-2023 | 1300 | |
+ company2 | 07-09-2023 | 1200 | |
+ company2 | 07-10-2023 | 1300 | |
+(20 rows)
+
+-- match everything
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (A+)
+ DEFINE
+ A AS TRUE
+);
+ company | tdate | price | first_value | last_value
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 130
+ company1 | 07-02-2023 | 200 | |
+ company1 | 07-03-2023 | 150 | |
+ company1 | 07-04-2023 | 140 | |
+ company1 | 07-05-2023 | 150 | |
+ company1 | 07-06-2023 | 90 | |
+ company1 | 07-07-2023 | 110 | |
+ company1 | 07-08-2023 | 130 | |
+ company1 | 07-09-2023 | 120 | |
+ company1 | 07-10-2023 | 130 | |
+ company2 | 07-01-2023 | 50 | 50 | 1300
+ company2 | 07-02-2023 | 2000 | |
+ company2 | 07-03-2023 | 1500 | |
+ company2 | 07-04-2023 | 1400 | |
+ company2 | 07-05-2023 | 1500 | |
+ company2 | 07-06-2023 | 60 | |
+ company2 | 07-07-2023 | 1100 | |
+ company2 | 07-08-2023 | 1300 | |
+ company2 | 07-09-2023 | 1200 | |
+ company2 | 07-10-2023 | 1300 | |
+(20 rows)
+
+-- nth_value beyond reduced frame (no IGNORE NULLS)
+-- Tests WinGetSlotInFrame/WinGetFuncArgInFrame out-of-frame with RPR
+SELECT company, tdate, price,
+ nth_value(price, 5) OVER w AS nth_5
+FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | nth_5
+----------+------------+-------+-------
+ company1 | 07-01-2023 | 100 |
+ company1 | 07-02-2023 | 200 |
+ company1 | 07-03-2023 | 150 |
+ company1 | 07-04-2023 | 140 |
+ company1 | 07-05-2023 | 150 |
+ company1 | 07-06-2023 | 90 |
+ company1 | 07-07-2023 | 110 |
+ company1 | 07-08-2023 | 130 |
+ company1 | 07-09-2023 | 120 |
+ company1 | 07-10-2023 | 130 |
+ company2 | 07-01-2023 | 50 |
+ company2 | 07-02-2023 | 2000 |
+ company2 | 07-03-2023 | 1500 |
+ company2 | 07-04-2023 | 1400 |
+ company2 | 07-05-2023 | 1500 |
+ company2 | 07-06-2023 | 60 |
+ company2 | 07-07-2023 | 1100 |
+ company2 | 07-08-2023 | 1300 |
+ company2 | 07-09-2023 | 1200 |
+ company2 | 07-10-2023 | 1300 |
+(20 rows)
+
+-- backtracking with reclassification of rows
+-- using AFTER MATCH SKIP PAST LAST ROW
+SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (A+ B+)
+ DEFINE
+ A AS price > 100,
+ B AS price > 100
+);
+ company | tdate | price | first_value | last_value
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 | 100 | |
+ company1 | 07-02-2023 | 200 | 07-02-2023 | 07-05-2023
+ company1 | 07-03-2023 | 150 | |
+ company1 | 07-04-2023 | 140 | |
+ company1 | 07-05-2023 | 150 | |
+ company1 | 07-06-2023 | 90 | |
+ company1 | 07-07-2023 | 110 | 07-07-2023 | 07-10-2023
+ company1 | 07-08-2023 | 130 | |
+ company1 | 07-09-2023 | 120 | |
+ company1 | 07-10-2023 | 130 | |
+ company2 | 07-01-2023 | 50 | |
+ company2 | 07-02-2023 | 2000 | 07-02-2023 | 07-05-2023
+ company2 | 07-03-2023 | 1500 | |
+ company2 | 07-04-2023 | 1400 | |
+ company2 | 07-05-2023 | 1500 | |
+ company2 | 07-06-2023 | 60 | |
+ company2 | 07-07-2023 | 1100 | 07-07-2023 | 07-10-2023
+ company2 | 07-08-2023 | 1300 | |
+ company2 | 07-09-2023 | 1200 | |
+ company2 | 07-10-2023 | 1300 | |
+(20 rows)
+
+-- backtracking with reclassification of rows
+-- using AFTER MATCH SKIP TO NEXT ROW
+SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (A+ B+)
+ DEFINE
+ A AS price > 100,
+ B AS price > 100
+);
+ company | tdate | price | first_value | last_value
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 | 100 | |
+ company1 | 07-02-2023 | 200 | 07-02-2023 | 07-05-2023
+ company1 | 07-03-2023 | 150 | 07-03-2023 | 07-05-2023
+ company1 | 07-04-2023 | 140 | 07-04-2023 | 07-05-2023
+ company1 | 07-05-2023 | 150 | |
+ company1 | 07-06-2023 | 90 | |
+ company1 | 07-07-2023 | 110 | 07-07-2023 | 07-10-2023
+ company1 | 07-08-2023 | 130 | 07-08-2023 | 07-10-2023
+ company1 | 07-09-2023 | 120 | 07-09-2023 | 07-10-2023
+ company1 | 07-10-2023 | 130 | |
+ company2 | 07-01-2023 | 50 | |
+ company2 | 07-02-2023 | 2000 | 07-02-2023 | 07-05-2023
+ company2 | 07-03-2023 | 1500 | 07-03-2023 | 07-05-2023
+ company2 | 07-04-2023 | 1400 | 07-04-2023 | 07-05-2023
+ company2 | 07-05-2023 | 1500 | |
+ company2 | 07-06-2023 | 60 | |
+ company2 | 07-07-2023 | 1100 | 07-07-2023 | 07-10-2023
+ company2 | 07-08-2023 | 1300 | 07-08-2023 | 07-10-2023
+ company2 | 07-09-2023 | 1200 | 07-09-2023 | 07-10-2023
+ company2 | 07-10-2023 | 1300 | |
+(20 rows)
+
+-- SKIP TO NEXT ROW with limited frame (Ishii-san's test case)
+-- Each row should produce its own match within its frame
+WITH data AS (
+ SELECT * FROM (VALUES
+ ('A', 1), ('A', 2),
+ ('B', 3), ('B', 4)
+ ) AS t(gid, id)
+)
+SELECT gid, id, array_agg(id) OVER w
+FROM data
+WINDOW w AS (
+ PARTITION BY gid
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE A AS id < 10
+);
+ gid | id | array_agg
+-----+----+-----------
+ A | 1 | {1,2}
+ A | 2 | {2}
+ B | 3 | {3,4}
+ B | 4 | {4}
+(4 rows)
+
+-- Limited frame with absorption test
+-- Row 0: frame [0,2], can't see B at row 3 -> no match
+-- Row 1: frame [1,3], can see A A B -> should match rows 1-3
+WITH frame_absorb_test AS (
+ SELECT * FROM (VALUES
+ (0, 'A'), (1, 'A'), (2, 'A'), (3, 'B')
+ ) AS t(id, flag)
+)
+SELECT id, flag, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM frame_absorb_test
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS flag = 'A',
+ B AS flag = 'B'
+);
+ id | flag | match_start | match_end
+----+------+-------------+-----------
+ 0 | A | |
+ 1 | A | 1 | 3
+ 2 | A | |
+ 3 | B | |
+(4 rows)
+
+-- ROWS BETWEEN CURRENT ROW AND offset FOLLOWING
+SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w,
+ count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value | count
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 | 100 | 07-01-2023 | 07-03-2023 | 3
+ company1 | 07-02-2023 | 200 | | | 0
+ company1 | 07-03-2023 | 150 | | | 0
+ company1 | 07-04-2023 | 140 | 07-04-2023 | 07-06-2023 | 3
+ company1 | 07-05-2023 | 150 | | | 0
+ company1 | 07-06-2023 | 90 | | | 0
+ company1 | 07-07-2023 | 110 | 07-07-2023 | 07-09-2023 | 3
+ company1 | 07-08-2023 | 130 | | | 0
+ company1 | 07-09-2023 | 120 | | | 0
+ company1 | 07-10-2023 | 130 | | | 0
+ company2 | 07-01-2023 | 50 | 07-01-2023 | 07-03-2023 | 3
+ company2 | 07-02-2023 | 2000 | | | 0
+ company2 | 07-03-2023 | 1500 | | | 0
+ company2 | 07-04-2023 | 1400 | 07-04-2023 | 07-06-2023 | 3
+ company2 | 07-05-2023 | 1500 | | | 0
+ company2 | 07-06-2023 | 60 | | | 0
+ company2 | 07-07-2023 | 1100 | 07-07-2023 | 07-09-2023 | 3
+ company2 | 07-08-2023 | 1300 | | | 0
+ company2 | 07-09-2023 | 1200 | | | 0
+ company2 | 07-10-2023 | 1300 | | | 0
+(20 rows)
+
+--
+-- Aggregates
+--
+-- using AFTER MATCH SKIP PAST LAST ROW
+SELECT company, tdate, price,
+ first_value(price) OVER w,
+ last_value(price) OVER w,
+ max(price) OVER w,
+ min(price) OVER w,
+ sum(price) OVER w,
+ avg(price) OVER w,
+ count(price) OVER w
+FROM stock
+WINDOW w AS (
+PARTITION BY company
+ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+AFTER MATCH SKIP PAST LAST ROW
+INITIAL
+PATTERN (START UP+ DOWN+)
+DEFINE
+START AS TRUE,
+UP AS price > PREV(price),
+DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value | max | min | sum | avg | count
+----------+------------+-------+-------------+------------+------+-----+------+-----------------------+-------
+ company1 | 07-01-2023 | 100 | 100 | 140 | 200 | 100 | 590 | 147.5000000000000000 | 4
+ company1 | 07-02-2023 | 200 | | | | | | | 0
+ company1 | 07-03-2023 | 150 | | | | | | | 0
+ company1 | 07-04-2023 | 140 | | | | | | | 0
+ company1 | 07-05-2023 | 150 | | | | | | | 0
+ company1 | 07-06-2023 | 90 | 90 | 120 | 130 | 90 | 450 | 112.5000000000000000 | 4
+ company1 | 07-07-2023 | 110 | | | | | | | 0
+ company1 | 07-08-2023 | 130 | | | | | | | 0
+ company1 | 07-09-2023 | 120 | | | | | | | 0
+ company1 | 07-10-2023 | 130 | | | | | | | 0
+ company2 | 07-01-2023 | 50 | 50 | 1400 | 2000 | 50 | 4950 | 1237.5000000000000000 | 4
+ company2 | 07-02-2023 | 2000 | | | | | | | 0
+ company2 | 07-03-2023 | 1500 | | | | | | | 0
+ company2 | 07-04-2023 | 1400 | | | | | | | 0
+ company2 | 07-05-2023 | 1500 | | | | | | | 0
+ company2 | 07-06-2023 | 60 | 60 | 1200 | 1300 | 60 | 3660 | 915.0000000000000000 | 4
+ company2 | 07-07-2023 | 1100 | | | | | | | 0
+ company2 | 07-08-2023 | 1300 | | | | | | | 0
+ company2 | 07-09-2023 | 1200 | | | | | | | 0
+ company2 | 07-10-2023 | 1300 | | | | | | | 0
+(20 rows)
+
+-- using AFTER MATCH SKIP TO NEXT ROW
+SELECT company, tdate, price,
+ first_value(price) OVER w,
+ last_value(price) OVER w,
+ max(price) OVER w,
+ min(price) OVER w,
+ sum(price) OVER w,
+ avg(price) OVER w,
+ count(price) OVER w
+FROM stock
+WINDOW w AS (
+PARTITION BY company
+ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+AFTER MATCH SKIP TO NEXT ROW
+INITIAL
+PATTERN (START UP+ DOWN+)
+DEFINE
+START AS TRUE,
+UP AS price > PREV(price),
+DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value | max | min | sum | avg | count
+----------+------------+-------+-------------+------------+------+------+------+-----------------------+-------
+ company1 | 07-01-2023 | 100 | 100 | 140 | 200 | 100 | 590 | 147.5000000000000000 | 4
+ company1 | 07-02-2023 | 200 | | | | | | | 0
+ company1 | 07-03-2023 | 150 | | | | | | | 0
+ company1 | 07-04-2023 | 140 | 140 | 90 | 150 | 90 | 380 | 126.6666666666666667 | 3
+ company1 | 07-05-2023 | 150 | | | | | | | 0
+ company1 | 07-06-2023 | 90 | 90 | 120 | 130 | 90 | 450 | 112.5000000000000000 | 4
+ company1 | 07-07-2023 | 110 | 110 | 120 | 130 | 110 | 360 | 120.0000000000000000 | 3
+ company1 | 07-08-2023 | 130 | | | | | | | 0
+ company1 | 07-09-2023 | 120 | | | | | | | 0
+ company1 | 07-10-2023 | 130 | | | | | | | 0
+ company2 | 07-01-2023 | 50 | 50 | 1400 | 2000 | 50 | 4950 | 1237.5000000000000000 | 4
+ company2 | 07-02-2023 | 2000 | | | | | | | 0
+ company2 | 07-03-2023 | 1500 | | | | | | | 0
+ company2 | 07-04-2023 | 1400 | 1400 | 60 | 1500 | 60 | 2960 | 986.6666666666666667 | 3
+ company2 | 07-05-2023 | 1500 | | | | | | | 0
+ company2 | 07-06-2023 | 60 | 60 | 1200 | 1300 | 60 | 3660 | 915.0000000000000000 | 4
+ company2 | 07-07-2023 | 1100 | 1100 | 1200 | 1300 | 1100 | 3600 | 1200.0000000000000000 | 3
+ company2 | 07-08-2023 | 1300 | | | | | | | 0
+ company2 | 07-09-2023 | 1200 | | | | | | | 0
+ company2 | 07-10-2023 | 1300 | | | | | | | 0
+(20 rows)
+
+-- JOIN case
+CREATE TEMP TABLE t1 (i int, v1 int);
+CREATE TEMP TABLE t2 (j int, v2 int);
+INSERT INTO t1 VALUES(1,10);
+INSERT INTO t1 VALUES(1,11);
+INSERT INTO t1 VALUES(1,12);
+INSERT INTO t2 VALUES(2,10);
+INSERT INTO t2 VALUES(2,11);
+INSERT INTO t2 VALUES(2,12);
+SELECT * FROM t1, t2 WHERE t1.v1 <= 11 AND t2.v2 <= 11;
+ i | v1 | j | v2
+---+----+---+----
+ 1 | 10 | 2 | 10
+ 1 | 10 | 2 | 11
+ 1 | 11 | 2 | 10
+ 1 | 11 | 2 | 11
+(4 rows)
+
+SELECT *, count(*) OVER w FROM t1, t2
+WINDOW w AS (
+ PARTITION BY t1.i
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE
+ A AS v1 <= 11 AND v2 <= 11
+);
+ i | v1 | j | v2 | count
+---+----+---+----+-------
+ 1 | 10 | 2 | 10 | 1
+ 1 | 10 | 2 | 11 | 1
+ 1 | 10 | 2 | 12 | 0
+ 1 | 11 | 2 | 10 | 1
+ 1 | 11 | 2 | 11 | 1
+ 1 | 11 | 2 | 12 | 0
+ 1 | 12 | 2 | 10 | 0
+ 1 | 12 | 2 | 11 | 0
+ 1 | 12 | 2 | 12 | 0
+(9 rows)
+
+-- WITH case
+WITH wstock AS (
+ SELECT * FROM stock WHERE tdate < '2023-07-08'
+)
+SELECT tdate, price,
+first_value(tdate) OVER w,
+count(*) OVER w
+ FROM wstock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ tdate | price | first_value | count
+------------+-------+-------------+-------
+ 07-01-2023 | 100 | 07-01-2023 | 4
+ 07-02-2023 | 200 | | 0
+ 07-03-2023 | 150 | | 0
+ 07-04-2023 | 140 | | 0
+ 07-05-2023 | 150 | | 0
+ 07-06-2023 | 90 | | 0
+ 07-07-2023 | 110 | | 0
+ 07-01-2023 | 50 | 07-01-2023 | 4
+ 07-02-2023 | 2000 | | 0
+ 07-03-2023 | 1500 | | 0
+ 07-04-2023 | 1400 | | 0
+ 07-05-2023 | 1500 | | 0
+ 07-06-2023 | 60 | | 0
+ 07-07-2023 | 1100 | | 0
+(14 rows)
+
+-- ReScan test: LATERAL join forces WindowAgg rescan with RPR
+-- Tests ExecReScanWindowAgg clearing prev_slot/next_slot
+SELECT g.x, sub.*
+FROM generate_series(1, 2) g(x),
+LATERAL (
+ SELECT id, price, count(*) OVER w AS c
+ FROM (VALUES (1, 100), (2, 200), (3, 150)) AS t(id, price)
+ WHERE id <= g.x + 1
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (START UP+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price)
+ )
+) sub
+ORDER BY g.x, sub.id;
+ x | id | price | c
+---+----+-------+---
+ 1 | 1 | 100 | 2
+ 1 | 2 | 200 | 0
+ 2 | 1 | 100 | 2
+ 2 | 2 | 200 | 0
+ 2 | 3 | 150 | 0
+(5 rows)
+
+-- PREV has multiple column reference
+CREATE TEMP TABLE rpr1 (id INTEGER, i SERIAL, j INTEGER);
+INSERT INTO rpr1(id, j) SELECT 1, g*2 FROM generate_series(1, 10) AS g;
+SELECT id, i, j, count(*) OVER w
+ FROM rpr1
+ WINDOW w AS (
+ PARTITION BY id
+ ORDER BY i
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (START COND+)
+ DEFINE
+ START AS TRUE,
+ COND AS PREV(i + j + 1) < 10
+);
+ id | i | j | count
+----+----+----+-------
+ 1 | 1 | 2 | 3
+ 1 | 2 | 4 | 0
+ 1 | 3 | 6 | 0
+ 1 | 4 | 8 | 0
+ 1 | 5 | 10 | 0
+ 1 | 6 | 12 | 0
+ 1 | 7 | 14 | 0
+ 1 | 8 | 16 | 0
+ 1 | 9 | 18 | 0
+ 1 | 10 | 20 | 0
+(10 rows)
+
+-- Smoke test for larger partitions.
+WITH s AS (
+ SELECT v, count(*) OVER w AS c
+ FROM (SELECT generate_series(1, 5000) v)
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ( r+ )
+ DEFINE r AS TRUE
+ )
+)
+-- Should be exactly one long match across all rows.
+SELECT * FROM s WHERE c > 0;
+ v | c
+---+------
+ 1 | 5000
+(1 row)
+
+WITH s AS (
+ SELECT v, count(*) OVER w AS c
+ FROM (SELECT generate_series(1, 5000) v)
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ( r )
+ DEFINE r AS TRUE
+ )
+)
+-- Every row should be its own match.
+SELECT count(*) FROM s WHERE c > 0;
+ count
+-------
+ 5000
+(1 row)
+
+-- Large partition test: 100K rows with A+ B* C{10000,} pattern
+-- Tests that int32 count doesn't overflow with large repetitions
+WITH data AS (
+ SELECT generate_series(0, 100000) AS v
+),
+result AS (
+ SELECT v,
+ count(*) OVER w AS match_len,
+ first_value(v) OVER w AS match_first,
+ last_value(v) OVER w AS match_last
+ FROM data
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (A+ B* C{10000,})
+ DEFINE
+ A AS v < 33333,
+ B AS v >= 33333 AND v < 66666,
+ C AS v >= 66666 AND v < 99999
+ )
+)
+-- Should match: A (33333 rows) + B (33333 rows) + C (33333 rows) = 99999 rows
+SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
+ match_first | match_last | match_len
+-------------+------------+-----------
+ 0 | 99998 | 99999
+(1 row)
+
+--
+-- Using IGNORE NULLS
+--
+-- no NULL rows case. The result should be identical with "basic test using PREV"
+SELECT company, tdate, price, first_value(price) IGNORE NULLS OVER w,
+ last_value(price) IGNORE NULLS OVER w,
+ nth_value(tdate, 2) IGNORE NULLS OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | first_value | last_value | nth_second
+----------+------------+-------+-------------+------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 140 | 07-02-2023
+ company1 | 07-02-2023 | 200 | | |
+ company1 | 07-03-2023 | 150 | | |
+ company1 | 07-04-2023 | 140 | | |
+ company1 | 07-05-2023 | 150 | | |
+ company1 | 07-06-2023 | 90 | 90 | 120 | 07-07-2023
+ company1 | 07-07-2023 | 110 | | |
+ company1 | 07-08-2023 | 130 | | |
+ company1 | 07-09-2023 | 120 | | |
+ company1 | 07-10-2023 | 130 | | |
+ company2 | 07-01-2023 | 50 | 50 | 1400 | 07-02-2023
+ company2 | 07-02-2023 | 2000 | | |
+ company2 | 07-03-2023 | 1500 | | |
+ company2 | 07-04-2023 | 1400 | | |
+ company2 | 07-05-2023 | 1500 | | |
+ company2 | 07-06-2023 | 60 | 60 | 1200 | 07-07-2023
+ company2 | 07-07-2023 | 1100 | | |
+ company2 | 07-08-2023 | 1300 | | |
+ company2 | 07-09-2023 | 1200 | | |
+ company2 | 07-10-2023 | 1300 | | |
+(20 rows)
+
+-- nth_value with IGNORE NULLS option wants to find the second row but
+-- due a NULL in the midlle, it returns the third row.
+WITH data AS (
+ SELECT * FROM (VALUES
+ (10, 1), (11, NULL), (12, 3), (13, 4)
+ ) AS t(gid, id))
+ SELECT gid, id, nth_value(id, 2) IGNORE NULLS OVER w AS second_val,
+ array_agg(id) OVER w
+ FROM data
+ WINDOW w AS (
+ ORDER BY gid
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS gid < 13
+ );
+ gid | id | second_val | array_agg
+-----+----+------------+------------
+ 10 | 1 | 3 | {1,NULL,3}
+ 11 | | |
+ 12 | 3 | |
+ 13 | 4 | |
+(4 rows)
+
+-- nth_value with IGNORE NULLS option wants to find the third row but
+-- due a NULL in the midlle, it reaches the end of reduced frame and
+-- return NULL
+WITH data AS (
+ SELECT * FROM (VALUES
+ (10, 1), (11, NULL), (12, 3), (13, 4)
+ ) AS t(gid, id))
+ SELECT gid, id, nth_value(id, 3) IGNORE NULLS OVER w AS thrid_val,
+ array_agg(id) OVER w
+ FROM data
+ WINDOW w AS (
+ ORDER BY gid
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS gid < 13
+ );
+ gid | id | thrid_val | array_agg
+-----+----+-----------+------------
+ 10 | 1 | | {1,NULL,3}
+ 11 | | |
+ 12 | 3 | |
+ 13 | 4 | |
+(4 rows)
+
+-- nth_value beyond reduced frame with IGNORE NULLS
+-- Tests ignorenulls_getfuncarginframe early out-of-frame check
+SELECT company, tdate, price,
+ nth_value(price, 5) IGNORE NULLS OVER w AS nth_5_in
+FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ company | tdate | price | nth_5_in
+----------+------------+-------+----------
+ company1 | 07-01-2023 | 100 |
+ company1 | 07-02-2023 | 200 |
+ company1 | 07-03-2023 | 150 |
+ company1 | 07-04-2023 | 140 |
+ company1 | 07-05-2023 | 150 |
+ company1 | 07-06-2023 | 90 |
+ company1 | 07-07-2023 | 110 |
+ company1 | 07-08-2023 | 130 |
+ company1 | 07-09-2023 | 120 |
+ company1 | 07-10-2023 | 130 |
+ company2 | 07-01-2023 | 50 |
+ company2 | 07-02-2023 | 2000 |
+ company2 | 07-03-2023 | 1500 |
+ company2 | 07-04-2023 | 1400 |
+ company2 | 07-05-2023 | 1500 |
+ company2 | 07-06-2023 | 60 |
+ company2 | 07-07-2023 | 1100 |
+ company2 | 07-08-2023 | 1300 |
+ company2 | 07-09-2023 | 1200 |
+ company2 | 07-10-2023 | 1300 |
+(20 rows)
+
+-- View and pg_get_viewdef tests.
+CREATE TEMP VIEW v_window AS
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
+ nth_value(tdate, 2) OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+SELECT * FROM v_window;
+ company | tdate | price | first_value | last_value | nth_second
+----------+------------+-------+-------------+------------+------------
+ company1 | 07-01-2023 | 100 | 100 | 140 | 07-02-2023
+ company1 | 07-02-2023 | 200 | | |
+ company1 | 07-03-2023 | 150 | | |
+ company1 | 07-04-2023 | 140 | | |
+ company1 | 07-05-2023 | 150 | | |
+ company1 | 07-06-2023 | 90 | 90 | 120 | 07-07-2023
+ company1 | 07-07-2023 | 110 | | |
+ company1 | 07-08-2023 | 130 | | |
+ company1 | 07-09-2023 | 120 | | |
+ company1 | 07-10-2023 | 130 | | |
+ company2 | 07-01-2023 | 50 | 50 | 1400 | 07-02-2023
+ company2 | 07-02-2023 | 2000 | | |
+ company2 | 07-03-2023 | 1500 | | |
+ company2 | 07-04-2023 | 1400 | | |
+ company2 | 07-05-2023 | 1500 | | |
+ company2 | 07-06-2023 | 60 | 60 | 1200 | 07-07-2023
+ company2 | 07-07-2023 | 1100 | | |
+ company2 | 07-08-2023 | 1300 | | |
+ company2 | 07-09-2023 | 1200 | | |
+ company2 | 07-10-2023 | 1300 | | |
+(20 rows)
+
+SELECT pg_get_viewdef('v_window');
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ first_value(price) OVER w AS first_value, +
+ last_value(price) OVER w AS last_value, +
+ nth_value(tdate, 2) OVER w AS nth_second +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (start up+ down+) +
+ DEFINE +
+ start AS true, +
+ up AS (price > prev(price)), +
+ down AS (price < prev(price)) );
+(1 row)
+
+--
+-- Pattern optimization tests
+-- VIEW shows original pattern, EXPLAIN shows optimized pattern
+--
+-- Test: duplicate alternatives removal (A | B | A)+ -> (A | B)+
+CREATE TEMP VIEW v_opt_dup AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A | B | A)+)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_dup'); -- original: ((a | b | a)+)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ((a | b | a)+) +
+ DEFINE +
+ a AS (price > 100), +
+ b AS (price <= 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_dup; -- optimized: ((a | b)+)
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_dup
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b)+
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: duplicate group removal ((A | B)+ | (A | B)+) -> (A | B)+
+CREATE TEMP VIEW v_opt_dup_group AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A | B)+ | (A | B)+)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_dup_group'); -- original: ((a | b)+ | (a | b)+)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ((a | b)+ | (a | b)+) +
+ DEFINE +
+ a AS (price > 100), +
+ b AS (price <= 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_dup_group; -- optimized: ((a | b)+)
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_dup_group
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b)+
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: consecutive vars merge (A A A) -> A{3}
+CREATE TEMP VIEW v_opt_merge AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A A A)
+ DEFINE
+ A AS price >= 140 AND price <= 150
+);
+SELECT pg_get_viewdef('v_opt_merge'); -- original: (a a a)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a a a) +
+ DEFINE +
+ a AS ((price >= 140) AND (price <= 150)) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge; -- optimized: a{3}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_merge
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{3}
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: quantified vars merge (A A+ A) -> A{3,}
+CREATE TEMP VIEW v_opt_merge_quant AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A A+ A)
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_merge_quant'); -- original: (a a+ a)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a a+ a) +
+ DEFINE +
+ a AS (price > 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_quant; -- optimized: a{3,}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_merge_quant
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{3,}"
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: merge two unbounded (A+ A+) -> A{2,}
+CREATE TEMP VIEW v_opt_merge_unbounded AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A+ A+)
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_merge_unbounded'); -- original: (a+ a+)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a+ a+) +
+ DEFINE +
+ a AS (price > 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_unbounded; -- optimized: a{2,}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_merge_unbounded
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{2,}"
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: merge with zero-min (A* A+) -> A+
+CREATE TEMP VIEW v_opt_merge_star AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A* A+)
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_merge_star'); -- original: (a* a+)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a* a+) +
+ DEFINE +
+ a AS (price > 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_star; -- optimized: a+
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_merge_star
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: complex merge (A A{2} A+ A{3}) -> A{7,}
+CREATE TEMP VIEW v_opt_merge_complex AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A A{2} A+ A{3})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_merge_complex'); -- original: (a a{2} a+ a{3})
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a a{2} a+ a{3}) +
+ DEFINE +
+ a AS (price > 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_complex; -- optimized: a{7,}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_merge_complex
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{7,}"
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: group merge ((A B) (A B)+) -> (A B){2,}
+CREATE TEMP VIEW v_opt_merge_group AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A B) (A B)+)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_merge_group'); -- original: ((a b) (a b)+)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ((a b) (a b)+) +
+ DEFINE +
+ a AS (price > 100), +
+ b AS (price <= 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group; -- expected: (a b){2,}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_merge_group
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b'){2,}"
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: group merge A B (A B)+ -> (A B){2,}
+CREATE TEMP VIEW v_opt_merge_group2 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A B (A B)+)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_merge_group2'); -- original: (a b (a b)+)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a b (a b)+) +
+ DEFINE +
+ a AS (price > 100), +
+ b AS (price <= 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group2; -- expected: (a b){2,}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_merge_group2
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b'){2,}"
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: group merge (A B) (A B)+ (A B) -> (A B){3,}
+CREATE TEMP VIEW v_opt_merge_group3 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A B) (A B)+ (A B))
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_merge_group3'); -- original: ((a b) (a b)+ (a b))
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ((a b) (a b)+ (a b)) +
+ DEFINE +
+ a AS (price > 100), +
+ b AS (price <= 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group3; -- expected: (a b){3,}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_merge_group3
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b'){3,}"
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: group merge A B A B (A B)+ A B A B -> (A B){5,}
+CREATE TEMP VIEW v_opt_merge_group4 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A B A B (A B)+ A B A B)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_merge_group4'); -- original: (a b a b (a b)+ a b a b)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a b a b (a b)+ a b a b) +
+ DEFINE +
+ a AS (price > 100), +
+ b AS (price <= 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group4; -- expected: (a b){5,}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_merge_group4
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b'){5,}"
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: group merge C A B (A B)+ A B C -> C (A B){3,} C
+CREATE TEMP VIEW v_opt_merge_group5 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (C A B (A B)+ A B C)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100,
+ C AS price > 200
+);
+SELECT pg_get_viewdef('v_opt_merge_group5'); -- original: (c a b (a b)+ a b c)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (c a b (a b)+ a b c) +
+ DEFINE +
+ a AS (price > 100), +
+ b AS (price <= 100), +
+ c AS (price > 200) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group5; -- expected: c (a b){3,} c
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_merge_group5
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: c (a b){3,} c
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: consecutive GROUP merge (A B)+ (A B)+ -> (A B){2,}
+CREATE TEMP VIEW v_opt_merge_consec_group AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A B)+ (A B)+)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_merge_consec_group'); -- original: ((a b)+ (a b)+)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ((a b)+ (a b)+) +
+ DEFINE +
+ a AS (price > 100), +
+ b AS (price <= 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_consec_group; -- expected: (a b){2,}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_merge_consec_group
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b'){2,}"
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: consecutive GROUP merge with different quantifiers (A B){2} (A B){3} -> (A B){5}
+CREATE TEMP VIEW v_opt_merge_consec_group2 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A B){2} (A B){3})
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_merge_consec_group2'); -- original: ((a b){2} (a b){3})
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ((a b){2} (a b){3}) +
+ DEFINE +
+ a AS (price > 100), +
+ b AS (price <= 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_consec_group2; -- expected: (a b){5}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_merge_consec_group2
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a b){5}
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test {n} quantifier display
+CREATE TEMP VIEW v_quantifier_n AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A{3})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_quantifier_n');
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a{3}) +
+ DEFINE +
+ a AS (price > 100) );
+(1 row)
+
+-- Test {n,} quantifier display
+CREATE TEMP VIEW v_quantifier_n_plus AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A{2,})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_quantifier_n_plus');
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a{2,}) +
+ DEFINE +
+ a AS (price > 100) );
+(1 row)
+
+-- Test: flatten nested SEQ (A (B C)) -> A B C
+CREATE TEMP VIEW v_opt_flatten_seq AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A (B C))
+ DEFINE
+ A AS price > 100,
+ B AS price > 150,
+ C AS price < 150
+);
+SELECT pg_get_viewdef('v_opt_flatten_seq'); -- original: (a (b c))
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a (b c)) +
+ DEFINE +
+ a AS (price > 100), +
+ b AS (price > 150), +
+ c AS (price < 150) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_flatten_seq; -- optimized: a b c
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_flatten_seq
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b c
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: flatten nested ALT (A | (B | C)) -> (A | B | C)
+CREATE TEMP VIEW v_opt_flatten_alt AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A | (B | C))+)
+ DEFINE
+ A AS price > 200,
+ B AS price > 100,
+ C AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_flatten_alt'); -- original: ((a | (b | c))+)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ((a | (b | c))+) +
+ DEFINE +
+ a AS (price > 200), +
+ b AS (price > 100), +
+ c AS (price <= 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_flatten_alt; -- optimized: ((a | b | c))+
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_flatten_alt
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b | c)+
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: unwrap GROUP{1,1} ((A)) -> A
+CREATE TEMP VIEW v_opt_unwrap_group AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (((A)))
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_unwrap_group'); -- original: (((a)))
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (((a))) +
+ DEFINE +
+ a AS (price > 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_unwrap_group; -- optimized: a
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_unwrap_group
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: quantifier multiplication (A{2}){3} -> A{6}
+CREATE TEMP VIEW v_opt_quant_mult AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A{2}){3})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_quant_mult'); -- original: ((a{2}){3})
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ((a{2}){3}) +
+ DEFINE +
+ a AS (price > 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult; -- optimized: a{6}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_quant_mult
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{6}
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: quantifier multiplication (A{2,4}){3} -> A{6,12}
+CREATE TEMP VIEW v_opt_quant_mult_range AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A{2,4}){3})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_quant_mult_range'); -- original: ((a{2,4}){3})
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ((a{2,4}){3}) +
+ DEFINE +
+ a AS (price > 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_range; -- optimized: a{6,12}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_quant_mult_range
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{6,12}
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: quantifier multiplication blocked (A{2}){3,5} -> no change
+-- outer range with child exact > 1 causes gaps (6,8,10 not 6,7,8,9,10)
+CREATE TEMP VIEW v_opt_quant_mult_range2 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A{2}){3,5})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_quant_mult_range2'); -- original: ((a{2}){3,5})
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ((a{2}){3,5}) +
+ DEFINE +
+ a AS (price > 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_range2; -- NOT optimized: (a{2}){3,5}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_quant_mult_range2
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a{2}){3,5}
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: quantifier multiplication blocked by INF (A+){3} -> no change
+CREATE TEMP VIEW v_opt_quant_mult_inf AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A+){3})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_quant_mult_inf'); -- original: ((a+){3})
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ((a+){3}) +
+ DEFINE +
+ a AS (price > 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_inf; -- no multiply: (a+){3}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_quant_mult_inf
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a+"){3}
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: unwrap single-item ALT after duplicate removal (A | A) -> A
+CREATE TEMP VIEW v_opt_unwrap_alt AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A | A)+)
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_unwrap_alt'); -- original: ((a | a)+)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ((a | a)+) +
+ DEFINE +
+ a AS (price > 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_unwrap_alt; -- optimized: a+
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_unwrap_alt
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: GROUP{1,1} to SEQ with flatten ((A B)(C D)) -> A B C D
+CREATE TEMP VIEW v_opt_group_to_seq AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (((A B)(C D)))
+ DEFINE
+ A AS price > 200,
+ B AS price > 150,
+ C AS price > 100,
+ D AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_group_to_seq'); -- original: (((a b)(c d)))
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (((a b) (c d))) +
+ DEFINE +
+ a AS (price > 200), +
+ b AS (price > 150), +
+ c AS (price > 100), +
+ d AS (price <= 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_group_to_seq; -- optimized: a b c d
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_group_to_seq
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b c d
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: combined consecutive GROUP + prefix merge A B (A B)+ (A B)+ -> (A B){3,}
+CREATE TEMP VIEW v_opt_combined_merge AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A B (A B)+ (A B)+)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_combined_merge'); -- original: (a b (a b)+ (a b)+)
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a b (a b)+ (a b)+) +
+ DEFINE +
+ a AS (price > 100), +
+ b AS (price <= 100) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_combined_merge; -- expected: (a b){3,}
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_combined_merge
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b'){3,}"
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: nested ALT pattern - bug reproduction
+CREATE TEMP VIEW v_opt_nested_alt AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (((A B) | C) D | A B C)
+ DEFINE
+ A AS price <= 100,
+ B AS price <= 150,
+ C AS price <= 200,
+ D AS price > 200
+);
+SELECT pg_get_viewdef('v_opt_nested_alt');
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (((a b) | c) d | a b c) +
+ DEFINE +
+ a AS (price <= 100), +
+ b AS (price <= 150), +
+ c AS (price <= 200), +
+ d AS (price > 200) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_nested_alt;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_nested_alt
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: ((a b | c) d | a b c)
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+-- Test: nested ALT with unbounded - A+ inside
+CREATE TEMP VIEW v_opt_nested_alt2 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (((A+ B) | C) D | A B C)
+ DEFINE
+ A AS price <= 100,
+ B AS price <= 150,
+ C AS price <= 200,
+ D AS price > 200
+);
+SELECT pg_get_viewdef('v_opt_nested_alt2');
+ pg_get_viewdef
+---------------------------------------------------------------------------------------
+ SELECT company, +
+ tdate, +
+ price, +
+ count(*) OVER w AS count +
+ FROM stock +
+ WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (((a+ b) | c) d | a b c) +
+ DEFINE +
+ a AS (price <= 100), +
+ b AS (price <= 150), +
+ c AS (price <= 200), +
+ d AS (price > 200) );
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_nested_alt2;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ Subquery Scan on v_opt_nested_alt2
+ -> WindowAgg
+ Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: ((a+" b | c) d | a b c)
+ -> Sort
+ Sort Key: stock.company
+ -> Seq Scan on stock
+(7 rows)
+
+--
+-- Error cases
+--
+-- row pattern definition variable name must not appear more than once
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price),
+ UP AS price > PREV(price)
+);
+ERROR: row pattern definition variable name "up" appears more than once in DEFINE clause
+LINE 11: UP AS price > PREV(price),
+ ^
+-- subqueries in DEFINE clause are not supported
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START LOWPRICE)
+ DEFINE
+ START AS TRUE,
+ LOWPRICE AS price < (SELECT 100)
+);
+ERROR: cannot use subquery in DEFINE expression
+LINE 11: LOWPRICE AS price < (SELECT 100)
+ ^
+-- aggregates in DEFINE clause are not supported
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START LOWPRICE)
+ DEFINE
+ START AS TRUE,
+ LOWPRICE AS price < count(*)
+);
+ERROR: aggregate functions are not allowed in DEFINE
+LINE 11: LOWPRICE AS price < count(*)
+ ^
+-- FRAME must start at current row when row pattern recognition is used
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ERROR: FRAME must start at CURRENT ROW when row pattern recognition is used
+LINE 6: ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+ ^
+DETAIL: Current frame starts with UNBOUNDED PRECEDING.
+HINT: Use: ROWS BETWEEN CURRENT ROW AND ...
+-- EXCLUDE is not permitted
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ EXCLUDE CURRENT ROW
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+LINE 7: EXCLUDE CURRENT ROW
+ ^
+DETAIL: Frame definition includes EXCLUDE CURRENT ROW.
+HINT: Remove the EXCLUDE clause from the window definition.
+-- SEEK is not supported
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ SEEK
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+ERROR: SEEK is not supported
+LINE 8: SEEK
+ ^
+HINT: Use INITIAL instead.
+-- PREV's argument must have at least 1 column reference
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(1),
+ DOWN AS price < PREV(1)
+);
+ERROR: row pattern navigation operation's argument must include at least one column reference
+-- Unsupported quantifier
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (START UP~ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(1),
+ DOWN AS price < PREV(1)
+);
+ERROR: unsupported quantifier "~"
+LINE 9: PATTERN (START UP~ DOWN+)
+ ^
+HINT: Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (START UP+? DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(1),
+ DOWN AS price < PREV(1)
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 9: PATTERN (START UP+? DOWN+)
+ ^
+-- Maximum pattern variables is 251 (RPR_VARID_MAX)
+-- Error: 252 variables exceeds limit of 251
+DO $$
+DECLARE
+ pattern_vars text;
+ define_vars text;
+ query text;
+BEGIN
+ SELECT string_agg('v' || lpad(i::text, 3, '0'), ' '),
+ string_agg('v' || lpad(i::text, 3, '0') || ' AS TRUE', ', ')
+ INTO pattern_vars, define_vars
+ FROM generate_series(1, 252) i;
+
+ query := format('SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (%s)
+ DEFINE %s)', pattern_vars, define_vars);
+
+ EXECUTE query;
+END;
+$$;
+ERROR: too many pattern variables
+DETAIL: Maximum is 251.
+CONTEXT: SQL statement "SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (v001 v002 v003 v004 v005 v006 v007 v008 v009 v010 v011 v012 v013 v014 v015 v016 v017 v018 v019 v020 v021 v022 v023 v024 v025 v026 v027 v028 v029 v030 v031 v032 v033 v034 v035 v036 v037 v038 v039 v040 v041 v042 v043 v044 v045 v046 v047 v048 v049 v050 v051 v052 v053 v054 v055 v056 v057 v058 v059 v060 v061 v062 v063 v064 v065 v066 v067 v068 v069 v070 v071 v072 v073 v074 v075 v076 v077 v078 v079 v080 v081 v082 v083 v084 v085 v086 v087 v088 v089 v090 v091 v092 v093 v094 v095 v096 v097 v098 v099 v100 v101 v102 v103 v104 v105 v106 v107 v108 v109 v110 v111 v112 v113 v114 v115 v116 v117 v118 v119 v120 v121 v122 v123 v124 v125 v126 v127 v128 v129 v130 v131 v132 v133 v134 v135 v136 v137 v138 v139 v140 v141 v142 v143 v144 v145 v146 v147 v148 v149 v150 v151 v152 v153 v154 v155 v156 v157 v158 v159 v160 v161 v162 v163 v164 v165 v166 v167 v168 v169 v170 v171 v172 v173 v174 v175 v176 v177 v178 v179 v180 v181 v182 v183 v184 v185 v186 v187 v188 v189 v190 v191 v192 v193 v194 v195 v196 v197 v198 v199 v200 v201 v202 v203 v204 v205 v206 v207 v208 v209 v210 v211 v212 v213 v214 v215 v216 v217 v218 v219 v220 v221 v222 v223 v224 v225 v226 v227 v228 v229 v230 v231 v232 v233 v234 v235 v236 v237 v238 v239 v240 v241 v242 v243 v244 v245 v246 v247 v248 v249 v250 v251 v252)
+ DEFINE v001 AS TRUE, v002 AS TRUE, v003 AS TRUE, v004 AS TRUE, v005 AS TRUE, v006 AS TRUE, v007 AS TRUE, v008 AS TRUE, v009 AS TRUE, v010 AS TRUE, v011 AS TRUE, v012 AS TRUE, v013 AS TRUE, v014 AS TRUE, v015 AS TRUE, v016 AS TRUE, v017 AS TRUE, v018 AS TRUE, v019 AS TRUE, v020 AS TRUE, v021 AS TRUE, v022 AS TRUE, v023 AS TRUE, v024 AS TRUE, v025 AS TRUE, v026 AS TRUE, v027 AS TRUE, v028 AS TRUE, v029 AS TRUE, v030 AS TRUE, v031 AS TRUE, v032 AS TRUE, v033 AS TRUE, v034 AS TRUE, v035 AS TRUE, v036 AS TRUE, v037 AS TRUE, v038 AS TRUE, v039 AS TRUE, v040 AS TRUE, v041 AS TRUE, v042 AS TRUE, v043 AS TRUE, v044 AS TRUE, v045 AS TRUE, v046 AS TRUE, v047 AS TRUE, v048 AS TRUE, v049 AS TRUE, v050 AS TRUE, v051 AS TRUE, v052 AS TRUE, v053 AS TRUE, v054 AS TRUE, v055 AS TRUE, v056 AS TRUE, v057 AS TRUE, v058 AS TRUE, v059 AS TRUE, v060 AS TRUE, v061 AS TRUE, v062 AS TRUE, v063 AS TRUE, v064 AS TRUE, v065 AS TRUE, v066 AS TRUE, v067 AS TRUE, v068 AS TRUE, v069 AS TRUE, v070 AS TRUE, v071 AS TRUE, v072 AS TRUE, v073 AS TRUE, v074 AS TRUE, v075 AS TRUE, v076 AS TRUE, v077 AS TRUE, v078 AS TRUE, v079 AS TRUE, v080 AS TRUE, v081 AS TRUE, v082 AS TRUE, v083 AS TRUE, v084 AS TRUE, v085 AS TRUE, v086 AS TRUE, v087 AS TRUE, v088 AS TRUE, v089 AS TRUE, v090 AS TRUE, v091 AS TRUE, v092 AS TRUE, v093 AS TRUE, v094 AS TRUE, v095 AS TRUE, v096 AS TRUE, v097 AS TRUE, v098 AS TRUE, v099 AS TRUE, v100 AS TRUE, v101 AS TRUE, v102 AS TRUE, v103 AS TRUE, v104 AS TRUE, v105 AS TRUE, v106 AS TRUE, v107 AS TRUE, v108 AS TRUE, v109 AS TRUE, v110 AS TRUE, v111 AS TRUE, v112 AS TRUE, v113 AS TRUE, v114 AS TRUE, v115 AS TRUE, v116 AS TRUE, v117 AS TRUE, v118 AS TRUE, v119 AS TRUE, v120 AS TRUE, v121 AS TRUE, v122 AS TRUE, v123 AS TRUE, v124 AS TRUE, v125 AS TRUE, v126 AS TRUE, v127 AS TRUE, v128 AS TRUE, v129 AS TRUE, v130 AS TRUE, v131 AS TRUE, v132 AS TRUE, v133 AS TRUE, v134 AS TRUE, v135 AS TRUE, v136 AS TRUE, v137 AS TRUE, v138 AS TRUE, v139 AS TRUE, v140 AS TRUE, v141 AS TRUE, v142 AS TRUE, v143 AS TRUE, v144 AS TRUE, v145 AS TRUE, v146 AS TRUE, v147 AS TRUE, v148 AS TRUE, v149 AS TRUE, v150 AS TRUE, v151 AS TRUE, v152 AS TRUE, v153 AS TRUE, v154 AS TRUE, v155 AS TRUE, v156 AS TRUE, v157 AS TRUE, v158 AS TRUE, v159 AS TRUE, v160 AS TRUE, v161 AS TRUE, v162 AS TRUE, v163 AS TRUE, v164 AS TRUE, v165 AS TRUE, v166 AS TRUE, v167 AS TRUE, v168 AS TRUE, v169 AS TRUE, v170 AS TRUE, v171 AS TRUE, v172 AS TRUE, v173 AS TRUE, v174 AS TRUE, v175 AS TRUE, v176 AS TRUE, v177 AS TRUE, v178 AS TRUE, v179 AS TRUE, v180 AS TRUE, v181 AS TRUE, v182 AS TRUE, v183 AS TRUE, v184 AS TRUE, v185 AS TRUE, v186 AS TRUE, v187 AS TRUE, v188 AS TRUE, v189 AS TRUE, v190 AS TRUE, v191 AS TRUE, v192 AS TRUE, v193 AS TRUE, v194 AS TRUE, v195 AS TRUE, v196 AS TRUE, v197 AS TRUE, v198 AS TRUE, v199 AS TRUE, v200 AS TRUE, v201 AS TRUE, v202 AS TRUE, v203 AS TRUE, v204 AS TRUE, v205 AS TRUE, v206 AS TRUE, v207 AS TRUE, v208 AS TRUE, v209 AS TRUE, v210 AS TRUE, v211 AS TRUE, v212 AS TRUE, v213 AS TRUE, v214 AS TRUE, v215 AS TRUE, v216 AS TRUE, v217 AS TRUE, v218 AS TRUE, v219 AS TRUE, v220 AS TRUE, v221 AS TRUE, v222 AS TRUE, v223 AS TRUE, v224 AS TRUE, v225 AS TRUE, v226 AS TRUE, v227 AS TRUE, v228 AS TRUE, v229 AS TRUE, v230 AS TRUE, v231 AS TRUE, v232 AS TRUE, v233 AS TRUE, v234 AS TRUE, v235 AS TRUE, v236 AS TRUE, v237 AS TRUE, v238 AS TRUE, v239 AS TRUE, v240 AS TRUE, v241 AS TRUE, v242 AS TRUE, v243 AS TRUE, v244 AS TRUE, v245 AS TRUE, v246 AS TRUE, v247 AS TRUE, v248 AS TRUE, v249 AS TRUE, v250 AS TRUE, v251 AS TRUE, v252 AS TRUE)"
+PL/pgSQL function inline_code_block line 17 at EXECUTE
+-- Error: 253 variables exceeds limit of 251
+DO $$
+DECLARE
+ pattern_vars text;
+ define_vars text;
+ query text;
+BEGIN
+ SELECT string_agg('v' || lpad(i::text, 3, '0'), ' '),
+ string_agg('v' || lpad(i::text, 3, '0') || ' AS TRUE', ', ')
+ INTO pattern_vars, define_vars
+ FROM generate_series(1, 253) i;
+
+ query := format('SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (%s)
+ DEFINE %s)', pattern_vars, define_vars);
+
+ EXECUTE query;
+END;
+$$;
+ERROR: too many pattern variables
+DETAIL: Maximum is 251.
+CONTEXT: SQL statement "SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (v001 v002 v003 v004 v005 v006 v007 v008 v009 v010 v011 v012 v013 v014 v015 v016 v017 v018 v019 v020 v021 v022 v023 v024 v025 v026 v027 v028 v029 v030 v031 v032 v033 v034 v035 v036 v037 v038 v039 v040 v041 v042 v043 v044 v045 v046 v047 v048 v049 v050 v051 v052 v053 v054 v055 v056 v057 v058 v059 v060 v061 v062 v063 v064 v065 v066 v067 v068 v069 v070 v071 v072 v073 v074 v075 v076 v077 v078 v079 v080 v081 v082 v083 v084 v085 v086 v087 v088 v089 v090 v091 v092 v093 v094 v095 v096 v097 v098 v099 v100 v101 v102 v103 v104 v105 v106 v107 v108 v109 v110 v111 v112 v113 v114 v115 v116 v117 v118 v119 v120 v121 v122 v123 v124 v125 v126 v127 v128 v129 v130 v131 v132 v133 v134 v135 v136 v137 v138 v139 v140 v141 v142 v143 v144 v145 v146 v147 v148 v149 v150 v151 v152 v153 v154 v155 v156 v157 v158 v159 v160 v161 v162 v163 v164 v165 v166 v167 v168 v169 v170 v171 v172 v173 v174 v175 v176 v177 v178 v179 v180 v181 v182 v183 v184 v185 v186 v187 v188 v189 v190 v191 v192 v193 v194 v195 v196 v197 v198 v199 v200 v201 v202 v203 v204 v205 v206 v207 v208 v209 v210 v211 v212 v213 v214 v215 v216 v217 v218 v219 v220 v221 v222 v223 v224 v225 v226 v227 v228 v229 v230 v231 v232 v233 v234 v235 v236 v237 v238 v239 v240 v241 v242 v243 v244 v245 v246 v247 v248 v249 v250 v251 v252 v253)
+ DEFINE v001 AS TRUE, v002 AS TRUE, v003 AS TRUE, v004 AS TRUE, v005 AS TRUE, v006 AS TRUE, v007 AS TRUE, v008 AS TRUE, v009 AS TRUE, v010 AS TRUE, v011 AS TRUE, v012 AS TRUE, v013 AS TRUE, v014 AS TRUE, v015 AS TRUE, v016 AS TRUE, v017 AS TRUE, v018 AS TRUE, v019 AS TRUE, v020 AS TRUE, v021 AS TRUE, v022 AS TRUE, v023 AS TRUE, v024 AS TRUE, v025 AS TRUE, v026 AS TRUE, v027 AS TRUE, v028 AS TRUE, v029 AS TRUE, v030 AS TRUE, v031 AS TRUE, v032 AS TRUE, v033 AS TRUE, v034 AS TRUE, v035 AS TRUE, v036 AS TRUE, v037 AS TRUE, v038 AS TRUE, v039 AS TRUE, v040 AS TRUE, v041 AS TRUE, v042 AS TRUE, v043 AS TRUE, v044 AS TRUE, v045 AS TRUE, v046 AS TRUE, v047 AS TRUE, v048 AS TRUE, v049 AS TRUE, v050 AS TRUE, v051 AS TRUE, v052 AS TRUE, v053 AS TRUE, v054 AS TRUE, v055 AS TRUE, v056 AS TRUE, v057 AS TRUE, v058 AS TRUE, v059 AS TRUE, v060 AS TRUE, v061 AS TRUE, v062 AS TRUE, v063 AS TRUE, v064 AS TRUE, v065 AS TRUE, v066 AS TRUE, v067 AS TRUE, v068 AS TRUE, v069 AS TRUE, v070 AS TRUE, v071 AS TRUE, v072 AS TRUE, v073 AS TRUE, v074 AS TRUE, v075 AS TRUE, v076 AS TRUE, v077 AS TRUE, v078 AS TRUE, v079 AS TRUE, v080 AS TRUE, v081 AS TRUE, v082 AS TRUE, v083 AS TRUE, v084 AS TRUE, v085 AS TRUE, v086 AS TRUE, v087 AS TRUE, v088 AS TRUE, v089 AS TRUE, v090 AS TRUE, v091 AS TRUE, v092 AS TRUE, v093 AS TRUE, v094 AS TRUE, v095 AS TRUE, v096 AS TRUE, v097 AS TRUE, v098 AS TRUE, v099 AS TRUE, v100 AS TRUE, v101 AS TRUE, v102 AS TRUE, v103 AS TRUE, v104 AS TRUE, v105 AS TRUE, v106 AS TRUE, v107 AS TRUE, v108 AS TRUE, v109 AS TRUE, v110 AS TRUE, v111 AS TRUE, v112 AS TRUE, v113 AS TRUE, v114 AS TRUE, v115 AS TRUE, v116 AS TRUE, v117 AS TRUE, v118 AS TRUE, v119 AS TRUE, v120 AS TRUE, v121 AS TRUE, v122 AS TRUE, v123 AS TRUE, v124 AS TRUE, v125 AS TRUE, v126 AS TRUE, v127 AS TRUE, v128 AS TRUE, v129 AS TRUE, v130 AS TRUE, v131 AS TRUE, v132 AS TRUE, v133 AS TRUE, v134 AS TRUE, v135 AS TRUE, v136 AS TRUE, v137 AS TRUE, v138 AS TRUE, v139 AS TRUE, v140 AS TRUE, v141 AS TRUE, v142 AS TRUE, v143 AS TRUE, v144 AS TRUE, v145 AS TRUE, v146 AS TRUE, v147 AS TRUE, v148 AS TRUE, v149 AS TRUE, v150 AS TRUE, v151 AS TRUE, v152 AS TRUE, v153 AS TRUE, v154 AS TRUE, v155 AS TRUE, v156 AS TRUE, v157 AS TRUE, v158 AS TRUE, v159 AS TRUE, v160 AS TRUE, v161 AS TRUE, v162 AS TRUE, v163 AS TRUE, v164 AS TRUE, v165 AS TRUE, v166 AS TRUE, v167 AS TRUE, v168 AS TRUE, v169 AS TRUE, v170 AS TRUE, v171 AS TRUE, v172 AS TRUE, v173 AS TRUE, v174 AS TRUE, v175 AS TRUE, v176 AS TRUE, v177 AS TRUE, v178 AS TRUE, v179 AS TRUE, v180 AS TRUE, v181 AS TRUE, v182 AS TRUE, v183 AS TRUE, v184 AS TRUE, v185 AS TRUE, v186 AS TRUE, v187 AS TRUE, v188 AS TRUE, v189 AS TRUE, v190 AS TRUE, v191 AS TRUE, v192 AS TRUE, v193 AS TRUE, v194 AS TRUE, v195 AS TRUE, v196 AS TRUE, v197 AS TRUE, v198 AS TRUE, v199 AS TRUE, v200 AS TRUE, v201 AS TRUE, v202 AS TRUE, v203 AS TRUE, v204 AS TRUE, v205 AS TRUE, v206 AS TRUE, v207 AS TRUE, v208 AS TRUE, v209 AS TRUE, v210 AS TRUE, v211 AS TRUE, v212 AS TRUE, v213 AS TRUE, v214 AS TRUE, v215 AS TRUE, v216 AS TRUE, v217 AS TRUE, v218 AS TRUE, v219 AS TRUE, v220 AS TRUE, v221 AS TRUE, v222 AS TRUE, v223 AS TRUE, v224 AS TRUE, v225 AS TRUE, v226 AS TRUE, v227 AS TRUE, v228 AS TRUE, v229 AS TRUE, v230 AS TRUE, v231 AS TRUE, v232 AS TRUE, v233 AS TRUE, v234 AS TRUE, v235 AS TRUE, v236 AS TRUE, v237 AS TRUE, v238 AS TRUE, v239 AS TRUE, v240 AS TRUE, v241 AS TRUE, v242 AS TRUE, v243 AS TRUE, v244 AS TRUE, v245 AS TRUE, v246 AS TRUE, v247 AS TRUE, v248 AS TRUE, v249 AS TRUE, v250 AS TRUE, v251 AS TRUE, v252 AS TRUE, v253 AS TRUE)"
+PL/pgSQL function inline_code_block line 17 at EXECUTE
+ CREATE TEMP TABLE stock_null (company TEXT, tdate DATE, price INTEGER);
+ INSERT INTO stock_null VALUES ('c1', '2023-07-01', 100);
+ INSERT INTO stock_null VALUES ('c1', '2023-07-02', NULL); -- NULL in middle
+ INSERT INTO stock_null VALUES ('c1', '2023-07-03', 200);
+ INSERT INTO stock_null VALUES ('c1', '2023-07-04', 150);
+ SELECT company, tdate, price, count(*) OVER w AS match_count
+ FROM stock_null
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (START UP DOWN)
+ DEFINE START AS TRUE, UP AS price > PREV(price), DOWN AS price <
+PREV(price)
+ );
+ company | tdate | price | match_count
+---------+------------+-------+-------------
+ c1 | 07-01-2023 | 100 | 0
+ c1 | 07-02-2023 | | 0
+ c1 | 07-03-2023 | 200 | 0
+ c1 | 07-04-2023 | 150 | 0
+(4 rows)
+
+-- Overlapping match tests (requires multi-context for correct behavior)
+-- Using array flags: 'X' = ANY(flags) for multi-TRUE support
+-- Test 1: A B C D E | B C D | C D E F - three overlapping patterns
+-- Different end points: B C D (4), A B C D E (5), C D E F (6)
+WITH test_overlap1 AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E']),
+ (6, ARRAY['F'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E | B C D | C D E F)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags),
+ F AS 'F' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 5
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {D} | |
+ 5 | {E} | |
+ 6 | {F} | |
+(6 rows)
+
+WITH test_overlap1 AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E']),
+ (6, ARRAY['F'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C D E | B C D | C D E F)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags),
+ F AS 'F' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 5
+ 2 | {B} | 2 | 4
+ 3 | {C} | 3 | 6
+ 4 | {D} | |
+ 5 | {E} | |
+ 6 | {F} | |
+(6 rows)
+
+-- PAST LAST: only one match
+-- TO NEXT ROW with multi-context: three matches
+-- Row 1: A B C D E (1-5)
+-- Row 2: B C D (2-4) <- ends first!
+-- Row 3: C D E F (3-6) <- ends last!
+-- Test 1b: Longer pattern FAILS, shorter pattern should survive
+-- Pattern: A+ B C D E | B+ C
+-- A+ B C D E fails (no E found in sequence)
+-- B+ C matches at rows 2-3
+-- Result: match 2-3 (B+ C)
+WITH test_overlap1b AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1b
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B C D E | B+ C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | |
+ 2 | {B} | 2 | 3
+ 3 | {C} | |
+ 4 | {D} | |
+ 5 | {X} | |
+(5 rows)
+
+-- Test 2: A B+ C | B+ D - long B sequence with different endings
+WITH test_overlap2 AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['B']),
+ (5, ARRAY['B']),
+ (6, ARRAY['C']),
+ (7, ARRAY['B']),
+ (8, ARRAY['B']),
+ (9, ARRAY['B']),
+ (10, ARRAY['D'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap2
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B+ C | B+ D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 6
+ 2 | {B} | |
+ 3 | {B} | |
+ 4 | {B} | |
+ 5 | {B} | |
+ 6 | {C} | |
+ 7 | {B} | 7 | 10
+ 8 | {B} | 8 | 10
+ 9 | {B} | 9 | 10
+ 10 | {D} | |
+(10 rows)
+
+-- Current result (correct):
+-- Row 1: A B+ C (1-6)
+-- Row 7-9: B+ D (7-10, 8-10, 9-10)
+-- Note: Row 2-6 cannot match B+ D because Row 6 is C, not D
+-- With absorption: 8-10 and 9-10 would be absorbed by 7-10 (earlier context covers later)
+-- Test 3: Greedy quantifier with late failure - A B C+ D | A B
+-- Pattern expects D after C+, but E comes instead ("betrayal")
+WITH test_betrayal AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['C']),
+ (5, ARRAY['C']),
+ (6, ARRAY['E'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_betrayal
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C+ D | A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 2
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {C} | |
+ 5 | {C} | |
+ 6 | {E} | |
+(6 rows)
+
+-- A B C+ D fails at Row 6 (E instead of D)
+-- Question: Does it fallback to A B (1-2)?
+-- Test 4: Lexical Order test - A B C | A B C D E
+-- SQL standard: first matching alternative wins
+WITH test_lexical AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_lexical
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C | A B C D E)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {D} | |
+ 5 | {E} | |
+(5 rows)
+
+-- SQL standard Lexical Order: A B C (1-3) wins (first alternative)
+-- Test 4b: Reversed pattern order - A B C D E | A B C
+WITH test_lexical2 AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_lexical2
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E | A B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 5
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {D} | |
+ 5 | {E} | |
+(5 rows)
+
+-- SQL standard Lexical Order: A B C D E (1-5) wins (first alternative)
+-- Test 5: Multiple TRUE in single row (overlapping pattern variables)
+-- Each row matches multiple DEFINE conditions simultaneously
+WITH test_multi_true AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A','B']), -- A and B both TRUE
+ (2, ARRAY['B','C']), -- B and C both TRUE
+ (3, ARRAY['C','D']), -- C and D both TRUE
+ (4, ARRAY['D','E']), -- D and E both TRUE
+ (5, ARRAY['E','_']) -- E only
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_multi_true
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A,B} | 1 | 5
+ 2 | {B,C} | |
+ 3 | {C,D} | |
+ 4 | {D,E} | |
+ 5 | {E,_} | |
+(5 rows)
+
+-- Row 1: A=T, B=T -> matches A
+-- Row 2: B=T, C=T -> matches B
+-- Row 3: C=T, D=T -> matches C
+-- Row 4: D=T, E=T -> matches D
+-- Row 5: E=T -> matches E
+-- Result: match 1-5 (A B C D E)
+-- Test 6: Diagonal pattern with multi-TRUE (shifted overlap)
+WITH test_diagonal AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A','_']),
+ (2, ARRAY['B','A']),
+ (3, ARRAY['C','B']),
+ (4, ARRAY['D','C']),
+ (5, ARRAY['_','D'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_diagonal
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A,_} | 1 | 4
+ 2 | {B,A} | 2 | 5
+ 3 | {C,B} | |
+ 4 | {D,C} | |
+ 5 | {_,D} | |
+(5 rows)
+
+-- Possible matches:
+-- Start Row 1: A(1) B(2) C(3) D(4) -> 1-4
+-- Start Row 2: A(2) B(3) C(4) D(5) -> 2-5 (because Row 2 has A too!)
+-- ===================================================================
+-- Context Absorption Tests
+-- ===================================================================
+-- Test absorption 1: Basic A+ pattern - later contexts absorbed by earlier
+WITH test_absorb_basic AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_basic
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | 2 | 4
+ 3 | {A} | 3 | 4
+ 4 | {A} | 4 | 4
+ 5 | {B} | |
+(5 rows)
+
+-- Pattern A+ is absorbable (unbounded first element, only one unbounded)
+-- 4 matches: (1-4, 2-4, 3-4, 4-4)
+-- Test absorption 2: A+ B pattern - absorption with fixed suffix
+WITH test_absorb_suffix AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_suffix
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | 2 | 4
+ 3 | {A} | 3 | 4
+ 4 | {B} | |
+ 5 | {X} | |
+(5 rows)
+
+-- Pattern A+ B is absorbable (A+ unbounded first, B bounded suffix)
+-- All potential matches end at same row (row 4 with B)
+-- 3 matches: (1-4, 2-4, 3-4)
+-- Test absorption 3: Per-branch absorption with ALT (B+ C | B+ D)
+WITH test_absorb_alt AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['B']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['D']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_alt
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (B+ C | B+ D)
+ DEFINE
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {B} | 1 | 4
+ 2 | {B} | 2 | 4
+ 3 | {B} | 3 | 4
+ 4 | {D} | |
+ 5 | {X} | |
+(5 rows)
+
+-- Both branches B+ C and B+ D are absorbable (B+ unbounded first)
+-- B+ D branch matches: 3 matches (1-4, 2-4, 3-4)
+-- Test absorption 4: Non-absorbable pattern (A B+ - unbounded not first)
+WITH test_no_absorb AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['B']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_no_absorb
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {B} | |
+ 3 | {B} | |
+ 4 | {B} | |
+ 5 | {X} | |
+(5 rows)
+
+-- Pattern A B+ is NOT absorbable (A bounded first, B+ unbounded but not first)
+-- Only Row 1 can start match (only row with A), so only one match: 1-4
+-- Test absorption 5: GROUP merge enables absorption
+WITH test_absorb_group AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']),
+ (7, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_group
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A B) (A B)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 6
+ 2 | {B} | |
+ 3 | {A} | 3 | 6
+ 4 | {B} | |
+ 5 | {A} | |
+ 6 | {B} | |
+ 7 | {X} | |
+(7 rows)
+
+-- Pattern optimized: (A B) (A B)+ -> (A B){2,}
+-- 2 matches: 1-6 (3 reps), 3-6 (2 reps)
+-- Test absorption 6: Multiple unbounded - first element unbounded enables absorption
+WITH test_multi_unbounded AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['B']),
+ (4, ARRAY['B']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_multi_unbounded
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | 2 | 4
+ 3 | {B} | |
+ 4 | {B} | |
+ 5 | {X} | |
+(5 rows)
+
+-- 2 matches: 1-4, 2-4 (same endpoint 4)
+-- ============================================
+-- Jacob's RPR Patterns (from jacob branch)
+-- ============================================
+-- Test: A? (optional, greedy)
+WITH jacob_optional AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_optional
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A?)
+ DEFINE A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 1
+ 2 | {X} | |
+(2 rows)
+
+-- Expected: 1-1 (matches A)
+-- Test: A{2} (exact count)
+WITH jacob_exact AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_exact
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2})
+ DEFINE A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 2
+ 2 | {A} | |
+ 3 | {A} | |
+ 4 | {X} | |
+(4 rows)
+
+-- Expected: 1-2
+-- Test: A{1,3} (bounded range, greedy)
+WITH jacob_bounded AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_bounded
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{1,3})
+ DEFINE A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {A} | |
+ 3 | {A} | |
+ 4 | {A} | 4 | 4
+ 5 | {X} | |
+(5 rows)
+
+-- Expected: 1-3 (greedy takes max), then 4-4
+-- Test: A | B (simple alternation)
+WITH jacob_simple_alt AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_simple_alt
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A | B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 1
+ 2 | {B} | 2 | 2
+ 3 | {X} | |
+(3 rows)
+
+-- Expected: 1-1 (A), 2-2 (B)
+-- Test: A | B | C (three-way alternation)
+WITH jacob_three_alt AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['B']),
+ (2, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_three_alt
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A | B | C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {B} | 1 | 1
+ 2 | {X} | |
+(2 rows)
+
+-- Expected: 1-1 (B)
+-- Test: A B C (concatenation)
+WITH jacob_concat AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_concat
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {X} | |
+(4 rows)
+
+-- Expected: 1-3
+-- Test: A B? C (optional middle)
+WITH jacob_optional_mid AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['C']),
+ (3, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_optional_mid
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B? C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 2
+ 2 | {C} | |
+ 3 | {X} | |
+(3 rows)
+
+-- Expected: 1-2 (A C, B skipped)
+-- Test: (A B){2} (nested group with quantifier)
+WITH jacob_nested_group AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_nested_group
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B){2})
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {B} | |
+ 3 | {A} | |
+ 4 | {B} | |
+ 5 | {X} | |
+(5 rows)
+
+-- Expected: 1-4 (A B A B)
+-- Test: (A){3} (quantifier on grouped single element)
+WITH jacob_group_quant AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_group_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A){3})
+ DEFINE A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {A} | |
+ 3 | {A} | |
+ 4 | {X} | |
+(4 rows)
+
+-- Expected: 1-3
+-- Test: A B C | A B C D E (lexical order - first alt wins)
+WITH jacob_lex_first AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_lex_first
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C | A B C D E)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {D} | |
+ 5 | {E} | |
+(5 rows)
+
+-- Expected: 1-3 (A B C wins by lexical order)
+-- Test: A B C D E | A B C (lexical order - longer first wins)
+WITH jacob_lex_long AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_lex_long
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E | A B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 5
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {D} | |
+ 5 | {E} | |
+(5 rows)
+
+-- Expected: 1-5 (A B C D E wins by lexical order)
+-- ============================================
+-- Alternation with quantifiers (BUG cases from Jacob's tests)
+-- ============================================
+-- Test: (A | B)+ C - alternation inside quantified group followed by C
+WITH jacob_alt_quant AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_alt_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B)+ C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {B} | |
+ 3 | {A} | |
+ 4 | {C} | |
+(4 rows)
+
+-- Expected: 1-4 (A B A C)
+-- Test: ((A | B) C)+ - alternation inside group with outer quantifier
+WITH jacob_alt_group AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['C']),
+ (3, ARRAY['B']),
+ (4, ARRAY['C']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_alt_group
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A | B) C)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {C} | |
+ 3 | {B} | |
+ 4 | {C} | |
+ 5 | {X} | |
+(5 rows)
+
+-- Expected: 1-4 (A C B C)
+-- ============================================
+-- RELUCTANT quantifiers (not yet supported)
+-- ============================================
+-- Test: A+? B (reluctant) - parser rejects with ERROR
+WITH jacob_reluctant AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+? B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 15: PATTERN (A+? B)
+ ^
+-- Expected: ERROR (reluctant quantifiers not yet supported)
+-- Test: A{1,3}? B (reluctant bounded) - parser rejects with ERROR
+WITH jacob_reluctant_bounded AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_reluctant_bounded
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{1,3}? B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 15: PATTERN (A{1,3}? B)
+ ^
+-- Expected: ERROR (reluctant quantifiers not yet supported)
+-- ============================================
+-- Nested quantifiers (pathological patterns)
+-- ============================================
+-- These patterns previously caused segfault or infinite loop.
+-- Now they are either optimized at compile time or handled safely at runtime.
+-- Test: (A*)* - nested unbounded quantifiers (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A*)*)
+ DEFINE A AS TRUE
+);
+ v | c
+---+---
+ 1 | 5
+ 2 | 0
+ 3 | 0
+ 4 | 0
+ 5 | 0
+(5 rows)
+
+-- Test: (A*)+ - inner nullable, outer requires one (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A*)+)
+ DEFINE A AS TRUE
+);
+ v | c
+---+---
+ 1 | 5
+ 2 | 0
+ 3 | 0
+ 4 | 0
+ 5 | 0
+(5 rows)
+
+-- Test: (A+)* - outer nullable (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A+)*)
+ DEFINE A AS TRUE
+);
+ v | c
+---+---
+ 1 | 5
+ 2 | 0
+ 3 | 0
+ 4 | 0
+ 5 | 0
+(5 rows)
+
+-- Test: (A+)+ - both require match (optimized to A+)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A+)+)
+ DEFINE A AS TRUE
+);
+ v | c
+---+---
+ 1 | 5
+ 2 | 0
+ 3 | 0
+ 4 | 0
+ 5 | 0
+(5 rows)
+
+-- Test: (A* B*)* - complex nested pattern (runtime protection)
+-- Not optimized but handled safely by empty-match loop prevention
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A* B*)*)
+ DEFINE A AS TRUE, B AS TRUE
+);
+ v | c
+---+---
+ 1 | 5
+ 2 | 0
+ 3 | 0
+ 4 | 0
+ 5 | 0
+(5 rows)
+
+-- Test: (((A)*)*)* - triple nested (optimized through recursive optimization)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 3) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((((A)*)*)*)
+ DEFINE A AS TRUE
+);
+ v | c
+---+---
+ 1 | 3
+ 2 | 0
+ 3 | 0
+(3 rows)
+
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
new file mode 100644
index 00000000000..ec67a099ee6
--- /dev/null
+++ b/src/test/regress/expected/rpr_base.out
@@ -0,0 +1,5538 @@
+-- ============================================================
+-- RPR Base Tests
+-- Tests for Row Pattern Recognition (ISO/IEC 19075-5:2016)
+-- ============================================================
+--
+-- Parser Layer:
+-- Keyword Usage Tests
+-- DEFINE Clause Tests
+-- FRAME Options Tests
+-- PARTITION BY + FRAME Tests
+-- PATTERN Syntax Tests
+-- Quantifiers Tests
+-- Navigation Functions Tests
+-- SKIP TO / INITIAL Tests
+-- Serialization/Deserialization Tests
+-- Error Cases Tests
+--
+-- Planner Layer:
+-- Pattern Optimization Tests
+-- Absorption Flag Display Tests
+-- Absorption Analysis Tests
+-- Edge Case Tests
+-- Optimization Fallback Tests
+-- Planner Integration Tests
+-- Subquery and CTE Tests
+-- JOIN Tests
+-- Complex Expression Tests
+-- Set Operations Tests
+-- Sorting and Grouping Tests
+-- Stress Tests
+-- Error Limit Tests
+--
+-- Contributed Tests:
+-- Jacob's Patterns
+-- Pathological Patterns
+-- ============================================================
+SET client_min_messages = WARNING;
+-- ============================================================
+-- Keyword Usage Tests
+-- ============================================================
+-- RPR keywords as column names
+-- Keywords: define, initial, past, pattern, seek
+CREATE TABLE rpr_keywords (
+ id INT,
+ define INT, -- DEFINE keyword
+ initial INT, -- INITIAL keyword
+ past INT, -- PAST keyword
+ pattern INT, -- PATTERN keyword
+ seek INT, -- SEEK keyword
+ skip INT -- SKIP keyword (pre-existing)
+);
+INSERT INTO rpr_keywords VALUES (1, 10, 20, 30, 40, 50, 60);
+SELECT id, define, initial, past, pattern, seek, skip
+FROM rpr_keywords
+ORDER BY id;
+ id | define | initial | past | pattern | seek | skip
+----+--------+---------+------+---------+------+------
+ 1 | 10 | 20 | 30 | 40 | 50 | 60
+(1 row)
+
+DROP TABLE rpr_keywords;
+-- ============================================================
+-- DEFINE Clause Tests
+-- ============================================================
+-- Simple column references
+CREATE TABLE stock_price (
+ dt DATE,
+ symbol TEXT,
+ price NUMERIC,
+ volume INT
+);
+INSERT INTO stock_price VALUES
+ ('2024-01-01', 'AAPL', 150, 1000),
+ ('2024-01-02', 'AAPL', 155, 1200),
+ ('2024-01-03', 'AAPL', 152, 900),
+ ('2024-01-04', 'AAPL', 160, 1500),
+ ('2024-01-05', 'AAPL', 158, 1100);
+-- Simple column reference
+SELECT dt, price, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+ PARTITION BY symbol
+ ORDER BY dt
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (UP+)
+ DEFINE UP AS price > 150
+)
+ORDER BY dt;
+ dt | price | cnt
+------------+-------+-----
+ 01-01-2024 | 150 | 0
+ 01-02-2024 | 155 | 4
+ 01-03-2024 | 152 | 0
+ 01-04-2024 | 160 | 0
+ 01-05-2024 | 158 | 0
+(5 rows)
+
+-- Multiple column references
+SELECT dt, price, volume, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+ PARTITION BY symbol
+ ORDER BY dt
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (GOOD+)
+ DEFINE GOOD AS price > 150 AND volume > 1000
+)
+ORDER BY dt;
+ dt | price | volume | cnt
+------------+-------+--------+-----
+ 01-01-2024 | 150 | 1000 | 0
+ 01-02-2024 | 155 | 1200 | 1
+ 01-03-2024 | 152 | 900 | 0
+ 01-04-2024 | 160 | 1500 | 2
+ 01-05-2024 | 158 | 1100 | 0
+(5 rows)
+
+-- Expression in DEFINE
+SELECT dt, price, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+ PARTITION BY symbol
+ ORDER BY dt
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (HIGH+)
+ DEFINE HIGH AS price * 1.1 > 165
+)
+ORDER BY dt;
+ dt | price | cnt
+------------+-------+-----
+ 01-01-2024 | 150 | 0
+ 01-02-2024 | 155 | 4
+ 01-03-2024 | 152 | 0
+ 01-04-2024 | 160 | 0
+ 01-05-2024 | 158 | 0
+(5 rows)
+
+-- Arithmetic and functions
+SELECT dt, price, volume, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+ PARTITION BY symbol
+ ORDER BY dt
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (CALC+)
+ DEFINE CALC AS (price + volume / 100) > 160
+)
+ORDER BY dt;
+ dt | price | volume | cnt
+------------+-------+--------+-----
+ 01-01-2024 | 150 | 1000 | 0
+ 01-02-2024 | 155 | 1200 | 4
+ 01-03-2024 | 152 | 900 | 0
+ 01-04-2024 | 160 | 1500 | 0
+ 01-05-2024 | 158 | 1100 | 0
+(5 rows)
+
+DROP TABLE stock_price;
+-- Auto-generated DEFINE
+CREATE TABLE rpr_auto (id INT, val INT);
+INSERT INTO rpr_auto VALUES (1, 10), (2, 20), (3, 30), (4, 15);
+-- One variable undefined (B auto-generated as "B IS TRUE")
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_auto
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ B*)
+ DEFINE A AS val > 15
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 3
+ 3 | 30 | 0
+ 4 | 15 | 0
+(4 rows)
+
+-- Multiple undefined variables
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_auto
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B C)
+ DEFINE A AS val > 0
+ -- B and C auto-generated as "B IS TRUE", "C IS TRUE"
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 15 | 0
+(4 rows)
+
+-- All variables defined explicitly
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_auto
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (X Y Z)
+ DEFINE
+ X AS val > 10,
+ Y AS val > 20,
+ Z AS val < 20
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 3
+ 3 | 30 | 0
+ 4 | 15 | 0
+(4 rows)
+
+DROP TABLE rpr_auto;
+-- Duplicate variable names
+CREATE TABLE rpr_dup (id INT);
+INSERT INTO rpr_dup VALUES (1), (2);
+-- Duplicate DEFINE entries
+SELECT COUNT(*) OVER w
+FROM rpr_dup
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS id > 0, A AS id < 10
+);
+ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
+LINE 7: DEFINE A AS id > 0, A AS id < 10
+ ^
+-- Expected: ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
+DROP TABLE rpr_dup;
+-- Boolean coercion
+CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
+INSERT INTO rpr_bool VALUES (1, true), (2, false);
+-- Non-boolean expression
+SELECT COUNT(*) OVER w
+FROM rpr_bool
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS id
+);
+ERROR: argument of DEFINE must be type boolean, not type integer
+LINE 7: DEFINE A AS id
+ ^
+-- Expected: ERROR: argument of DEFINE must be type boolean
+-- Boolean column reference
+SELECT id, flag, COUNT(*) OVER w as cnt
+FROM rpr_bool
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (T+)
+ DEFINE T AS flag
+)
+ORDER BY id;
+ id | flag | cnt
+----+------+-----
+ 1 | t | 1
+ 2 | f | 0
+(2 rows)
+
+-- NULL::boolean
+SELECT id, COUNT(*) OVER w as cnt
+FROM rpr_bool
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (N+)
+ DEFINE N AS NULL::boolean
+)
+ORDER BY id;
+ id | cnt
+----+-----
+ 1 | 0
+ 2 | 0
+(2 rows)
+
+DROP TABLE rpr_bool;
+-- Complex expressions
+CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
+INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
+-- CASE expression
+SELECT id, val1, val2, COUNT(*) OVER w as cnt
+FROM rpr_complex
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (C+)
+ DEFINE C AS CASE WHEN val1 > 10 THEN val2 > 20 ELSE false END
+)
+ORDER BY id;
+ id | val1 | val2 | cnt
+----+------+------+-----
+ 1 | 10 | 20 | 0
+ 2 | 15 | 25 | 2
+ 3 | 20 | 30 | 0
+(3 rows)
+
+DROP TABLE rpr_complex;
+-- Pattern variable not in PATTERN (should be ignored)
+CREATE TABLE rpr_unused (id INT);
+INSERT INTO rpr_unused VALUES (1), (2);
+-- Extra DEFINE variable
+SELECT id, COUNT(*) OVER w as cnt
+FROM rpr_unused
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS id > 0, B AS id > 5 -- B not in pattern
+)
+ORDER BY id;
+ id | cnt
+----+-----
+ 1 | 2
+ 2 | 0
+(2 rows)
+
+DROP TABLE rpr_unused;
+-- ============================================================
+-- FRAME Options Tests
+-- ============================================================
+CREATE TABLE rpr_frame (id INT, val INT);
+INSERT INTO rpr_frame VALUES
+ (1, 10), (2, 10), (3, 10), -- Same val: 10
+ (4, 20), (5, 20), -- Same val: 20
+ (6, 30);
+-- Valid frame options
+-- ROWS: counts physical rows (1 FOLLOWING = next 1 physical row)
+-- Expected result: Each row can see 1 physical row ahead
+-- id=1,2,3 (val=10): can see next row -> cnt=2
+-- id=4,5 (val=20): can see next row -> cnt=2
+-- id=6 (val=30): no next row -> cnt=1
+-- Result: [2,2,2,2,2,1]
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY val
+ ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B?)
+ DEFINE A AS val >= 0, B AS val >= 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 10 | 2
+ 3 | 10 | 2
+ 4 | 20 | 2
+ 5 | 20 | 2
+ 6 | 30 | 1
+(6 rows)
+
+-- Invalid frame start positions
+-- Not starting at CURRENT ROW
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+ERROR: FRAME must start at CURRENT ROW when row pattern recognition is used
+LINE 5: ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+ ^
+DETAIL: Current frame starts with UNBOUNDED PRECEDING.
+HINT: Use: ROWS BETWEEN CURRENT ROW AND ...
+-- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
+-- EXCLUDE options
+-- EXCLUDE not permitted
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ EXCLUDE CURRENT ROW
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+LINE 6: EXCLUDE CURRENT ROW
+ ^
+DETAIL: Frame definition includes EXCLUDE CURRENT ROW.
+HINT: Remove the EXCLUDE clause from the window definition.
+-- Expected: ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+-- EXCLUDE GROUP not permitted
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ EXCLUDE GROUP
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+LINE 6: EXCLUDE GROUP
+ ^
+DETAIL: Frame definition includes EXCLUDE GROUP.
+HINT: Remove the EXCLUDE clause from the window definition.
+-- Expected: ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+-- EXCLUDE TIES not permitted
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ EXCLUDE TIES
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+LINE 6: EXCLUDE TIES
+ ^
+DETAIL: Frame definition includes EXCLUDE TIES.
+HINT: Remove the EXCLUDE clause from the window definition.
+-- Expected: ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+-- RANGE frame not starting at CURRENT ROW
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+LINE 5: RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWIN...
+ ^
+HINT: Use: ROWS instead
+-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+-- GROUPS frame not starting at CURRENT ROW
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
+LINE 5: GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI...
+ ^
+HINT: Use: ROWS instead
+-- Expected: ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
+-- Starting with N PRECEDING
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+ERROR: FRAME must start at CURRENT ROW when row pattern recognition is used
+LINE 5: ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING
+ ^
+DETAIL: Current frame starts with offset PRECEDING.
+HINT: Use: ROWS BETWEEN CURRENT ROW AND ...
+-- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
+-- Starting with N FOLLOWING
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+ERROR: FRAME must start at CURRENT ROW when row pattern recognition is used
+LINE 5: ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING
+ ^
+DETAIL: Current frame starts with offset FOLLOWING.
+HINT: Use: ROWS BETWEEN CURRENT ROW AND ...
+-- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
+-- Frame end bound edge cases
+-- End before start: CURRENT ROW AND 1 PRECEDING
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 1 PRECEDING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+ERROR: frame starting from current row cannot have preceding rows
+LINE 5: ROWS BETWEEN CURRENT ROW AND 1 PRECEDING
+ ^
+-- Expected: ERROR: frame starting from current row cannot have preceding rows
+-- End before start: CURRENT ROW AND UNBOUNDED PRECEDING
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+ERROR: frame end cannot be UNBOUNDED PRECEDING
+LINE 5: ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING
+ ^
+-- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
+-- Single row frame: CURRENT ROW AND CURRENT ROW
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND CURRENT ROW
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 1
+ 2 | 10 | 1
+ 3 | 10 | 1
+ 4 | 20 | 1
+ 5 | 20 | 1
+ 6 | 30 | 1
+(6 rows)
+
+-- Zero offset: CURRENT ROW AND 0 FOLLOWING (equivalent to CURRENT ROW)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 0 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 1
+ 2 | 10 | 1
+ 3 | 10 | 1
+ 4 | 20 | 1
+ 5 | 20 | 1
+ 6 | 30 | 1
+(6 rows)
+
+-- Large offset: CURRENT ROW AND 1000 FOLLOWING
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 1000 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 6
+ 2 | 10 | 5
+ 3 | 10 | 4
+ 4 | 20 | 3
+ 5 | 20 | 2
+ 6 | 30 | 1
+(6 rows)
+
+-- Maximum offset: CURRENT ROW AND 2147483646 FOLLOWING (INT_MAX - 1)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 2147483646 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 6
+ 2 | 10 | 5
+ 3 | 10 | 4
+ 4 | 20 | 3
+ 5 | 20 | 2
+ 6 | 30 | 1
+(6 rows)
+
+-- RANGE frame with RPR (not permitted)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY val
+ RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B?)
+ DEFINE A AS val >= 0, B AS val >= 0
+)
+ORDER BY id;
+ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+LINE 5: RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+ ^
+HINT: Use: ROWS instead
+-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+-- GROUPS frame with RPR (not permitted)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY val
+ GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B?)
+ DEFINE A AS val >= 0, B AS val >= 0
+)
+ORDER BY id;
+ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
+LINE 5: GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
+ ^
+HINT: Use: ROWS instead
+-- Expected: ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
+DROP TABLE rpr_frame;
+-- ============================================================
+-- PARTITION BY + FRAME Tests
+-- ============================================================
+-- Test PARTITION BY with RPR to ensure proper partitioning behavior
+CREATE TABLE rpr_partition (id INT, grp INT, val INT);
+INSERT INTO rpr_partition VALUES
+ (1, 1, 10), (2, 1, 20), (3, 1, 30),
+ (4, 2, 15), (5, 2, 25), (6, 2, 35);
+-- PARTITION BY with ROWS frame
+SELECT id, grp, val, COUNT(*) OVER w as cnt
+FROM rpr_partition
+WINDOW w AS (
+ PARTITION BY grp
+ ORDER BY val
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B+)
+ DEFINE A AS val >= 10, B AS val > 15
+)
+ORDER BY id;
+ id | grp | val | cnt
+----+-----+-----+-----
+ 1 | 1 | 10 | 3
+ 2 | 1 | 20 | 2
+ 3 | 1 | 30 | 0
+ 4 | 2 | 15 | 3
+ 5 | 2 | 25 | 2
+ 6 | 2 | 35 | 0
+(6 rows)
+
+-- Expected: Pattern matching should reset for each partition
+-- PARTITION BY with RANGE frame
+SELECT id, grp, val, COUNT(*) OVER w as cnt
+FROM rpr_partition
+WINDOW w AS (
+ PARTITION BY grp
+ ORDER BY val
+ RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B?)
+ DEFINE A AS val >= 10, B AS val >= 20
+)
+ORDER BY id;
+ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+LINE 6: RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+ ^
+HINT: Use: ROWS instead
+-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+DROP TABLE rpr_partition;
+-- ============================================================
+-- PATTERN Syntax Tests
+-- ============================================================
+CREATE TABLE rpr_pattern (id INT, val INT);
+INSERT INTO rpr_pattern VALUES
+ (1, 5), (2, 10), (3, 15), (4, 20), (5, 25),
+ (6, 30), (7, 35), (8, 40), (9, 45), (10, 50);
+-- Alternation (|)
+-- Multiple alternatives
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ | B+ | C+)
+ DEFINE A AS val > 35, B AS val BETWEEN 15 AND 35, C AS val < 15
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 5 | 2
+ 2 | 10 | 0
+ 3 | 15 | 5
+ 4 | 20 | 0
+ 5 | 25 | 0
+ 6 | 30 | 0
+ 7 | 35 | 0
+ 8 | 40 | 3
+ 9 | 45 | 0
+ 10 | 50 | 0
+(10 rows)
+
+-- Grouping
+-- Nested grouping with quantifier
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (((A B) C)+)
+ DEFINE A AS val > 10, B AS val > 20, C AS val > 30
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 5 | 0
+ 2 | 10 | 0
+ 3 | 15 | 0
+ 4 | 20 | 0
+ 5 | 25 | 6
+ 6 | 30 | 0
+ 7 | 35 | 0
+ 8 | 40 | 0
+ 9 | 45 | 0
+ 10 | 50 | 0
+(10 rows)
+
+-- Sequence
+-- Multi-element sequence
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B C D E)
+ DEFINE
+ A AS val < 15,
+ B AS val BETWEEN 15 AND 25,
+ C AS val BETWEEN 25 AND 35,
+ D AS val BETWEEN 35 AND 45,
+ E AS val >= 45
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 5 | 0
+ 2 | 10 | 0
+ 3 | 15 | 0
+ 4 | 20 | 0
+ 5 | 25 | 0
+ 6 | 30 | 0
+ 7 | 35 | 0
+ 8 | 40 | 0
+ 9 | 45 | 0
+ 10 | 50 | 0
+(10 rows)
+
+-- Complex combinations
+-- Alternation with grouping
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B) | (C D))
+ DEFINE A AS val < 20, B AS val >= 20, C AS val < 30, D AS val >= 30
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 5 | 0
+ 2 | 10 | 0
+ 3 | 15 | 2
+ 4 | 20 | 0
+ 5 | 25 | 2
+ 6 | 30 | 0
+ 7 | 35 | 0
+ 8 | 40 | 0
+ 9 | 45 | 0
+ 10 | 50 | 0
+(10 rows)
+
+-- Alternation + sequence + grouping
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (START (UP{2,} DOWN? | FLAT+) FINISH)
+ DEFINE
+ START AS val >= 0,
+ UP AS val > 20,
+ DOWN AS val <= 30,
+ FLAT AS val BETWEEN 25 AND 35,
+ FINISH AS val > 40
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 5 | 0
+ 2 | 10 | 0
+ 3 | 15 | 0
+ 4 | 20 | 7
+ 5 | 25 | 0
+ 6 | 30 | 0
+ 7 | 35 | 0
+ 8 | 40 | 0
+ 9 | 45 | 0
+ 10 | 50 | 0
+(10 rows)
+
+-- Nested alternation in groups
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B) (C | D))
+ DEFINE A AS val < 15, B AS val BETWEEN 15 AND 25, C AS val BETWEEN 25 AND 35, D AS val > 35
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 5 | 0
+ 2 | 10 | 0
+ 3 | 15 | 0
+ 4 | 20 | 2
+ 5 | 25 | 0
+ 6 | 30 | 0
+ 7 | 35 | 0
+ 8 | 40 | 0
+ 9 | 45 | 0
+ 10 | 50 | 0
+(10 rows)
+
+DROP TABLE rpr_pattern;
+-- ============================================================
+-- Quantifiers Tests
+-- ============================================================
+CREATE TABLE rpr_quant (id INT, val INT);
+INSERT INTO rpr_quant VALUES
+ (1, 10), (2, 20), (3, 30), (4, 40), (5, 50),
+ (6, 60), (7, 70), (8, 80), (9, 90), (10, 100);
+-- Basic greedy quantifiers
+-- * (zero or more)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A*)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 10
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- + (one or more)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 50
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 5
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- ? (zero or one)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A?)
+ DEFINE A AS val = 50
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 1
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Edge case quantifiers
+-- {0} is not allowed (min must be >= 1)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{0} B)
+ DEFINE A AS val > 1000, B AS val > 0
+)
+ORDER BY id;
+ERROR: quantifier bound must be between 1 and 2147483646
+LINE 6: PATTERN (A{0} B)
+ ^
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+-- {0,0} is not allowed (max must be >= 1)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{0,0} B)
+ DEFINE A AS val > 1000, B AS val > 0
+)
+ORDER BY id;
+ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
+LINE 6: PATTERN (A{0,0} B)
+ ^
+-- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
+-- {0,1} (equivalent to ?)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{0,1})
+ DEFINE A AS val = 50
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 1
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Exact quantifiers {n}
+-- {3} (representative exact quantifier)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{3})
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 3
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 3
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Range quantifiers {n,}
+-- {2,} (representative n or more)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2,})
+ DEFINE A AS val > 40
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 6
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Upper bound quantifiers {,m}
+-- {,3} (representative up to m)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,3})
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 3
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 3
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 1
+(10 rows)
+
+-- Range quantifiers {n,m}
+-- {3,7} (representative range)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{3,7})
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 7
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 3
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+DROP TABLE rpr_quant;
+-- Reluctant quantifiers (not yet supported)
+CREATE TABLE rpr_reluctant (id INT, val INT);
+INSERT INTO rpr_reluctant VALUES (1, 10), (2, 20), (3, 30);
+-- *? (zero or more, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A*?)
+ DEFINE A AS val > 0
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 6: PATTERN (A*?)
+ ^
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+-- +? (one or more, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+?)
+ DEFINE A AS val > 0
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 6: PATTERN (A+?)
+ ^
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+-- ?? (zero or one, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A??)
+ DEFINE A AS val > 0
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 6: PATTERN (A??)
+ ^
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+-- {n,}? (n or more, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2,}?)
+ DEFINE A AS val > 0
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 6: PATTERN (A{2,}?)
+ ^
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+-- {n,m}? (n to m, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{1,3}?)
+ DEFINE A AS val > 0
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 6: PATTERN (A{1,3}?)
+ ^
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+-- {n}? (exactly n, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2}?)
+ DEFINE A AS val > 0
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 6: PATTERN (A{2}?)
+ ^
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+-- {,m}? (up to m, reluctant) - COMPLETELY UNTESTED RULE!
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,3}?)
+ DEFINE A AS val > 0
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 6: PATTERN (A{,3}?)
+ ^
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+-- Invalid reluctant patterns (wrong token after quantifier)
+-- {2}+ (should be {2}? not {2}+)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2}+)
+ DEFINE A AS val > 0
+);
+ERROR: syntax error at or near "+"
+LINE 6: PATTERN (A{2}+)
+ ^
+-- Expected: ERROR: syntax error at or near "+"
+-- {2,}* (should be {2,}? not {2,}*)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2,}*)
+ DEFINE A AS val > 0
+);
+ERROR: syntax error at or near "*"
+LINE 6: PATTERN (A{2,}*)
+ ^
+-- Expected: ERROR: syntax error at or near "*"
+-- {,3}* (should be {,3}? not {,3}*)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,3}*)
+ DEFINE A AS val > 0
+);
+ERROR: syntax error at or near "*"
+LINE 6: PATTERN (A{,3}*)
+ ^
+-- Expected: ERROR: syntax error at or near "*"
+-- {1,3}+ (should be {1,3}? not {1,3}+)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{1,3}+)
+ DEFINE A AS val > 0
+);
+ERROR: syntax error at or near "+"
+LINE 6: PATTERN (A{1,3}+)
+ ^
+-- Expected: ERROR: syntax error at or near "+"
+-- Boundary errors in reluctant quantifiers
+-- {-1}? (negative bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{-1}?)
+ DEFINE A AS val > 0
+);
+ERROR: syntax error at or near "-"
+LINE 6: PATTERN (A{-1}?)
+ ^
+-- Expected: ERROR: syntax error at or near "-"
+-- {2147483647}? (INT_MAX)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2147483647}?)
+ DEFINE A AS val > 0
+);
+ERROR: quantifier bound must be between 1 and 2147483646
+LINE 6: PATTERN (A{2147483647}?)
+ ^
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+-- {-1,}? (negative lower bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{-1,}?)
+ DEFINE A AS val > 0
+);
+ERROR: syntax error at or near "-"
+LINE 6: PATTERN (A{-1,}?)
+ ^
+-- Expected: ERROR: syntax error at or near "-"
+-- {2147483647,}? (INT_MAX lower bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2147483647,}?)
+ DEFINE A AS val > 0
+);
+ERROR: quantifier bound must be between 0 and 2147483646
+LINE 6: PATTERN (A{2147483647,}?)
+ ^
+-- Expected: ERROR: quantifier bound must be between 0 and 2147483646
+-- {,0}? (zero upper bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,0}?)
+ DEFINE A AS val > 0
+);
+ERROR: quantifier bound must be between 1 and 2147483646
+LINE 6: PATTERN (A{,0}?)
+ ^
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+-- {,2147483647}? (INT_MAX upper bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,2147483647}?)
+ DEFINE A AS val > 0
+);
+ERROR: quantifier bound must be between 1 and 2147483646
+LINE 6: PATTERN (A{,2147483647}?)
+ ^
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+-- {-1,3}? (negative lower in range)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{-1,3}?)
+ DEFINE A AS val > 0
+);
+ERROR: syntax error at or near "-"
+LINE 6: PATTERN (A{-1,3}?)
+ ^
+-- Expected: ERROR: syntax error at or near "-"
+-- {1,2147483647}? (INT_MAX upper in range)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{1,2147483647}?)
+ DEFINE A AS val > 0
+);
+ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
+LINE 6: PATTERN (A{1,2147483647}?)
+ ^
+-- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
+-- {5,3}? (min > max)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{5,3}?)
+ DEFINE A AS val > 0
+);
+ERROR: quantifier minimum bound must not exceed maximum
+LINE 6: PATTERN (A{5,3}?)
+ ^
+-- Expected: ERROR: quantifier minimum bound must not exceed maximum
+-- Token-separated reluctant quantifiers (space between quantifier and ?)
+-- These may be tokenized differently by the lexer
+-- * ? (token separated)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A* ?)
+ DEFINE A AS val > 0
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 6: PATTERN (A* ?)
+ ^
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+-- + ? (token separated)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ ?)
+ DEFINE A AS val > 0
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 6: PATTERN (A+ ?)
+ ^
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+-- {2,} ? (token separated)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2,} ?)
+ DEFINE A AS val > 0
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 6: PATTERN (A{2,} ?)
+ ^
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+-- Invalid token combinations
+-- * + (invalid combination)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A* +)
+ DEFINE A AS val > 0
+);
+ERROR: syntax error at or near "+"
+LINE 6: PATTERN (A* +)
+ ^
+-- Expected: ERROR: syntax error at or near "+"
+-- + * (invalid combination)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ *)
+ DEFINE A AS val > 0
+);
+ERROR: syntax error at or near "*"
+LINE 6: PATTERN (A+ *)
+ ^
+-- Expected: ERROR: syntax error at or near "*"
+-- ? ? (invalid combination)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A? ?)
+ DEFINE A AS val > 0
+);
+ERROR: reluctant quantifiers are not yet supported
+LINE 6: PATTERN (A? ?)
+ ^
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+DROP TABLE rpr_reluctant;
+-- Quantifier boundary conditions
+CREATE TABLE rpr_bounds (id INT);
+INSERT INTO rpr_bounds VALUES (1), (2);
+-- min > max
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{5,3})
+ DEFINE A AS id > 0
+);
+ERROR: quantifier minimum bound must not exceed maximum
+LINE 6: PATTERN (A{5,3})
+ ^
+-- Expected: ERROR: quantifier minimum bound must not exceed maximum
+-- Large bounds
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{1000,2000})
+ DEFINE A AS id > 0
+);
+ count
+-------
+ 0
+ 0
+(2 rows)
+
+-- Very large bound
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{100000})
+ DEFINE A AS id > 0
+);
+ count
+-------
+ 0
+ 0
+(2 rows)
+
+-- INT_MAX - 1 = 2147483646 (at limit)
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2147483646})
+ DEFINE A AS id > 0
+);
+ count
+-------
+ 0
+ 0
+(2 rows)
+
+-- INT_MAX = 2147483647 (over limit)
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2147483647})
+ DEFINE A AS id > 0
+);
+ERROR: quantifier bound must be between 1 and 2147483646
+LINE 6: PATTERN (A{2147483647})
+ ^
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+-- {n,} boundary errors
+-- Negative lower bound in {n,}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{-1,})
+ DEFINE A AS id > 0
+);
+ERROR: syntax error at or near "-"
+LINE 6: PATTERN (A{-1,})
+ ^
+-- Expected: ERROR: syntax error at or near "-"
+-- INT_MAX in {n,}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2147483647,})
+ DEFINE A AS id > 0
+);
+ERROR: quantifier bound must be between 0 and 2147483646
+LINE 6: PATTERN (A{2147483647,})
+ ^
+-- Expected: ERROR: quantifier bound must be between 0 and 2147483646
+-- {,m} boundary errors
+-- Zero upper bound in {,m}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,0})
+ DEFINE A AS id > 0
+);
+ERROR: quantifier bound must be between 1 and 2147483646
+LINE 6: PATTERN (A{,0})
+ ^
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+-- INT_MAX in {,m}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,2147483647})
+ DEFINE A AS id > 0
+);
+ERROR: quantifier bound must be between 1 and 2147483646
+LINE 6: PATTERN (A{,2147483647})
+ ^
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+DROP TABLE rpr_bounds;
+-- ============================================================
+-- Navigation Functions Tests (PREV / NEXT)
+-- ============================================================
+CREATE TABLE rpr_nav (id INT, val INT);
+INSERT INTO rpr_nav VALUES
+ (1, 10), (2, 20), (3, 15), (4, 25), (5, 30);
+-- PREV function - reference previous row in pattern
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE
+ A AS val > 0,
+ B AS val > PREV(val)
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 0
+ 3 | 15 | 3
+ 4 | 25 | 0
+ 5 | 30 | 0
+(5 rows)
+
+-- NEXT function - reference next row in pattern
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ B)
+ DEFINE
+ A AS val < NEXT(val),
+ B AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 0
+ 3 | 15 | 3
+ 4 | 25 | 0
+ 5 | 30 | 0
+(5 rows)
+
+-- Combined PREV and NEXT
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B C)
+ DEFINE
+ A AS val > 0,
+ B AS val > PREV(val) AND val < NEXT(val),
+ C AS val > PREV(val)
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 15 | 3
+ 4 | 25 | 0
+ 5 | 30 | 0
+(5 rows)
+
+DROP TABLE rpr_nav;
+-- ============================================================
+-- SKIP TO / INITIAL Tests
+-- ============================================================
+CREATE TABLE rpr_skip (id INT, val INT);
+INSERT INTO rpr_skip VALUES
+ (1, 1), (2, 2), (3, 3), (4, 4), (5, 5),
+ (6, 6), (7, 7), (8, 8);
+-- SKIP TO NEXT ROW
+-- SKIP TO NEXT ROW
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_skip
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C)
+ DEFINE A AS val > 0, B AS val > 2, C AS val > 4
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 1 | 0
+ 2 | 2 | 0
+ 3 | 3 | 3
+ 4 | 4 | 3
+ 5 | 5 | 3
+ 6 | 6 | 3
+ 7 | 7 | 0
+ 8 | 8 | 0
+(8 rows)
+
+-- SKIP PAST LAST ROW
+-- SKIP PAST LAST ROW
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_skip
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS val > 0, B AS val > 2, C AS val > 4
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 1 | 0
+ 2 | 2 | 0
+ 3 | 3 | 3
+ 4 | 4 | 0
+ 5 | 5 | 0
+ 6 | 6 | 3
+ 7 | 7 | 0
+ 8 | 8 | 0
+(8 rows)
+
+-- Default behavior (should be SKIP PAST LAST ROW)
+-- No SKIP TO clause (default)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_skip
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B)
+ DEFINE A AS val > 0, B AS val > 1
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 1 | 2
+ 2 | 2 | 0
+ 3 | 3 | 2
+ 4 | 4 | 0
+ 5 | 5 | 2
+ 6 | 6 | 0
+ 7 | 7 | 2
+ 8 | 8 | 0
+(8 rows)
+
+-- Compare default with explicit PAST LAST ROW
+-- Results should be identical
+WITH default_skip AS (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_skip
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B C)
+ DEFINE A AS val > 0, B AS val > 2, C AS val > 4
+ )
+),
+explicit_skip AS (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_skip
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS val > 0, B AS val > 2, C AS val > 4
+ )
+)
+SELECT 'default' as type, * FROM default_skip
+UNION ALL
+SELECT 'explicit' as type, * FROM explicit_skip
+ORDER BY type, id;
+ type | id | val | cnt
+----------+----+-----+-----
+ default | 1 | 1 | 0
+ default | 2 | 2 | 0
+ default | 3 | 3 | 3
+ default | 4 | 4 | 0
+ default | 5 | 5 | 0
+ default | 6 | 6 | 3
+ default | 7 | 7 | 0
+ default | 8 | 8 | 0
+ explicit | 1 | 1 | 0
+ explicit | 2 | 2 | 0
+ explicit | 3 | 3 | 3
+ explicit | 4 | 4 | 0
+ explicit | 5 | 5 | 0
+ explicit | 6 | 6 | 3
+ explicit | 7 | 7 | 0
+ explicit | 8 | 8 | 0
+(16 rows)
+
+DROP TABLE rpr_skip;
+-- INITIAL clause
+CREATE TABLE rpr_init (id INT, val INT);
+INSERT INTO rpr_init VALUES (1, 10), (2, 20), (3, 30), (4, 40);
+-- Explicit INITIAL
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_init
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 4
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+(4 rows)
+
+-- Implicit INITIAL (default)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_init
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 4
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+(4 rows)
+
+DROP TABLE rpr_init;
+-- SEEK
+CREATE TABLE rpr_seek (id INT, val INT);
+INSERT INTO rpr_seek VALUES (1, 10);
+-- SEEK keyword
+SELECT COUNT(*) OVER w
+FROM rpr_seek
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ SEEK
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+ERROR: SEEK is not supported
+LINE 6: SEEK
+ ^
+HINT: Use INITIAL instead.
+-- Expected: ERROR: SEEK is not supported
+-- HINT: Use INITIAL instead.
+DROP TABLE rpr_seek;
+-- ============================================================
+-- Serialization/Deserialization Tests
+-- ============================================================
+-- View creation and deparsing
+CREATE TABLE rpr_serial (id INT, val INT);
+INSERT INTO rpr_serial VALUES
+ (1, 10), (2, 20), (3, 15), (4, 25), (5, 30);
+-- Simple pattern
+CREATE VIEW rpr_serial_v1 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+-- Verify view works (tests deserialization)
+SELECT * FROM rpr_serial_v1 ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 5
+ 2 | 20 | 0
+ 3 | 15 | 0
+ 4 | 25 | 0
+ 5 | 30 | 0
+(5 rows)
+
+-- Verify deparsing
+SELECT pg_get_viewdef('rpr_serial_v1'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS cnt +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a+) +
+ DEFINE +
+ a AS (val > 0) );
+(1 row)
+
+DROP VIEW rpr_serial_v1;
+-- Complex pattern with alternation
+CREATE VIEW rpr_serial_v2 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ | B*)
+ DEFINE A AS val > 20, B AS val <= 20
+);
+SELECT * FROM rpr_serial_v2 ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 3 | 15 | 0
+ 4 | 25 | 2
+ 5 | 30 | 0
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v2'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS cnt +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a+ | b*) +
+ DEFINE +
+ a AS (val > 20), +
+ b AS (val <= 20) );
+(1 row)
+
+DROP VIEW rpr_serial_v2;
+-- Pattern with grouping and quantifiers
+CREATE VIEW rpr_serial_v3 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B){2,5} | C*)
+ DEFINE
+ A AS val > 10,
+ B AS val > 20,
+ C AS val <= 10
+);
+SELECT * FROM rpr_serial_v3 ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 1
+ 2 | 20 | 0
+ 3 | 15 | 0
+ 4 | 25 | 0
+ 5 | 30 | 0
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v3'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS cnt +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ((a b){2,5} | c*) +
+ DEFINE +
+ a AS (val > 10), +
+ b AS (val > 20), +
+ c AS (val <= 10) );
+(1 row)
+
+DROP VIEW rpr_serial_v3;
+-- All features combined
+CREATE VIEW rpr_serial_v4 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (START (MID{1,3} | ALT+) FINISH)
+ DEFINE
+ START AS val > 5,
+ MID AS val BETWEEN 10 AND 25,
+ ALT AS val > 25,
+ FINISH AS val > 15
+);
+SELECT * FROM rpr_serial_v4 ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 5
+ 2 | 20 | 4
+ 3 | 15 | 3
+ 4 | 25 | 0
+ 5 | 30 | 0
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v4'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS cnt +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP TO NEXT ROW +
+ INITIAL +
+ PATTERN (start (mid{1,3} | alt+) finish) +
+ DEFINE +
+ start AS (val > 5), +
+ mid AS ((val >= 10) AND (val <= 25)), +
+ alt AS (val > 25), +
+ finish AS (val > 15) );
+(1 row)
+
+DROP VIEW rpr_serial_v4;
+-- Additional quantifiers for deparsing coverage
+-- ? quantifier (zero or one)
+CREATE VIEW rpr_serial_v5 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B?)
+ DEFINE A AS val > 10, B AS val > 20
+);
+SELECT * FROM rpr_serial_v5 ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 1
+ 3 | 15 | 2
+ 4 | 25 | 0
+ 5 | 30 | 1
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v5'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS cnt +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a b?) +
+ DEFINE +
+ a AS (val > 10), +
+ b AS (val > 20) );
+(1 row)
+
+DROP VIEW rpr_serial_v5;
+-- {n,} quantifier (n or more)
+CREATE VIEW rpr_serial_v6 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2,})
+ DEFINE A AS val > 15
+);
+SELECT * FROM rpr_serial_v6 ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 15 | 0
+ 4 | 25 | 2
+ 5 | 30 | 0
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v6'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS cnt +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a{2,}) +
+ DEFINE +
+ a AS (val > 15) );
+(1 row)
+
+DROP VIEW rpr_serial_v6;
+-- {n} quantifier (exactly n)
+CREATE VIEW rpr_serial_v7 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{3})
+ DEFINE A AS val > 0
+);
+SELECT * FROM rpr_serial_v7 ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 3 | 15 | 0
+ 4 | 25 | 0
+ 5 | 30 | 0
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v7'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS cnt +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a{3}) +
+ DEFINE +
+ a AS (val > 0) );
+(1 row)
+
+DROP VIEW rpr_serial_v7;
+-- Nested ALT pattern (tests deparse of complex nested structure)
+CREATE VIEW rpr_serial_v8 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (((A+ B) | C) D | A B C)
+ DEFINE A AS val <= 15, B AS val <= 25, C AS val <= 30, D AS val > 30
+);
+SELECT * FROM rpr_serial_v8 ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 3 | 15 | 0
+ 4 | 25 | 0
+ 5 | 30 | 0
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v8'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS cnt +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (((a+ b) | c) d | a b c) +
+ DEFINE +
+ a AS (val <= 15), +
+ b AS (val <= 25), +
+ c AS (val <= 30), +
+ d AS (val > 30) );
+(1 row)
+
+DROP VIEW rpr_serial_v8;
+DROP TABLE rpr_serial;
+-- Materialized view (if supported)
+CREATE TABLE rpr_mview (id INT, val INT);
+INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30);
+CREATE MATERIALIZED VIEW rpr_mview_v1 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_mview
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+SELECT * FROM rpr_mview_v1 ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 3 | 30 | 0
+(3 rows)
+
+SELECT pg_get_viewdef('rpr_mview_v1'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS cnt +
+ FROM rpr_mview +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a+) +
+ DEFINE +
+ a AS (val > 0) );
+(1 row)
+
+-- Refresh test
+REFRESH MATERIALIZED VIEW rpr_mview_v1;
+SELECT * FROM rpr_mview_v1 ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 3 | 30 | 0
+(3 rows)
+
+DROP MATERIALIZED VIEW rpr_mview_v1;
+DROP TABLE rpr_mview;
+-- Prepared statements (tests outfuncs.c / readfuncs.c)
+CREATE TABLE rpr_prep (id INT, val INT);
+INSERT INTO rpr_prep VALUES (1, 10), (2, 20), (3, 30);
+-- Simple prepared statement
+PREPARE rpr_prep_simple AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_prep
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+EXECUTE rpr_prep_simple;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 3 | 30 | 0
+(3 rows)
+
+EXECUTE rpr_prep_simple;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 3 | 30 | 0
+(3 rows)
+
+DEALLOCATE rpr_prep_simple;
+-- Prepared statement with parameters
+PREPARE rpr_prep_param(int) AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_prep
+WHERE id <= $1
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 10
+)
+ORDER BY id;
+EXECUTE rpr_prep_param(2);
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 1
+(2 rows)
+
+EXECUTE rpr_prep_param(3);
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 2
+ 3 | 30 | 0
+(3 rows)
+
+DEALLOCATE rpr_prep_param;
+-- Complex prepared statement
+PREPARE rpr_prep_complex AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_prep
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A B){1,2} | C+)
+ DEFINE
+ A AS val > 5,
+ B AS val > 15,
+ C AS val <= 15
+)
+ORDER BY id;
+EXECUTE rpr_prep_complex;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 2
+ 3 | 30 | 0
+(3 rows)
+
+EXECUTE rpr_prep_complex;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 2
+ 3 | 30 | 0
+(3 rows)
+
+DEALLOCATE rpr_prep_complex;
+DROP TABLE rpr_prep;
+-- CTE and Subquery (tests copyfuncs.c)
+CREATE TABLE rpr_copy (id INT, val INT);
+INSERT INTO rpr_copy VALUES (1, 10), (2, 20), (3, 30), (4, 40);
+-- Simple CTE
+WITH rpr_cte AS (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_copy
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+)
+SELECT * FROM rpr_cte ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 4
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+(4 rows)
+
+-- CTE with multiple references (forces node copy)
+WITH rpr_cte AS (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_copy
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 15
+ )
+)
+SELECT c1.id, c1.cnt as cnt1, c2.cnt as cnt2
+FROM rpr_cte c1
+JOIN rpr_cte c2 ON c1.id = c2.id
+ORDER BY c1.id;
+ id | cnt1 | cnt2
+----+------+------
+ 1 | 0 | 0
+ 2 | 3 | 3
+ 3 | 0 | 0
+ 4 | 0 | 0
+(4 rows)
+
+-- Subquery in FROM clause
+SELECT *
+FROM (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_copy
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B?)
+ DEFINE A AS val > 10, B AS val > 20
+ )
+) sub
+WHERE cnt > 0
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+(0 rows)
+
+-- Nested subqueries
+SELECT *
+FROM (
+ SELECT *
+ FROM (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_copy
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val >= 10
+ )
+ ) inner_sub
+ WHERE cnt > 0
+) outer_sub
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 4
+(1 row)
+
+DROP TABLE rpr_copy;
+-- DISTINCT and set operations (tests equalfuncs.c)
+CREATE TABLE rpr_equal (id INT, val INT);
+INSERT INTO rpr_equal VALUES (1, 10), (2, 20), (3, 10), (4, 20);
+-- DISTINCT with RPR
+SELECT DISTINCT cnt
+FROM (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WINDOW w AS (
+ ORDER BY val
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+) sub
+ORDER BY cnt;
+ cnt
+-----
+ 1
+ 2
+ 3
+ 4
+(4 rows)
+
+-- UNION with RPR in both sides
+SELECT id, val, cnt FROM (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WHERE val = 10
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+) sub1
+UNION
+SELECT id, val, cnt FROM (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WHERE val = 20
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+) sub2
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 2
+ 3 | 10 | 0
+ 4 | 20 | 0
+(4 rows)
+
+-- UNION ALL
+SELECT id, cnt FROM (
+ SELECT id, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 10
+ )
+) sub
+UNION ALL
+SELECT id, cnt FROM (
+ SELECT id, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (B+)
+ DEFINE B AS val <= 10
+ )
+) sub
+ORDER BY id, cnt;
+ id | cnt
+----+-----
+ 1 | 0
+ 1 | 1
+ 2 | 0
+ 2 | 1
+ 3 | 0
+ 3 | 1
+ 4 | 0
+ 4 | 1
+(8 rows)
+
+-- INTERSECT
+SELECT id, cnt FROM (
+ SELECT id, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WHERE id <= 3
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+) sub1
+INTERSECT
+SELECT id, cnt FROM (
+ SELECT id, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WHERE id >= 2
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+) sub2
+ORDER BY id;
+ id | cnt
+----+-----
+ 3 | 0
+(1 row)
+
+DROP TABLE rpr_equal;
+-- View with multiple window definitions
+CREATE TABLE rpr_multiwin (id INT, val INT);
+INSERT INTO rpr_multiwin VALUES (1, 10), (2, 20), (3, 30);
+CREATE VIEW rpr_multiwin_v AS
+SELECT
+ id,
+ val,
+ COUNT(*) OVER w1 as cnt1,
+ COUNT(*) OVER w2 as cnt2
+FROM rpr_multiwin
+WINDOW
+ w1 AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 15
+ ),
+ w2 AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (B*)
+ DEFINE B AS val <= 15
+ );
+SELECT * FROM rpr_multiwin_v ORDER BY id;
+ id | val | cnt1 | cnt2
+----+-----+------+------
+ 1 | 10 | 0 | 1
+ 2 | 20 | 2 | 0
+ 3 | 30 | 0 | 0
+(3 rows)
+
+SELECT pg_get_viewdef('rpr_multiwin_v'::regclass);
+ pg_get_viewdef
+-------------------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w1 AS cnt1, +
+ count(*) OVER w2 AS cnt2 +
+ FROM rpr_multiwin +
+ WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a+) +
+ DEFINE +
+ a AS (val > 15) ), w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (b*) +
+ DEFINE +
+ b AS (val <= 15) );
+(1 row)
+
+DROP VIEW rpr_multiwin_v;
+DROP TABLE rpr_multiwin;
+-- ============================================================
+-- Error Cases Tests
+-- ============================================================
+DROP TABLE IF EXISTS rpr_err;
+CREATE TABLE rpr_err (id INT, val INT);
+INSERT INTO rpr_err VALUES (1, 10), (2, 20);
+-- Syntax errors
+-- Invalid quantifier syntax
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+!)
+ DEFINE A AS val > 0
+);
+ERROR: unsupported quantifier "+!"
+LINE 6: PATTERN (A+!)
+ ^
+HINT: Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+-- Expected: Syntax error
+-- Unmatched parentheses
+SET client_min_messages = NOTICE;
+DO $$
+BEGIN
+ EXECUTE 'SELECT COUNT(*) OVER w FROM rpr_err WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A B) DEFINE A AS val > 0, B AS val > 10)';
+ RAISE NOTICE 'Unmatched parentheses: UNEXPECTED SUCCESS';
+EXCEPTION
+ WHEN syntax_error THEN
+ RAISE NOTICE 'Unmatched parentheses: EXPECTED ERROR - %', SQLERRM;
+ WHEN OTHERS THEN
+ RAISE NOTICE 'Unmatched parentheses: UNEXPECTED ERROR - %', SQLERRM;
+END $$;
+NOTICE: Unmatched parentheses: EXPECTED ERROR - syntax error at or near "AS"
+SET client_min_messages = WARNING;
+-- Empty DEFINE
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE
+);
+ERROR: syntax error at or near ")"
+LINE 8: );
+ ^
+-- Expected: Syntax error
+-- Empty PATTERN
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ()
+ DEFINE A AS val > 0
+);
+ERROR: syntax error at or near ")"
+LINE 6: PATTERN ()
+ ^
+-- Expected: Syntax error
+-- DEFINE without PATTERN (PATTERN and DEFINE must be used together)
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ DEFINE A AS val > 0
+);
+ERROR: syntax error at or near "DEFINE"
+LINE 6: DEFINE A AS val > 0
+ ^
+-- Expected: Syntax error
+-- Qualified column references (NOT SUPPORTED)
+-- Pattern variables in DEFINE clause cannot use qualified references (A.price)
+-- This gives a confusing error about missing FROM-clause entry
+-- Qualified reference in DEFINE clause
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS A.val > 0
+);
+ERROR: missing FROM-clause entry for table "a"
+LINE 7: DEFINE A AS A.val > 0
+ ^
+-- Expected: ERROR: missing FROM-clause entry for table "a"
+-- Semantic errors
+-- Undefined column in DEFINE
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS nonexistent_column > 0
+);
+ERROR: column "nonexistent_column" does not exist
+LINE 7: DEFINE A AS nonexistent_column > 0
+ ^
+-- Expected: ERROR: column "nonexistent_column" does not exist
+-- Type mismatch
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 'string'
+);
+ERROR: invalid input syntax for type integer: "string"
+LINE 7: DEFINE A AS val > 'string'
+ ^
+-- Expected: ERROR: invalid input syntax for type integer: "string"
+-- Aggregate function in DEFINE (if not allowed)
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS COUNT(*) > 0
+);
+ERROR: aggregate functions are not allowed in DEFINE
+LINE 7: DEFINE A AS COUNT(*) > 0
+ ^
+-- Expected: ERROR or works depending on implementation
+-- Subquery in DEFINE (NOT SUPPORTED)
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > (SELECT max(val) FROM rpr_err)
+);
+ERROR: cannot use subquery in DEFINE expression
+LINE 7: DEFINE A AS val > (SELECT max(val) FROM rpr_err)
+ ^
+-- Expected: ERROR: cannot use subquery in DEFINE expression
+-- Edge cases
+-- Pattern variable not used (should work, extra vars ignored)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0, B AS val > 5, C AS val > 10
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 0
+(2 rows)
+
+DROP TABLE rpr_err;
+-- NULL handling
+CREATE TABLE rpr_null (id INT, val INT);
+INSERT INTO rpr_null VALUES (1, 10), (2, NULL), (3, 30);
+-- NULL in DEFINE expression
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_null
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 15
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | | 0
+ 3 | 30 | 1
+(3 rows)
+
+-- IS NULL in DEFINE
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_null
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (N+)
+ DEFINE N AS val IS NULL
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | | 1
+ 3 | 30 | 0
+(3 rows)
+
+-- IS NOT NULL in DEFINE
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_null
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (NN+)
+ DEFINE NN AS val IS NOT NULL
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 1
+ 2 | | 0
+ 3 | 30 | 1
+(3 rows)
+
+DROP TABLE rpr_null;
+-- ============================================================
+-- Pattern Optimization Tests
+-- ============================================================
+-- Tests for pattern optimization in optimizer/plan/rpr.c
+-- Use EXPLAIN to verify optimized pattern (shown as "Pattern: ...")
+CREATE TABLE rpr_plan (id INT, val INT);
+INSERT INTO rpr_plan VALUES
+ (1, 10), (2, 20), (3, 30), (4, 40), (5, 50),
+ (6, 60), (7, 70), (8, 80), (9, 90), (10, 100);
+-- Consecutive VAR merge: A A A -> a{3}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A A A) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{3}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Consecutive VAR merge: A{2} A{3} -> a{5}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2} A{3}) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{5}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Consecutive VAR merge: A+ A* -> a+
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ A*) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Consecutive VAR merge: A A+ -> a{2,}
+-- Tests line 251: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars
+-- prev: A{1,1} (finite), child: A+ (infinite) triggers line 251 evaluation
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A A+) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{2,}"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (((A B){5}) ((A B){10})) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a b){15}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Consecutive GROUP merge with unbounded: (A B)+ (A B)+ -> (a b){2,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B)+ (A B)+) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b'){2,}"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,}
+-- Tests line 325: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups
+-- prev: (A B){2,2} (finite), child: (A B)+ (infinite) triggers line 325 evaluation
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B){2} (A B)+) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b'){3,}"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- PREFIX merge: A B (A B)+ -> (a b){2,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B (A B)+) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b'){2,}"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- PREFIX and SUFFIX merge: A B (A B)+ A B -> (a b){3,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B (A B)+ A B) DEFINE A AS val <= 40, B AS val > 40);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b'){3,}"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Flatten nested: A ((B) (C)) -> a b c
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A ((B) (C))) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b c
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- ALT flatten: (A | (B | C))+ -> (a | b | c)+
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | (B | C))+) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b | c)+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- ALT deduplicate: (A | B | A) -> (a | b)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B | A)+) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b)+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Quantifier multiply: (A{2}){3} -> a{6}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2}){3}) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{6}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9}
+-- outer exact, child range - optimization applies
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2,3}){3}) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{6,9}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Quantifier NO multiply: (A{2}){2,3} stays as (a{2}){2,3}
+-- outer range - gaps would occur (4,6 not 4,5,6), no optimization
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2}){2,3}) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a{2}){2,3}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Quantifier NO multiply: (A{2}){2,} stays as (a{2}){2,}
+-- outer unbounded - gaps would occur (4,6,8,... not 4,5,6,...), no optimization
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2}){2,}) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a{2}){2,}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Quantifier multiply: (A){2,} -> a{2,}
+-- child exact 1 - no gaps, optimization applies
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A){2,}) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{2,}"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Quantifier multiply: (A)+ -> a+
+-- child exact 1 - no gaps, optimization applies
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A)+) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Quantifier NO multiply: (A{2}){3,5} stays as (a{2}){3,5}
+-- outer range, child exact > 1 - gaps would occur (6,8,10 not 6,7,8,9,10)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2}){3,5}) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a{2}){3,5}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Quantifier NO multiply: (A{2,3}){2,3} stays as (a{2,3}){2,3}
+-- outer range, child range - gaps possible (e.g., (A{4,5}){2,3} misses 11)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2,3}){2,3}) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a{2,3}){2,3}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Nested unbounded: (A*)* -> a*
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A*)*) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a*"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Nested unbounded: (A+)* -> a*
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A+)*) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a*"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Nested unbounded: (A+)+ -> a+
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A+)+) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Unwrap GROUP{1,1}: (A) -> a
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A)) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Unwrap GROUP{1,1}: (A B) -> a b
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B)) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Combined optimization: A A (B B)+ B B C C C -> a{2} (b{2}){2,} c{3}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A A (B B)+ B B C C C)
+ DEFINE A AS val <= 20, B AS val > 20 AND val <= 70, C AS val > 70);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{2} (b{2}){2,} c{3}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Consecutive GROUP merge with unbounded: (A+) (A+) -> a{2,}
+-- Tests mergeConsecutiveGroups with child->max == INF
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A+) (A+)) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{2,}"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Consecutive GROUP merge finite: (A{10}){20} -> a{200}
+-- Tests mergeConsecutiveGroups with both finite
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{10}){20}) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{200}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Different GROUP prevents merge: (A B){2} (C D){3}
+-- Tests mergeConsecutiveGroups flush previous
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B){2} (C D){3})
+ DEFINE A AS val <= 25, B AS val > 25 AND val <= 50,
+ C AS val > 50 AND val <= 75, D AS val > 75);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a b){2} (c d){3}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Different children count prevents merge: (A B)+ (A B C)+
+-- Tests rprPatternChildrenEqual length check
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B)+ (A B C)+)
+ DEFINE A AS val <= 33, B AS val > 33 AND val <= 66, C AS val > 66);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b')+" (a b c)+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- PREFIX only merge: A B (A B)+ -> (a b){2,}
+-- Tests mergeGroupPrefixSuffix: absorb preceding elements into GROUP min
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B (A B)+) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b'){2,}"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- SUFFIX only merge: (A B)+ A B -> (a b){2,}
+-- Tests mergeGroupPrefixSuffix: absorb following elements into GROUP min
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B)+ A B) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b'){2,}"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Multiple SUFFIX absorption with skipUntil: (A B)+ A B A B C
+-- Tests mergeGroupPrefixSuffix: skip absorbed suffix elements
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B)+ A B A B C)
+ DEFINE A AS val <= 50, B AS val > 50 AND val <= 75, C AS val > 75);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b'){3,}" c
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- PREFIX merge with remaining prefix: A B C D (C D)+
+-- Tests mergeGroupPrefixSuffix: trimmed list reconstruction
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B C D (C D)+)
+ DEFINE A AS val <= 25, B AS val > 25 AND val <= 50,
+ C AS val > 50 AND val <= 75, D AS val > 75);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b (c d){2,}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- PREFIX merge with quantifiers: A B* (A B*)+ -> (a b*){2,}
+-- Tests mergeGroupPrefixSuffix: quantifier comparison in rprPatternEqual
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B* (A B*)+)
+ DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a b*){2,}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- PREFIX merge with multiple quantifiers: A+ B* C? (A+ B* C?)+ -> (a+ b* c?){2,}
+-- Tests mergeGroupPrefixSuffix: complex quantifier patterns
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ B* C? (A+ B* C?)+)
+ DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a+" b* c?){2,}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- SUFFIX merge with quantifiers: (A B*)+ A B* -> (a b*){2,}
+-- Tests mergeGroupPrefixSuffix: suffix with quantifiers
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B*)+ A B*)
+ DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a b*){2,}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Unwrap GROUP{1,1}: ((A | B | C)) -> (a | b | c)
+-- Tests tryUnwrapGroup removing redundant outer GROUP
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B | C)) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b | c)
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- ============================================================
+-- Absorption Flag Display Tests
+-- ============================================================
+-- Tests absorption marker display in EXPLAIN output
+-- Markers: ' = branch element, " = judgment point
+-- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern)
+-- Simple VAR: A+ -> a+" (judgment point)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B)+) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b')+"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- ALT both absorbable: A+ | B+ -> (a+" | b+")
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ | B+) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a+" | b+")
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- ALT one absorbable: A+ | B -> (a+" | b)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ | B) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a+" | b)
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Sequence with absorbable start: A+ B -> a+" b
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Complex nested: ((A+ B) | C) D | A B C - deeply nested ALT
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (((A+ B) | C) D | A B C)
+ DEFINE A AS val <= 30, B AS val <= 60, C AS val <= 80, D AS val > 80);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: ((a+" b | c) d | a b c)
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Nested unbounded: (A+ | B)+ -> (a+" | b)+ (first iteration absorbable)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ | B)+)
+ DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a+" | b)+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- ALT inside unbounded GROUP: (A+ B | A B)* -> (a+" b | a b)* (first iteration absorbable)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ B | A B)*)
+ DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a+" b | a b)*
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (A B+) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Non-absorbable (no unbounded branch): (A | B){2,} -> (a | b){2,} (no markers)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A | B){2,}) DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b){2,}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Non-absorbable (SKIP TO NEXT ROW): A+ -> a+ (no markers)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW PATTERN (A+) DEFINE A AS val > 0);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Non-absorbable (limited frame): A+ -> a+ (no markers)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0);
+ QUERY PLAN
+----------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND '10'::bigint FOLLOWING)
+ Pattern: a+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- ============================================================
+-- Absorption Analysis Tests
+-- ============================================================
+-- Tests context absorption optimization (O(n^2) -> O(n))
+-- Files: rpr.c (computeAbsorbability)
+-- Simple Absorbable Pattern: A+ B
+-- Pattern starts with unbounded VAR
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 6
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Absorbable GROUP Pattern: (A B)+ C
+-- Pattern starts with unbounded GROUP
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+ C)
+ DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Non-Absorbable: Unbounded Not at Start
+-- Pattern: A B+ (unbounded not at start)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 6
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- ALT with Absorbable Branches
+-- Pattern: (A+ | B+) C - both branches absorbable
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A+ | B+) C)
+ DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 4
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- ALT with Mixed Branches
+-- Pattern: (A+ | B C) - only first branch absorbable
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A+ | B C)+)
+ DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 2
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Non-Absorbable: ALT Inside GROUP
+-- Pattern: (A | B){2,} - ALT inside unbounded GROUP
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B){2,})
+ DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 10
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Non-Absorbable: Nested Unbounded
+-- Pattern: ((A B)+ C)+ - nested GROUP structure
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A B)+ C)+)
+ DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Non-Absorbable: Unbounded Element Inside GROUP
+-- Pattern: (A B+){2,} - unbounded inside GROUP
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B+){2,})
+ DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Runtime Conditions: SKIP TO NEXT ROW
+-- Absorption disabled with SKIP TO NEXT ROW
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 6
+ 2 | 20 | 5
+ 3 | 30 | 4
+ 4 | 40 | 3
+ 5 | 50 | 2
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Runtime Conditions: Limited Frame
+-- Absorption disabled with limited frame end
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 5 FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 6
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- ============================================================
+-- Edge Case Tests
+-- ============================================================
+-- Tests boundary conditions and complex scenarios
+-- Empty Match Prevention
+-- Pattern that could match empty: A*
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A*)
+ DEFINE A AS val > 1000 -- Never matches
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- All Rows Match
+-- Pattern where every row matches
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val >= 0 -- Always true
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 10
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Large Quantifiers
+-- Pattern: A{100} (large exact quantifier)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{100})
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Pattern: A{10,20} (large range quantifier)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{10,20})
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 10
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Complex Multi-Level Nesting
+-- Pattern: (((A B) | C)+ D)+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((((A B) | C)+ D)+)
+ DEFINE A AS val <= 20, B AS val > 20 AND val <= 40,
+ C AS val > 40 AND val <= 60, D AS val > 60
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 3
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Long Alternation Chain
+-- Pattern: A | B | C | D | E (5-way ALT)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A | B | C | D | E)
+ DEFINE A AS val = 10, B AS val = 30, C AS val = 50,
+ D AS val = 70, E AS val = 90
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 1
+ 2 | 20 | 0
+ 3 | 30 | 1
+ 4 | 40 | 0
+ 5 | 50 | 1
+ 6 | 60 | 0
+ 7 | 70 | 1
+ 8 | 80 | 0
+ 9 | 90 | 1
+ 10 | 100 | 0
+(10 rows)
+
+-- Long Sequence
+-- Pattern: A B C D E F G H (8-element SEQ)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B C D E F G H)
+ DEFINE A AS val >= 10, B AS val >= 20, C AS val >= 30,
+ D AS val >= 40, E AS val >= 50, F AS val >= 60,
+ G AS val >= 70, H AS val >= 80
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 8
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Interleaved Quantifiers
+-- Pattern: A{2} B+ C{3,5} D* E{1,}
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2} B+ C{3,5} D* E{1,})
+ DEFINE A AS val > 0, B AS val > 0, C AS val > 0,
+ D AS val > 0, E AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 10
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- ============================================================
+-- Optimization Fallback Tests
+-- ============================================================
+-- Tests for optimization edge cases and fallback behavior
+CREATE TABLE rpr_fallback (id INT, val INT);
+INSERT INTO rpr_fallback VALUES (1, 10), (2, 20);
+-- Test: min quantifier overflow causes optimization fallback (min == max case)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2000000000}){2})
+ DEFINE A AS val > 0
+);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a{2000000000}){2}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_fallback
+(6 rows)
+
+-- Expected: Fallback - pattern not merged due to min overflow (4000000000 > INT32_MAX)
+-- Test: max-only quantifier overflow causes optimization fallback
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{1,2000000000}){2})
+ DEFINE A AS val > 0
+);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a{1,2000000000}){2}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_fallback
+(6 rows)
+
+-- Expected: Fallback - min OK (2*1=2), but max overflow (2*2000000000 > INT32_MAX)
+-- Test: max quantifier exceeds valid range (2147483647 = INT_MAX, limit is 2147483646)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2000000000,2147483647}){2})
+ DEFINE A AS val > 0
+);
+ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
+LINE 6: PATTERN ((A{2000000000,2147483647}){2})
+ ^
+-- Expected: ERROR at parse time before optimization
+-- Test: nested unbounded with large min causes overflow fallback
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2000000000,}){2000000000,})
+ DEFINE A AS val > 0
+);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a{2000000000,}"){2000000000,}
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_fallback
+(6 rows)
+
+-- Expected: Fallback - min overflow (2000000000 * 2000000000 > INT32_MAX)
+-- Test: prefix mismatch causes optimization fallback
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B (C D)+)
+ DEFINE A AS val > 0, B AS val > 5, C AS val > 10, D AS val > 15
+);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b (c d)+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_fallback
+(6 rows)
+
+-- Expected: Fallback - prefix elements don't match GROUP content
+DROP TABLE rpr_fallback;
+-- ============================================================
+-- Planner Integration Tests
+-- ============================================================
+-- Tests full planning pipeline and WindowAgg plan node creation
+-- Files: planner.c, createplan.c
+CREATE TABLE rpr_planner (id INT, category VARCHAR(10), val INT);
+INSERT INTO rpr_planner VALUES
+ (1, 'A', 10), (2, 'A', 20), (3, 'A', 30),
+ (4, 'B', 40), (5, 'B', 50), (6, 'B', 60),
+ (7, 'C', 70), (8, 'C', 80), (9, 'C', 90);
+-- Multiple Window Functions in Same Query
+SELECT id, category, val,
+ COUNT(*) OVER w1 as cnt1,
+ COUNT(*) OVER w2 as cnt2
+FROM rpr_planner
+WINDOW w1 AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+),
+w2 AS (
+ PARTITION BY category
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (B+)
+ DEFINE B AS val >= 40
+)
+ORDER BY id;
+ id | category | val | cnt1 | cnt2
+----+----------+-----+------+------
+ 1 | A | 10 | 9 | 0
+ 2 | A | 20 | 0 | 0
+ 3 | A | 30 | 0 | 0
+ 4 | B | 40 | 0 | 3
+ 5 | B | 50 | 0 | 0
+ 6 | B | 60 | 0 | 0
+ 7 | C | 70 | 0 | 3
+ 8 | C | 80 | 0 | 0
+ 9 | C | 90 | 0 | 0
+(9 rows)
+
+-- Window Function with PARTITION BY
+SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_planner
+WINDOW w AS (
+ PARTITION BY category
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY category, id;
+ id | category | val | cnt
+----+----------+-----+-----
+ 1 | A | 10 | 3
+ 2 | A | 20 | 0
+ 3 | A | 30 | 0
+ 4 | B | 40 | 3
+ 5 | B | 50 | 0
+ 6 | B | 60 | 0
+ 7 | C | 70 | 3
+ 8 | C | 80 | 0
+ 9 | C | 90 | 0
+(9 rows)
+
+-- Window Function with Complex ORDER BY
+SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_planner
+WINDOW w AS (
+ ORDER BY category DESC, val ASC
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY category DESC, val ASC;
+ id | category | val | cnt
+----+----------+-----+-----
+ 7 | C | 70 | 9
+ 8 | C | 80 | 0
+ 9 | C | 90 | 0
+ 4 | B | 40 | 0
+ 5 | B | 50 | 0
+ 6 | B | 60 | 0
+ 1 | A | 10 | 0
+ 2 | A | 20 | 0
+ 3 | A | 30 | 0
+(9 rows)
+
+-- Named Window Reference
+SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_planner
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | category | val | cnt
+----+----------+-----+-----
+ 1 | A | 10 | 9
+ 2 | A | 20 | 0
+ 3 | A | 30 | 0
+ 4 | B | 40 | 0
+ 5 | B | 50 | 0
+ 6 | B | 60 | 0
+ 7 | C | 70 | 0
+ 8 | C | 80 | 0
+ 9 | C | 90 | 0
+(9 rows)
+
+-- Inline Window Definition
+SELECT id, category, val,
+ COUNT(*) OVER (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ) as cnt
+FROM rpr_planner
+ORDER BY id;
+ id | category | val | cnt
+----+----------+-----+-----
+ 1 | A | 10 | 9
+ 2 | A | 20 | 0
+ 3 | A | 30 | 0
+ 4 | B | 40 | 0
+ 5 | B | 50 | 0
+ 6 | B | 60 | 0
+ 7 | C | 70 | 0
+ 8 | C | 80 | 0
+ 9 | C | 90 | 0
+(9 rows)
+
+-- Window with Aggregate Functions
+SELECT category,
+ COUNT(*) OVER w as window_cnt,
+ COUNT(*) as agg_cnt
+FROM rpr_planner
+WINDOW w AS (
+ PARTITION BY category
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+GROUP BY category
+ORDER BY category;
+ERROR: syntax error at or near "GROUP"
+LINE 12: GROUP BY category
+ ^
+-- Expected: ERROR (GROUP BY with window RPR not supported)
+-- ============================================================
+-- Subquery and CTE Tests
+-- Files: planner.c, prepjointree.c
+-- ============================================================
+-- Tests RPR with subqueries and CTEs
+-- RPR in Subquery (FROM clause)
+SELECT * FROM (
+ SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+ FROM rpr_planner
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+) sub
+WHERE cnt > 5
+ORDER BY id;
+ id | category | val | cnt
+----+----------+-----+-----
+ 1 | A | 10 | 9
+(1 row)
+
+-- RPR with Subquery in WHERE
+SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_planner
+WHERE val > (SELECT AVG(val) FROM rpr_planner)
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 50
+)
+ORDER BY id;
+ id | category | val | cnt
+----+----------+-----+-----
+ 6 | B | 60 | 4
+ 7 | C | 70 | 0
+ 8 | C | 80 | 0
+ 9 | C | 90 | 0
+(4 rows)
+
+-- CTE with RPR
+WITH rpr_cte AS (
+ SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+ FROM rpr_planner
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+)
+SELECT * FROM rpr_cte WHERE cnt > 5 ORDER BY id;
+ id | category | val | cnt
+----+----------+-----+-----
+ 1 | A | 10 | 9
+(1 row)
+
+-- Multiple CTE References
+WITH rpr_cte AS (
+ SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+ FROM rpr_planner
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+)
+SELECT c1.id, c1.cnt, c2.cnt as cnt2
+FROM rpr_cte c1
+JOIN rpr_cte c2 ON c1.id = c2.id
+ORDER BY c1.id;
+ id | cnt | cnt2
+----+-----+------
+ 1 | 9 | 9
+ 2 | 0 | 0
+ 3 | 0 | 0
+ 4 | 0 | 0
+ 5 | 0 | 0
+ 6 | 0 | 0
+ 7 | 0 | 0
+ 8 | 0 | 0
+ 9 | 0 | 0
+(9 rows)
+
+-- Nested CTEs
+WITH cte1 AS (
+ SELECT id, category, val FROM rpr_planner WHERE val > 30
+),
+cte2 AS (
+ SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+ FROM cte1
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+)
+SELECT * FROM cte2 ORDER BY id;
+ id | category | val | cnt
+----+----------+-----+-----
+ 4 | B | 40 | 6
+ 5 | B | 50 | 0
+ 6 | B | 60 | 0
+ 7 | C | 70 | 0
+ 8 | C | 80 | 0
+ 9 | C | 90 | 0
+(6 rows)
+
+-- ============================================================
+-- JOIN Tests
+-- Files: prepjointree.c, setrefs.c
+-- ============================================================
+-- Tests RPR with JOINs and multiple table references
+CREATE TABLE rpr_join1 (id INT, val1 INT);
+CREATE TABLE rpr_join2 (id INT, val2 INT);
+INSERT INTO rpr_join1 VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50);
+INSERT INTO rpr_join2 VALUES (1, 100), (2, 200), (3, 300), (4, 400), (5, 500);
+-- RPR After INNER JOIN
+SELECT t1.id, t1.val1, t2.val2,
+ COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+INNER JOIN rpr_join2 t2 ON t1.id = t2.id
+WINDOW w AS (
+ ORDER BY t1.id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val1 + val2 > 100
+)
+ORDER BY t1.id;
+ id | val1 | val2 | cnt
+----+------+------+-----
+ 1 | 10 | 100 | 5
+ 2 | 20 | 200 | 0
+ 3 | 30 | 300 | 0
+ 4 | 40 | 400 | 0
+ 5 | 50 | 500 | 0
+(5 rows)
+
+-- RPR After LEFT JOIN
+SELECT t1.id, t1.val1, t2.val2,
+ COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+LEFT JOIN rpr_join2 t2 ON t1.id = t2.id
+WINDOW w AS (
+ ORDER BY t1.id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val1 > 0
+)
+ORDER BY t1.id;
+ id | val1 | val2 | cnt
+----+------+------+-----
+ 1 | 10 | 100 | 5
+ 2 | 20 | 200 | 0
+ 3 | 30 | 300 | 0
+ 4 | 40 | 400 | 0
+ 5 | 50 | 500 | 0
+(5 rows)
+
+-- RPR with Multiple Tables in DEFINE
+SELECT t1.id, t1.val1, t2.val2,
+ COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+INNER JOIN rpr_join2 t2 ON t1.id = t2.id
+WINDOW w AS (
+ ORDER BY t1.id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ B)
+ DEFINE A AS t1.val1 > 20,
+ B AS t2.val2 > 200
+)
+ORDER BY t1.id;
+ id | val1 | val2 | cnt
+----+------+------+-----
+ 1 | 10 | 100 | 0
+ 2 | 20 | 200 | 0
+ 3 | 30 | 300 | 3
+ 4 | 40 | 400 | 0
+ 5 | 50 | 500 | 0
+(5 rows)
+
+-- RPR After Cross Join
+SELECT t1.id as id1, t2.id as id2, t1.val1, t2.val2,
+ COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+CROSS JOIN rpr_join2 t2
+WHERE t1.id <= 2 AND t2.id <= 2
+WINDOW w AS (
+ ORDER BY t1.id, t2.id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val1 + val2 > 0
+)
+ORDER BY t1.id, t2.id;
+ id1 | id2 | val1 | val2 | cnt
+-----+-----+------+------+-----
+ 1 | 1 | 10 | 100 | 4
+ 1 | 2 | 10 | 200 | 0
+ 2 | 1 | 20 | 100 | 0
+ 2 | 2 | 20 | 200 | 0
+(4 rows)
+
+-- Self-Join with RPR
+SELECT a.id, a.val1, b.val1 as val1_next,
+ COUNT(*) OVER w as cnt
+FROM rpr_join1 a
+INNER JOIN rpr_join1 b ON a.id + 1 = b.id
+WINDOW w AS (
+ ORDER BY a.id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (X+)
+ DEFINE X AS a.val1 < b.val1
+)
+ORDER BY a.id;
+ id | val1 | val1_next | cnt
+----+------+-----------+-----
+ 1 | 10 | 20 | 4
+ 2 | 20 | 30 | 0
+ 3 | 30 | 40 | 0
+ 4 | 40 | 50 | 0
+(4 rows)
+
+DROP TABLE rpr_join1, rpr_join2;
+-- ============================================================
+-- Complex Expression Tests
+-- Files: createplan.c, setrefs.c
+-- ============================================================
+-- Tests complex target list expressions
+CREATE TABLE rpr_target (id INT, val INT);
+INSERT INTO rpr_target VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50);
+-- Expressions in Target List
+SELECT id,
+ val * 2 as doubled,
+ val + 10 as added,
+ COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | doubled | added | cnt
+----+---------+-------+-----
+ 1 | 20 | 20 | 5
+ 2 | 40 | 30 | 0
+ 3 | 60 | 40 | 0
+ 4 | 80 | 50 | 0
+ 5 | 100 | 60 | 0
+(5 rows)
+
+-- CASE Expression in Target List
+SELECT id, val,
+ CASE
+ WHEN val < 30 THEN 'low'
+ WHEN val < 50 THEN 'medium'
+ ELSE 'high'
+ END as category,
+ COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | category | cnt
+----+-----+----------+-----
+ 1 | 10 | low | 5
+ 2 | 20 | low | 0
+ 3 | 30 | medium | 0
+ 4 | 40 | medium | 0
+ 5 | 50 | high | 0
+(5 rows)
+
+-- Subquery in Target List
+SELECT id, val,
+ (SELECT MAX(val) FROM rpr_target) as max_val,
+ COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | max_val | cnt
+----+-----+---------+-----
+ 1 | 10 | 50 | 5
+ 2 | 20 | 50 | 0
+ 3 | 30 | 50 | 0
+ 4 | 40 | 50 | 0
+ 5 | 50 | 50 | 0
+(5 rows)
+
+-- Function Calls in Target List
+SELECT id, val,
+ COALESCE(val, 0) as coalesced,
+ ABS(val - 30) as distance,
+ COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | coalesced | distance | cnt
+----+-----+-----------+----------+-----
+ 1 | 10 | 10 | 20 | 5
+ 2 | 20 | 20 | 10 | 0
+ 3 | 30 | 30 | 0 | 0
+ 4 | 40 | 40 | 10 | 0
+ 5 | 50 | 50 | 20 | 0
+(5 rows)
+
+-- Column Aliases and References
+SELECT id as row_id,
+ val as value,
+ COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY row_id;
+ row_id | value | cnt
+--------+-------+-----
+ 1 | 10 | 5
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+(5 rows)
+
+DROP TABLE rpr_target;
+-- ============================================================
+-- Set Operations Tests
+-- Files: planner.c
+-- ============================================================
+-- Tests RPR with UNION, INTERSECT, EXCEPT
+CREATE TABLE rpr_set1 (id INT, val INT);
+CREATE TABLE rpr_set2 (id INT, val INT);
+INSERT INTO rpr_set1 VALUES (1, 10), (2, 20), (3, 30);
+INSERT INTO rpr_set2 VALUES (2, 20), (3, 30), (4, 40);
+-- UNION with RPR
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+UNION
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 2 | 20 | 3
+ 3 | 30 | 0
+ 4 | 40 | 0
+(5 rows)
+
+-- UNION ALL with RPR
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+UNION ALL
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+ORDER BY id, val;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 2 | 20 | 3
+ 3 | 30 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+(6 rows)
+
+-- INTERSECT with RPR
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+INTERSECT
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 3 | 30 | 0
+(1 row)
+
+-- EXCEPT with RPR
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+EXCEPT
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+(2 rows)
+
+DROP TABLE rpr_set1, rpr_set2;
+-- ============================================================
+-- Sorting and Grouping Tests
+-- Files: planner.c, createplan.c
+-- ============================================================
+-- Tests RPR interaction with sorting and grouping
+CREATE TABLE rpr_sort (id INT, category VARCHAR(10), val INT);
+INSERT INTO rpr_sort VALUES
+ (1, 'A', 30), (2, 'B', 20), (3, 'A', 10),
+ (4, 'B', 40), (5, 'A', 50), (6, 'B', 60);
+-- RPR with GROUP BY
+SELECT category,
+ COUNT(*) as group_cnt,
+ MAX(val) as max_val,
+ COUNT(*) OVER w as window_cnt
+FROM rpr_sort
+GROUP BY category
+WINDOW w AS (
+ ORDER BY category
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS COUNT(*) > 0
+)
+ORDER BY category;
+ERROR: aggregate functions are not allowed in DEFINE
+LINE 11: DEFINE A AS COUNT(*) > 0
+ ^
+-- RPR with HAVING
+SELECT category,
+ COUNT(*) as group_cnt,
+ COUNT(*) OVER w as window_cnt
+FROM rpr_sort
+GROUP BY category
+HAVING COUNT(*) > 2
+WINDOW w AS (
+ ORDER BY category
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS COUNT(*) > 0
+)
+ORDER BY category;
+ERROR: aggregate functions are not allowed in DEFINE
+LINE 11: DEFINE A AS COUNT(*) > 0
+ ^
+-- RPR with DISTINCT
+SELECT DISTINCT category,
+ COUNT(*) OVER w as cnt
+FROM rpr_sort
+WINDOW w AS (
+ PARTITION BY category
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY category;
+ category | cnt
+----------+-----
+ A | 3
+ A | 0
+ B | 0
+ B | 3
+(4 rows)
+
+-- RPR with ORDER BY (different from window ORDER BY)
+SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_sort
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY val DESC;
+ id | category | val | cnt
+----+----------+-----+-----
+ 6 | B | 60 | 0
+ 5 | A | 50 | 0
+ 4 | B | 40 | 0
+ 1 | A | 30 | 6
+ 2 | B | 20 | 0
+ 3 | A | 10 | 0
+(6 rows)
+
+-- RPR with LIMIT and OFFSET
+SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_sort
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id
+LIMIT 3 OFFSET 1;
+ id | category | val | cnt
+----+----------+-----+-----
+ 2 | B | 20 | 0
+ 3 | A | 10 | 0
+ 4 | B | 40 | 0
+(3 rows)
+
+DROP TABLE rpr_sort;
+DROP TABLE rpr_planner;
+-- ============================================================
+-- Stress Tests
+-- ============================================================
+-- Edge cases and stress scenarios
+CREATE TABLE rpr_stress (id INT, val INT);
+INSERT INTO rpr_stress SELECT i, i * 10 FROM generate_series(1, 20) i;
+-- Very Long Query with Many Windows
+SELECT id, val,
+ COUNT(*) OVER w1 as cnt1,
+ COUNT(*) OVER w2 as cnt2,
+ COUNT(*) OVER w3 as cnt3
+FROM rpr_stress
+WINDOW w1 AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+),
+w2 AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (B+)
+ DEFINE B AS val > 50
+),
+w3 AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (C+)
+ DEFINE C AS val > 100
+)
+ORDER BY id;
+ id | val | cnt1 | cnt2 | cnt3
+----+-----+------+------+------
+ 1 | 10 | 20 | 0 | 0
+ 2 | 20 | 0 | 0 | 0
+ 3 | 30 | 0 | 0 | 0
+ 4 | 40 | 0 | 0 | 0
+ 5 | 50 | 0 | 0 | 0
+ 6 | 60 | 0 | 15 | 0
+ 7 | 70 | 0 | 0 | 0
+ 8 | 80 | 0 | 0 | 0
+ 9 | 90 | 0 | 0 | 0
+ 10 | 100 | 0 | 0 | 0
+ 11 | 110 | 0 | 0 | 10
+ 12 | 120 | 0 | 0 | 0
+ 13 | 130 | 0 | 0 | 0
+ 14 | 140 | 0 | 0 | 0
+ 15 | 150 | 0 | 0 | 0
+ 16 | 160 | 0 | 0 | 0
+ 17 | 170 | 0 | 0 | 0
+ 18 | 180 | 0 | 0 | 0
+ 19 | 190 | 0 | 0 | 0
+ 20 | 200 | 0 | 0 | 0
+(20 rows)
+
+-- Deeply Nested Subqueries with RPR
+SELECT * FROM (
+ SELECT * FROM (
+ SELECT * FROM (
+ SELECT id, val,
+ COUNT(*) OVER w as cnt
+ FROM rpr_stress
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+ ) sub1
+ ) sub2
+) sub3
+WHERE cnt > 10
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 20
+(1 row)
+
+-- Complex Expression in DEFINE Clause
+SELECT id, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_stress
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ B)
+ DEFINE A AS (val % 3 = 0 OR val % 5 = 0),
+ B AS (val * 2 > 100 AND val / 2 < 100)
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 19
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+ 11 | 110 | 0
+ 12 | 120 | 0
+ 13 | 130 | 0
+ 14 | 140 | 0
+ 15 | 150 | 0
+ 16 | 160 | 0
+ 17 | 170 | 0
+ 18 | 180 | 0
+ 19 | 190 | 0
+ 20 | 200 | 0
+(20 rows)
+
+-- Window with No Matching Rows
+SELECT id, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_stress
+WHERE val > 1000 -- No rows match
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+(0 rows)
+
+-- Window on Single Row
+SELECT id, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_stress
+WHERE id = 10
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 10 | 100 | 1
+(1 row)
+
+DROP TABLE rpr_stress;
+-- ============================================================
+-- Error Limit Tests
+-- ============================================================
+-- Tests for error conditions in rpr.c
+CREATE TABLE rpr_errors (id INT, val INT);
+INSERT INTO rpr_errors VALUES (1, 10), (2, 20);
+-- Test: PATTERN variable without DEFINE (A), DEFINE variable not in PATTERN (B)
+SELECT id, val, COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A)
+ DEFINE
+ B AS TRUE
+);
+ id | val | count
+----+-----+-------
+ 1 | 10 | 0
+ 2 | 20 | 0
+(2 rows)
+
+-- Expected: Success - A is implicitly TRUE, B is filtered out
+-- Test: 3 variables in PATTERN, 253 in DEFINE (DEFINE filtering test)
+SELECT COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (V1 V2 V3)
+ DEFINE
+ V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
+ V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
+ V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0,
+ V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0,
+ V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0,
+ V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0,
+ V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0,
+ V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0,
+ V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0,
+ V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0,
+ V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0,
+ V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0,
+ V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0,
+ V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0,
+ V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0,
+ V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0,
+ V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0,
+ V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0,
+ V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0,
+ V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0,
+ V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
+ V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
+ V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
+ V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
+ V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
+ V251 AS val > 0, V252 AS val > 0, V253 AS val > 0
+);
+ count
+-------
+ 0
+ 0
+(2 rows)
+
+-- Expected: Success - unused DEFINE variables are filtered out
+-- Test: 251 variables in PATTERN, 252 in DEFINE (boundary - should succeed)
+SELECT COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251)
+ DEFINE
+ V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
+ V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
+ V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0,
+ V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0,
+ V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0,
+ V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0,
+ V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0,
+ V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0,
+ V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0,
+ V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0,
+ V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0,
+ V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0,
+ V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0,
+ V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0,
+ V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0,
+ V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0,
+ V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0,
+ V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0,
+ V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0,
+ V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0,
+ V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
+ V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
+ V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
+ V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
+ V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
+ V251 AS val > 0, V252 AS val > 0
+);
+ count
+-------
+ 0
+ 0
+(2 rows)
+
+-- Expected: Success - unused DEFINE variables are filtered out
+-- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
+SELECT COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251 V252)
+ DEFINE
+ V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
+ V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
+ V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0,
+ V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0,
+ V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0,
+ V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0,
+ V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0,
+ V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0,
+ V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0,
+ V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0,
+ V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0,
+ V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0,
+ V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0,
+ V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0,
+ V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0,
+ V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0,
+ V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0,
+ V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0,
+ V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0,
+ V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0,
+ V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
+ V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
+ V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
+ V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
+ V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
+ V251 AS val > 0
+);
+ERROR: too many pattern variables
+DETAIL: Maximum is 251.
+-- Expected: ERROR - too many pattern variables (Maximum is 251)
+-- Test: Pattern nesting at maximum depth (depth 253)
+-- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
+SELECT id, val, COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+ DEFINE A AS val > 0
+);
+ id | val | count
+----+-----+-------
+ 1 | 10 | 0
+ 2 | 20 | 0
+(2 rows)
+
+-- Expected: Should succeed
+-- Test: Pattern nesting depth exceeds maximum (depth 254)
+-- Note: 254 nested GROUP{3,7} quantifiers produce depth 254 after optimization
+SELECT id, val, COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+ DEFINE A AS val > 0
+);
+ERROR: pattern nesting too deep
+DETAIL: Pattern nesting depth 254 exceeds maximum 253.
+-- Expected: ERROR - pattern nesting too deep
+DROP TABLE rpr_errors;
+-- ============================================================
+-- Jacob's Patterns
+-- ============================================================
+-- Basic pattern matching tests from jacob branch
+-- Test: A? (optional, greedy)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A?)
+ DEFINE A AS val > 50
+);
+ id | val | c
+----+-----+---
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 1
+ 7 | 70 | 1
+ 8 | 80 | 1
+ 9 | 90 | 1
+ 10 | 100 | 1
+(10 rows)
+
+-- Test: A{2} (exact count)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2})
+ DEFINE A AS val <= 50
+);
+ id | val | c
+----+-----+---
+ 1 | 10 | 2
+ 2 | 20 | 0
+ 3 | 30 | 2
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Test: A{1,3} (bounded range, greedy)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{1,3})
+ DEFINE A AS val <= 50
+);
+ id | val | c
+----+-----+---
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 2
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Test: A | B (simple alternation)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A | B)
+ DEFINE A AS val <= 30, B AS val > 70
+);
+ id | val | c
+----+-----+---
+ 1 | 10 | 1
+ 2 | 20 | 1
+ 3 | 30 | 1
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 1
+ 9 | 90 | 1
+ 10 | 100 | 1
+(10 rows)
+
+-- Test: A | B | C (three-way alternation)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A | B | C)
+ DEFINE A AS val <= 20, B AS val BETWEEN 40 AND 60, C AS val > 80
+);
+ id | val | c
+----+-----+---
+ 1 | 10 | 1
+ 2 | 20 | 1
+ 3 | 30 | 0
+ 4 | 40 | 1
+ 5 | 50 | 1
+ 6 | 60 | 1
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 1
+ 10 | 100 | 1
+(10 rows)
+
+-- Test: A B C (concatenation)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 60
+);
+ id | val | c
+----+-----+---
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Test: A B? C (optional middle)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B? C)
+ DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 60
+);
+ id | val | c
+----+-----+---
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Test: (A B)+ (grouped quantifier)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+)
+ DEFINE A AS val <= 50, B AS val > 50
+);
+ id | val | c
+----+-----+---
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 2
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Test: (A | B)+ C (alternation with quantifier)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B)+ C)
+ DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 80
+);
+ id | val | c
+----+-----+---
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 40 | 0
+ 5 | 50 | 0
+ 6 | 60 | 0
+ 7 | 70 | 0
+ 8 | 80 | 0
+ 9 | 90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Test: (A+ | (A | B)+)* - nested alternation inside quantified group
+-- Previously caused infinite recursion in nfa_advance_alt when the inner
+-- BEGIN(+)'s skip jump was followed as an ALT branch pointer.
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM (VALUES
+ (1, ARRAY['A', 'B']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C'])
+) AS t(id, flags)
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A+ | (A | B)+)*)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A,B} | 1 | 2
+ 2 | {B} | |
+ 3 | {C} | |
+(3 rows)
+
+-- ============================================================
+-- Pathological Patterns
+-- ============================================================
+-- These patterns previously caused issues. Now optimized or handled safely.
+-- Test: (A*)* - nested unbounded (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A*)*)
+ DEFINE A AS TRUE
+);
+ v | c
+---+---
+ 1 | 5
+ 2 | 0
+ 3 | 0
+ 4 | 0
+ 5 | 0
+(5 rows)
+
+-- Test: (A*)+ - inner nullable (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A*)+)
+ DEFINE A AS TRUE
+);
+ v | c
+---+---
+ 1 | 5
+ 2 | 0
+ 3 | 0
+ 4 | 0
+ 5 | 0
+(5 rows)
+
+-- Test: (A+)* - outer nullable (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A+)*)
+ DEFINE A AS TRUE
+);
+ v | c
+---+---
+ 1 | 5
+ 2 | 0
+ 3 | 0
+ 4 | 0
+ 5 | 0
+(5 rows)
+
+-- Test: (A+)+ - both require match (optimized to A+)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A+)+)
+ DEFINE A AS TRUE
+);
+ v | c
+---+---
+ 1 | 5
+ 2 | 0
+ 3 | 0
+ 4 | 0
+ 5 | 0
+(5 rows)
+
+-- Test: (((A)*)*)* - triple nested (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 3) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((((A)*)*)*)
+ DEFINE A AS TRUE
+);
+ v | c
+---+---
+ 1 | 3
+ 2 | 0
+ 3 | 0
+(3 rows)
+
+-- Optional group with alternation: A ((B | C) (D | E))* F?
+-- When only A matches, the * group matches 0 times and F? matches 0 times
+SELECT id, val, match_len
+FROM (SELECT id, val,
+ COUNT(*) OVER w AS match_len
+ FROM (VALUES (1, 1), (2, 99)) AS t(id, val)
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A ((B | C) (D | E))* F?)
+ DEFINE A AS val = 1,
+ B AS val = 2, C AS val = 3,
+ D AS val = 4, E AS val = 5,
+ F AS val = 6
+ )
+) s;
+ id | val | match_len
+----+-----+-----------
+ 1 | 1 | 1
+ 2 | 99 | 0
+(2 rows)
+
+DROP TABLE rpr_plan;
+-- ============================================================
+-- End of rpr_base.sql
+-- ============================================================
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
new file mode 100644
index 00000000000..f23d06f6d59
--- /dev/null
+++ b/src/test/regress/expected/rpr_explain.out
@@ -0,0 +1,3863 @@
+-- ============================================================
+-- RPR EXPLAIN Tests
+-- Tests for Row Pattern Recognition EXPLAIN output
+-- ============================================================
+--
+-- This test suite validates EXPLAIN output for RPR queries,
+-- including NFA statistics shown in EXPLAIN ANALYZE:
+-- - NFA States: peak, total, merged
+-- - NFA Contexts: peak, total, absorbed, skipped
+-- - NFA: matched (len min/max/avg), mismatched (len min/max/avg)
+-- - Pattern deparse formatting
+-- - Multiple output formats (text, JSON, XML)
+--
+-- Test Coverage:
+-- Basic NFA Statistics Tests
+-- State Statistics Tests
+-- Context Statistics Tests
+-- Match Length Statistics Tests
+-- Mismatch Length Statistics Tests
+-- JSON Format Tests
+-- XML Format Tests
+-- Multiple Partitions Tests
+-- Edge Cases
+-- Complex Pattern Tests
+-- Real-world Pattern Examples
+-- Performance-oriented Tests
+-- INITIAL vs no INITIAL comparison
+-- Quantifier Variations
+-- Regression Tests for Statistics Accuracy
+-- Alternation Pattern Tests
+-- Group Pattern Tests
+-- Window Function Combinations
+-- DEFINE Expression Variations
+-- Large Scale Statistics Verification
+-- ============================================================
+-- Filter function to normalize Storage memory values only (not NFA statistics).
+-- NFA statistics should not change between platforms; if they do, it could
+-- indicate issues such as uninitialized memory access.
+-- Works for text, JSON, and XML formats.
+create function rpr_explain_filter(text) returns setof text
+language plpgsql as
+$$
+declare
+ ln text;
+begin
+ for ln in execute $1
+ loop
+ -- Normalize memory size in Storage line only (platform-dependent)
+ -- Keep NFA statistics numbers unchanged (they are test assertions)
+
+ -- Text format: "Storage: Memory Maximum Storage: 18kB"
+ if ln ~ 'Storage:.*Maximum Storage:' then
+ ln := regexp_replace(ln, '\m\d+kB', 'NkB', 'g');
+ end if;
+
+ -- JSON format: "Maximum Storage": 17 (number in kB units)
+ if ln ~ '"Maximum Storage":' then
+ ln := regexp_replace(ln, '"Maximum Storage": \d+', '"Maximum Storage": 0', 'g');
+ end if;
+
+ -- XML format: <Maximum-Storage>17</Maximum-Storage> (number in kB units)
+ if ln ~ '<Maximum-Storage>' then
+ ln := regexp_replace(ln, '<Maximum-Storage>\d+</Maximum-Storage>', '<Maximum-Storage>0</Maximum-Storage>', 'g');
+ end if;
+
+ return next ln;
+ end loop;
+end;
+$$;
+-- Setup: Create test tables
+CREATE TEMP TABLE nfa_test (
+ id serial,
+ v int,
+ cat char(1)
+);
+-- Insert test data: 100 rows with predictable pattern
+INSERT INTO nfa_test (v, cat)
+SELECT i,
+ CASE
+ WHEN i % 5 = 1 THEN 'A'
+ WHEN i % 5 = 2 THEN 'B'
+ WHEN i % 5 = 3 THEN 'C'
+ WHEN i % 5 = 4 THEN 'D'
+ ELSE 'E'
+ END
+FROM generate_series(1, 100) i;
+-- Additional test table with more complex patterns
+CREATE TEMP TABLE nfa_complex (
+ id serial,
+ price int,
+ trend char(1) -- U=up, D=down, S=stable
+);
+INSERT INTO nfa_complex (price, trend)
+VALUES
+ (100, 'S'), (105, 'U'), (110, 'U'), (108, 'D'), (112, 'U'),
+ (115, 'U'), (113, 'D'), (111, 'D'), (109, 'D'), (110, 'U'),
+ (120, 'U'), (125, 'U'), (130, 'U'), (128, 'D'), (126, 'D'),
+ (124, 'D'), (122, 'D'), (120, 'D'), (118, 'D'), (119, 'U'),
+ (121, 'U'), (123, 'U'), (125, 'U'), (127, 'U'), (129, 'U'),
+ (131, 'U'), (133, 'U'), (130, 'D'), (127, 'D'), (124, 'D');
+-- ============================================================
+-- Basic NFA Statistics Tests
+-- ============================================================
+-- Simple pattern - should show basic statistics
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS cat = 'A', B AS cat = 'B'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+------------------
+ PATTERN (a b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS cat = ''A'', B AS cat = ''B''
+)');
+ rpr_explain_filter
+-------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 2 peak, 101 total, 0 merged
+ NFA Contexts: 2 peak, 101 total, 60 pruned
+ NFA: 20 matched (len 2/2/2.0), 0 mismatched
+ NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
+ -> Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Pattern with no matches - 0 matched
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (X Y Z)
+ DEFINE X AS cat = 'X', Y AS cat = 'Y', Z AS cat = 'Z'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------
+ PATTERN (x y z)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (X Y Z)
+ DEFINE X AS cat = ''X'', Y AS cat = ''Y'', Z AS cat = ''Z''
+);');
+ rpr_explain_filter
+-------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: x y z
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 1 peak, 101 total, 0 merged
+ NFA Contexts: 2 peak, 101 total, 100 pruned
+ NFA: 0 matched, 0 mismatched
+ -> Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(8 rows)
+
+DROP VIEW rpr_v;
+-- Pattern matching every row - high match count
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (R)
+ DEFINE R AS TRUE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+----------------
+ PATTERN (r)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (R)
+ DEFINE R AS TRUE
+);');
+ rpr_explain_filter
+-------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: r
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 2 peak, 101 total, 0 merged
+ NFA Contexts: 2 peak, 101 total, 0 pruned
+ NFA: 100 matched (len 1/1/1.0), 0 mismatched
+ -> Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(8 rows)
+
+DROP VIEW rpr_v;
+-- Regression test: Space before parenthesis in pattern deparse
+-- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A (B | C))
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+------------------------
+ PATTERN (a (b | c))
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A (B | C))
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a (b | c)
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 28 total, 0 merged
+ NFA Contexts: 2 peak, 21 total, 6 pruned
+ NFA: 7 matched (len 2/2/2.0), 0 mismatched
+ NFA: 0 absorbed, 7 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Regression test: Sequential alternations at same depth
+-- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)"
+-- Previously failed due to missing parentheses on ALT depth decrease
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A ((B | C) (D | E))*)
+ DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-----------------------------------
+ PATTERN (a ((b | c) (d | e))*)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A ((B | C) (D | E))*)
+ DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a ((b | c) (d | e))*
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 4 peak, 49 total, 0 merged
+ NFA Contexts: 3 peak, 31 total, 24 pruned
+ NFA: 6 matched (len 1/1/1.0), 0 mismatched
+ -> Function Scan on generate_series s (actual rows=30.00 loops=1)
+(8 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- State Statistics Tests (peak, total, merged)
+-- ============================================================
+-- Simple quantifier pattern - A+ with short matches (no merging)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS v % 2 = 1
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-----------------
+ PATTERN (a+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS v % 2 = 1
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 76 total, 0 merged
+ NFA Contexts: 3 peak, 51 total, 25 pruned
+ NFA: 25 matched (len 1/1/1.0), 0 mismatched
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(8 rows)
+
+DROP VIEW rpr_v;
+-- Alternation pattern - multiple state branches
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B | C) (D | E))
+ DEFINE
+ A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
+ D AS cat = 'D', E AS cat = 'E'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+----------------------------------
+ PATTERN ((a | b | c) (d | e))
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B | C) (D | E))
+ DEFINE
+ A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+ D AS cat = ''D'', E AS cat = ''E''
+);');
+ rpr_explain_filter
+-------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b | c) (d | e)
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 5 peak, 363 total, 0 merged
+ NFA Contexts: 3 peak, 101 total, 20 pruned
+ NFA: 20 matched (len 2/2/2.0), 40 mismatched (len 2/2/2.0)
+ NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
+ -> Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Complex pattern with high state count
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B* C+)
+ DEFINE
+ A AS v % 3 = 1,
+ B AS v % 3 = 2,
+ C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-----------------------
+ PATTERN (a+ b* c+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B* C+)
+ DEFINE
+ A AS v % 3 = 1,
+ B AS v % 3 = 2,
+ C AS v % 3 = 0
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b* c+
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 5 peak, 235 total, 0 merged
+ NFA Contexts: 3 peak, 101 total, 34 pruned
+ NFA: 33 matched (len 3/3/3.0), 0 mismatched
+ NFA: 0 absorbed, 33 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Grouped pattern with quantifier - state merging
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+---------------------
+ PATTERN ((a b)+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b')+"
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 91 total, 0 merged
+ NFA Contexts: 2 peak, 61 total, 0 pruned
+ NFA: 1 matched (len 60/60/60.0), 0 mismatched
+ NFA: 29 absorbed (len 1/1/1.0), 30 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- State explosion pattern - many alternations
+-- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+------------------------------------------------------------------------------
+ PATTERN ((a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b))
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b){8}
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 16 peak, 548 total, 0 merged
+ NFA Contexts: 8 peak, 101 total, 1 pruned
+ NFA: 12 matched (len 8/8/8.0), 3 mismatched (len 2/4/3.0)
+ NFA: 0 absorbed, 84 skipped (len 1/7/4.0)
+ -> Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Consecutive ALT merge followed by different ALT
+-- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) (C | D))
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------------------
+ PATTERN ((a | b) (a | b) (c | d))
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) (C | D))
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b){2} (c | d)
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 6 peak, 111 total, 0 merged
+ NFA Contexts: 3 peak, 41 total, 12 pruned
+ NFA: 9 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
+ NFA: 0 absorbed, 18 skipped (len 1/2/1.5)
+ -> Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Consecutive ALT merge followed by non-ALT element
+-- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) C)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------------
+ PATTERN ((a | b) (a | b) c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) C)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b){2} c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 5 peak, 109 total, 0 merged
+ NFA Contexts: 3 peak, 41 total, 2 pruned
+ NFA: 12 matched (len 3/3/3.0), 2 mismatched (len 2/2/2.0)
+ NFA: 0 absorbed, 24 skipped (len 1/2/1.5)
+ -> Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B)+ (A | B))
+ DEFINE A AS v % 2 = 0, B AS v % 2 = 1
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+---------------------------------------
+ PATTERN ((a | b) (a | b)+ (a | b))
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B)+ (A | B))
+ DEFINE A AS v % 2 = 0, B AS v % 2 = 1
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b){3,}
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 6 peak, 161 total, 0 merged
+ NFA Contexts: 3 peak, 41 total, 0 pruned
+ NFA: 1 matched (len 40/40/40.0), 0 mismatched
+ NFA: 0 absorbed, 39 skipped (len 1/2/1.0)
+ -> Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- High state merging - alternation with plus quantifier
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B | C)+ D)
+ DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-----------------------------
+ PATTERN ((a | b | c)+ d)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B | C)+ D)
+ DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b | c)+ d
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 15 peak, 753 total, 0 merged
+ NFA Contexts: 4 peak, 101 total, 0 pruned
+ NFA: 25 matched (len 4/4/4.0), 0 mismatched
+ NFA: 0 absorbed, 75 skipped (len 1/3/2.0)
+ -> Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Nested quantifiers causing state growth
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A | B)+)+)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------
+ PATTERN (((a | b)+)+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A | B)+)+)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2
+);');
+ rpr_explain_filter
+------------------------------------------------------------------------
+ WindowAgg (actual rows=1000.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b)+
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 5 peak, 3336 total, 0 merged
+ NFA Contexts: 3 peak, 1001 total, 333 pruned
+ NFA: 334 matched (len 1/2/2.0), 0 mismatched
+ NFA: 0 absorbed, 333 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=1000.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Context Statistics Tests (peak, total, absorbed, skipped)
+-- ============================================================
+-- Context absorption with unbounded quantifier at start
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 91 total, 0 merged
+ NFA Contexts: 2 peak, 51 total, 0 pruned
+ NFA: 10 matched (len 5/5/5.0), 0 mismatched
+ NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- No absorption - bounded quantifier
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2,4} B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-----------------------
+ PATTERN (a{2,4} b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2,4} B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{2,4} b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 7 peak, 101 total, 0 merged
+ NFA Contexts: 5 peak, 51 total, 0 pruned
+ NFA: 10 matched (len 5/5/5.0), 0 mismatched
+ NFA: 0 absorbed, 40 skipped (len 1/4/2.5)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Contexts skipped by SKIP PAST LAST ROW
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------
+ PATTERN (a b c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 2 peak, 101 total, 0 merged
+ NFA Contexts: 3 peak, 101 total, 80 pruned
+ NFA: 10 matched (len 3/3/3.0), 0 mismatched
+ NFA: 0 absorbed, 10 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- High context absorption - unbounded group
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+ C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-----------------------
+ PATTERN ((a b)+ c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+ C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b')+" c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 134 total, 0 merged
+ NFA Contexts: 3 peak, 101 total, 34 pruned
+ NFA: 33 matched (len 3/3/3.0), 0 mismatched
+ NFA: 0 absorbed, 33 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Match Length Statistics Tests
+-- ============================================================
+-- Fixed length matches - all same length
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E)
+ DEFINE
+ A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
+ D AS cat = 'D', E AS cat = 'E'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+------------------------
+ PATTERN (a b c d e)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E)
+ DEFINE
+ A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+ D AS cat = ''D'', E AS cat = ''E''
+);');
+ rpr_explain_filter
+-------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b c d e
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 2 peak, 101 total, 0 merged
+ NFA Contexts: 3 peak, 101 total, 60 pruned
+ NFA: 20 matched (len 5/5/5.0), 0 mismatched
+ NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
+ -> Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Variable length matches - min/max/avg differ
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 191 total, 0 merged
+ NFA Contexts: 2 peak, 101 total, 0 pruned
+ NFA: 10 matched (len 10/10/10.0), 0 mismatched
+ NFA: 80 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Very long matches
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 200) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v <= 195, B AS v > 195
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 200) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v <= 195, B AS v > 195
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=200.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 396 total, 0 merged
+ NFA Contexts: 2 peak, 201 total, 4 pruned
+ NFA: 1 matched (len 196/196/196.0), 0 mismatched
+ NFA: 194 absorbed (len 1/1/1.0), 1 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=200.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Mix of short and long matches
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
+ B AS v % 20 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
+ B AS v % 20 = 0
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 171 total, 0 merged
+ NFA Contexts: 3 peak, 101 total, 25 pruned
+ NFA: 5 matched (len 5/5/5.0), 5 mismatched (len 11/11/11.0)
+ NFA: 60 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Mismatch Length Statistics Tests
+-- ============================================================
+-- Pattern that causes mismatches with length > 1
+-- Mismatch happens when partial match fails after processing multiple rows
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM (
+ SELECT v,
+ CASE WHEN v % 10 IN (1,2,3) THEN 'A'
+ WHEN v % 10 IN (4,5) THEN 'B'
+ WHEN v % 10 = 6 THEN 'C'
+ ELSE 'X' END AS cat
+ FROM generate_series(1, 100) AS s(v)
+) t
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B+ C)
+ DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+----------------------
+ PATTERN (a+ b+ c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM (
+ SELECT v,
+ CASE WHEN v % 10 IN (1,2,3) THEN ''A''
+ WHEN v % 10 IN (4,5) THEN ''B''
+ WHEN v % 10 = 6 THEN ''C''
+ ELSE ''X'' END AS cat
+ FROM generate_series(1, 100) AS s(v)
+) t
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B+ C)
+ DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b+ c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 151 total, 0 merged
+ NFA Contexts: 3 peak, 101 total, 60 pruned
+ NFA: 10 matched (len 6/6/6.0), 0 mismatched
+ NFA: 20 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Long partial matches that fail
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM (
+ SELECT i AS v,
+ CASE
+ WHEN i <= 20 THEN 'A'
+ WHEN i <= 25 THEN 'B'
+ WHEN i = 26 THEN 'X' -- breaks the pattern
+ WHEN i <= 50 THEN 'A'
+ WHEN i <= 55 THEN 'B'
+ WHEN i = 56 THEN 'C' -- completes pattern
+ ELSE 'Y'
+ END AS cat
+ FROM generate_series(1, 60) i
+) t
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B+ C)
+ DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+----------------------
+ PATTERN (a+ b+ c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM (
+ SELECT i AS v,
+ CASE
+ WHEN i <= 20 THEN ''A''
+ WHEN i <= 25 THEN ''B''
+ WHEN i = 26 THEN ''X'' -- breaks the pattern
+ WHEN i <= 50 THEN ''A''
+ WHEN i <= 55 THEN ''B''
+ WHEN i = 56 THEN ''C'' -- completes pattern
+ ELSE ''Y''
+ END AS cat
+ FROM generate_series(1, 60) i
+) t
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B+ C)
+ DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b+ c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 115 total, 0 merged
+ NFA Contexts: 3 peak, 61 total, 15 pruned
+ NFA: 1 matched (len 30/30/30.0), 1 mismatched (len 26/26/26.0)
+ NFA: 42 absorbed (len 1/1/1.0), 1 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series i (actual rows=60.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- JSON Format Tests
+-- ============================================================
+-- JSON format output with all statistics
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B+)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------
+ PATTERN (a+ b+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B+)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2
+)');
+ rpr_explain_filter
+----------------------------------------------------------------------------
+ [ +
+ { +
+ "Plan": { +
+ "Node Type": "WindowAgg", +
+ "Parallel Aware": false, +
+ "Async Capable": false, +
+ "Actual Rows": 50.00, +
+ "Actual Loops": 1, +
+ "Disabled": false, +
+ "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
+ "Pattern": "a+\" b+", +
+ "Storage": "Memory", +
+ "Maximum Storage": 0, +
+ "NFA States Peak": 3, +
+ "NFA States Total": 85, +
+ "NFA States Merged": 0, +
+ "NFA Contexts Peak": 3, +
+ "NFA Contexts Total": 51, +
+ "NFA Contexts Absorbed": 0, +
+ "NFA Contexts Skipped": 17, +
+ "NFA Contexts Pruned": 16, +
+ "NFA Matched": 17, +
+ "NFA Mismatched": 0, +
+ "NFA Match Length Min": 2, +
+ "NFA Match Length Max": 2, +
+ "NFA Match Length Avg": 2.0, +
+ "NFA Skipped Length Min": 1, +
+ "NFA Skipped Length Max": 1, +
+ "NFA Skipped Length Avg": 1.0, +
+ "Plans": [ +
+ { +
+ "Node Type": "Function Scan", +
+ "Parent Relationship": "Outer", +
+ "Parallel Aware": false, +
+ "Async Capable": false, +
+ "Function Name": "generate_series", +
+ "Alias": "s", +
+ "Actual Rows": 50.00, +
+ "Actual Loops": 1, +
+ "Disabled": false +
+ } +
+ ] +
+ }, +
+ "Triggers": [ +
+ ] +
+ } +
+ ]
+(1 row)
+
+DROP VIEW rpr_v;
+-- JSON format with match length statistics
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+)');
+ rpr_explain_filter
+----------------------------------------------------------------------------
+ [ +
+ { +
+ "Plan": { +
+ "Node Type": "WindowAgg", +
+ "Parallel Aware": false, +
+ "Async Capable": false, +
+ "Actual Rows": 100.00, +
+ "Actual Loops": 1, +
+ "Disabled": false, +
+ "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
+ "Pattern": "a+\" b", +
+ "Storage": "Memory", +
+ "Maximum Storage": 0, +
+ "NFA States Peak": 3, +
+ "NFA States Total": 191, +
+ "NFA States Merged": 0, +
+ "NFA Contexts Peak": 2, +
+ "NFA Contexts Total": 101, +
+ "NFA Contexts Absorbed": 80, +
+ "NFA Contexts Skipped": 10, +
+ "NFA Contexts Pruned": 0, +
+ "NFA Matched": 10, +
+ "NFA Mismatched": 0, +
+ "NFA Match Length Min": 10, +
+ "NFA Match Length Max": 10, +
+ "NFA Match Length Avg": 10.0, +
+ "NFA Absorbed Length Min": 1, +
+ "NFA Absorbed Length Max": 1, +
+ "NFA Absorbed Length Avg": 1.0, +
+ "NFA Skipped Length Min": 1, +
+ "NFA Skipped Length Max": 1, +
+ "NFA Skipped Length Avg": 1.0, +
+ "Plans": [ +
+ { +
+ "Node Type": "Function Scan", +
+ "Parent Relationship": "Outer", +
+ "Parallel Aware": false, +
+ "Async Capable": false, +
+ "Function Name": "generate_series", +
+ "Alias": "s", +
+ "Actual Rows": 100.00, +
+ "Actual Loops": 1, +
+ "Disabled": false +
+ } +
+ ] +
+ }, +
+ "Triggers": [ +
+ ] +
+ } +
+ ]
+(1 row)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- XML Format Tests
+-- ============================================================
+-- XML format output
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+------------------
+ PATTERN (a b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT XML)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+)');
+ rpr_explain_filter
+--------------------------------------------------------------------------------
+ <explain xmlns="http://www.postgresql.org/2009/explain"> +
+ <Query> +
+ <Plan> +
+ <Node-Type>WindowAgg</Node-Type> +
+ <Parallel-Aware>false</Parallel-Aware> +
+ <Async-Capable>false</Async-Capable> +
+ <Actual-Rows>30.00</Actual-Rows> +
+ <Actual-Loops>1</Actual-Loops> +
+ <Disabled>false</Disabled> +
+ <Window>w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)</Window>+
+ <Pattern>a b</Pattern> +
+ <Storage>Memory</Storage> +
+ <Maximum-Storage>0</Maximum-Storage> +
+ <NFA-States-Peak>2</NFA-States-Peak> +
+ <NFA-States-Total>31</NFA-States-Total> +
+ <NFA-States-Merged>0</NFA-States-Merged> +
+ <NFA-Contexts-Peak>2</NFA-Contexts-Peak> +
+ <NFA-Contexts-Total>31</NFA-Contexts-Total> +
+ <NFA-Contexts-Absorbed>0</NFA-Contexts-Absorbed> +
+ <NFA-Contexts-Skipped>15</NFA-Contexts-Skipped> +
+ <NFA-Contexts-Pruned>0</NFA-Contexts-Pruned> +
+ <NFA-Matched>15</NFA-Matched> +
+ <NFA-Mismatched>0</NFA-Mismatched> +
+ <NFA-Match-Length-Min>2</NFA-Match-Length-Min> +
+ <NFA-Match-Length-Max>2</NFA-Match-Length-Max> +
+ <NFA-Match-Length-Avg>2.0</NFA-Match-Length-Avg> +
+ <NFA-Skipped-Length-Min>1</NFA-Skipped-Length-Min> +
+ <NFA-Skipped-Length-Max>1</NFA-Skipped-Length-Max> +
+ <NFA-Skipped-Length-Avg>1.0</NFA-Skipped-Length-Avg> +
+ <Plans> +
+ <Plan> +
+ <Node-Type>Function Scan</Node-Type> +
+ <Parent-Relationship>Outer</Parent-Relationship> +
+ <Parallel-Aware>false</Parallel-Aware> +
+ <Async-Capable>false</Async-Capable> +
+ <Function-Name>generate_series</Function-Name> +
+ <Alias>s</Alias> +
+ <Actual-Rows>30.00</Actual-Rows> +
+ <Actual-Loops>1</Actual-Loops> +
+ <Disabled>false</Disabled> +
+ </Plan> +
+ </Plans> +
+ </Plan> +
+ <Triggers> +
+ </Triggers> +
+ </Query> +
+ </explain>
+(1 row)
+
+DROP VIEW rpr_v;
+-- JSON format with mismatch statistics
+-- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS v = 1, B AS v = 2, C AS v = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------
+ PATTERN (a b c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
+SELECT count(*) OVER w
+FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS v = 1, B AS v = 2, C AS v = 3
+)');
+ rpr_explain_filter
+----------------------------------------------------------------------------
+ [ +
+ { +
+ "Plan": { +
+ "Node Type": "WindowAgg", +
+ "Parallel Aware": false, +
+ "Async Capable": false, +
+ "Actual Rows": 9.00, +
+ "Actual Loops": 1, +
+ "Disabled": false, +
+ "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
+ "Pattern": "a b c", +
+ "Storage": "Memory", +
+ "Maximum Storage": 0, +
+ "NFA States Peak": 2, +
+ "NFA States Total": 10, +
+ "NFA States Merged": 0, +
+ "NFA Contexts Peak": 3, +
+ "NFA Contexts Total": 10, +
+ "NFA Contexts Absorbed": 0, +
+ "NFA Contexts Skipped": 1, +
+ "NFA Contexts Pruned": 5, +
+ "NFA Matched": 1, +
+ "NFA Mismatched": 2, +
+ "NFA Match Length Min": 3, +
+ "NFA Match Length Max": 3, +
+ "NFA Match Length Avg": 3.0, +
+ "NFA Mismatch Length Min": 3, +
+ "NFA Mismatch Length Max": 3, +
+ "NFA Mismatch Length Avg": 3.0, +
+ "NFA Skipped Length Min": 1, +
+ "NFA Skipped Length Max": 1, +
+ "NFA Skipped Length Avg": 1.0, +
+ "Plans": [ +
+ { +
+ "Node Type": "Values Scan", +
+ "Parent Relationship": "Outer", +
+ "Parallel Aware": false, +
+ "Async Capable": false, +
+ "Alias": "*VALUES*", +
+ "Actual Rows": 9.00, +
+ "Actual Loops": 1, +
+ "Disabled": false +
+ } +
+ ] +
+ }, +
+ "Triggers": [ +
+ ] +
+ } +
+ ]
+(1 row)
+
+DROP VIEW rpr_v;
+-- JSON format with skipped context statistics
+-- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+------------------------------------------------------------------------------
+ PATTERN ((a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b))
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+)');
+ rpr_explain_filter
+----------------------------------------------------------------------------
+ [ +
+ { +
+ "Plan": { +
+ "Node Type": "WindowAgg", +
+ "Parallel Aware": false, +
+ "Async Capable": false, +
+ "Actual Rows": 100.00, +
+ "Actual Loops": 1, +
+ "Disabled": false, +
+ "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
+ "Pattern": "(a | b){8}", +
+ "Storage": "Memory", +
+ "Maximum Storage": 0, +
+ "NFA States Peak": 16, +
+ "NFA States Total": 548, +
+ "NFA States Merged": 0, +
+ "NFA Contexts Peak": 8, +
+ "NFA Contexts Total": 101, +
+ "NFA Contexts Absorbed": 0, +
+ "NFA Contexts Skipped": 84, +
+ "NFA Contexts Pruned": 1, +
+ "NFA Matched": 12, +
+ "NFA Mismatched": 3, +
+ "NFA Match Length Min": 8, +
+ "NFA Match Length Max": 8, +
+ "NFA Match Length Avg": 8.0, +
+ "NFA Mismatch Length Min": 2, +
+ "NFA Mismatch Length Max": 4, +
+ "NFA Mismatch Length Avg": 3.0, +
+ "NFA Skipped Length Min": 1, +
+ "NFA Skipped Length Max": 7, +
+ "NFA Skipped Length Avg": 4.0, +
+ "Plans": [ +
+ { +
+ "Node Type": "Function Scan", +
+ "Parent Relationship": "Outer", +
+ "Parallel Aware": false, +
+ "Async Capable": false, +
+ "Function Name": "generate_series", +
+ "Alias": "s", +
+ "Actual Rows": 100.00, +
+ "Actual Loops": 1, +
+ "Disabled": false +
+ } +
+ ] +
+ }, +
+ "Triggers": [ +
+ ] +
+ } +
+ ]
+(1 row)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Multiple Partitions Tests
+-- ============================================================
+-- Statistics across multiple partitions
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM (
+ SELECT p, v
+ FROM generate_series(1, 3) p,
+ generate_series(1, 30) v
+) t
+WINDOW w AS (
+ PARTITION BY p
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM (
+ SELECT p, v
+ FROM generate_series(1, 3) p,
+ generate_series(1, 30) v
+) t
+WINDOW w AS (
+ PARTITION BY p
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+ rpr_explain_filter
+------------------------------------------------------------------------------------
+ WindowAgg (actual rows=90.00 loops=1)
+ Window: w AS (PARTITION BY p.p ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 165 total, 0 merged
+ NFA Contexts: 2 peak, 93 total, 0 pruned
+ NFA: 18 matched (len 5/5/5.0), 0 mismatched
+ NFA: 54 absorbed (len 1/1/1.0), 18 skipped (len 1/1/1.0)
+ -> Sort (actual rows=90.00 loops=1)
+ Sort Key: p.p
+ Sort Method: quicksort Memory: 27kB
+ -> Nested Loop (actual rows=90.00 loops=1)
+ -> Function Scan on generate_series p (actual rows=3.00 loops=1)
+ -> Function Scan on generate_series v (actual rows=30.00 loops=3)
+(14 rows)
+
+DROP VIEW rpr_v;
+-- Different pattern behavior per partition
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM (
+ SELECT
+ CASE WHEN v <= 25 THEN 1 ELSE 2 END AS p,
+ v % 10 AS val
+ FROM generate_series(1, 50) v
+) t
+WINDOW w AS (
+ PARTITION BY p
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS val < 5, B AS val >= 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM (
+ SELECT
+ CASE WHEN v <= 25 THEN 1 ELSE 2 END AS p,
+ v % 10 AS val
+ FROM generate_series(1, 50) v
+) t
+WINDOW w AS (
+ PARTITION BY p
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS val < 5, B AS val >= 5
+);');
+ rpr_explain_filter
+--------------------------------------------------------------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (PARTITION BY (CASE WHEN (v.v <= 25) THEN 1 ELSE 2 END) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 77 total, 0 merged
+ NFA Contexts: 2 peak, 52 total, 21 pruned
+ NFA: 5 matched (len 5/6/5.8), 0 mismatched
+ NFA: 19 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0)
+ -> Sort (actual rows=50.00 loops=1)
+ Sort Key: (CASE WHEN (v.v <= 25) THEN 1 ELSE 2 END)
+ Sort Method: quicksort Memory: 26kB
+ -> Function Scan on generate_series v (actual rows=50.00 loops=1)
+(12 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Edge Cases
+-- ============================================================
+-- Empty result set
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 0) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v = 1, B AS v = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+------------------
+ PATTERN (a b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 0) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v = 1, B AS v = 2
+);');
+ rpr_explain_filter
+---------------------------------------------------------------------
+ WindowAgg (actual rows=0.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b
+ -> Function Scan on generate_series s (actual rows=0.00 loops=1)
+(4 rows)
+
+DROP VIEW rpr_v;
+-- Single row
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A)
+ DEFINE A AS TRUE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+----------------
+ PATTERN (a)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 1) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A)
+ DEFINE A AS TRUE
+);');
+ rpr_explain_filter
+---------------------------------------------------------------------
+ WindowAgg (actual rows=1.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 2 peak, 2 total, 0 merged
+ NFA Contexts: 2 peak, 2 total, 0 pruned
+ NFA: 1 matched (len 1/1/1.0), 0 mismatched
+ -> Function Scan on generate_series s (actual rows=1.00 loops=1)
+(8 rows)
+
+DROP VIEW rpr_v;
+-- Pattern longer than data
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 5) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E F G H I J)
+ DEFINE
+ A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5,
+ F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+----------------------------------
+ PATTERN (a b c d e f g h i j)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 5) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E F G H I J)
+ DEFINE
+ A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5,
+ F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10
+);');
+ rpr_explain_filter
+---------------------------------------------------------------------
+ WindowAgg (actual rows=5.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b c d e f g h i j
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 2 peak, 6 total, 0 merged
+ NFA Contexts: 3 peak, 6 total, 4 pruned
+ NFA: 0 matched, 1 mismatched (len 5/5/5.0)
+ -> Function Scan on generate_series s (actual rows=5.00 loops=1)
+(8 rows)
+
+DROP VIEW rpr_v;
+-- All rows match as single match
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS TRUE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-----------------
+ PATTERN (a+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS TRUE
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 101 total, 0 merged
+ NFA Contexts: 2 peak, 51 total, 0 pruned
+ NFA: 1 matched (len 50/50/50.0), 0 mismatched
+ NFA: 49 absorbed (len 1/1/1.0), 0 skipped
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Complex Pattern Tests
+-- ============================================================
+-- Nested groups
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A B) C)+)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------------
+ PATTERN (((a b) c)+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A B) C)+)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b' c')+"
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 81 total, 0 merged
+ NFA Contexts: 3 peak, 61 total, 20 pruned
+ NFA: 1 matched (len 60/60/60.0), 0 mismatched
+ NFA: 19 absorbed (len 1/1/1.0), 20 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Multiple alternations
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (C | D | E))
+ DEFINE
+ A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
+ D AS cat = 'D', E AS cat = 'E'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+----------------------------------
+ PATTERN ((a | b) (c | d | e))
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (C | D | E))
+ DEFINE
+ A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+ D AS cat = ''D'', E AS cat = ''E''
+);');
+ rpr_explain_filter
+-------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b) (c | d | e)
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 5 peak, 282 total, 0 merged
+ NFA Contexts: 3 peak, 101 total, 40 pruned
+ NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
+ NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
+ -> Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Optional elements
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B? C)
+ DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+---------------------
+ PATTERN (a b? c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B? C)
+ DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b? c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 64 total, 0 merged
+ NFA Contexts: 3 peak, 51 total, 25 pruned
+ NFA: 12 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
+ NFA: 0 absorbed, 12 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Bounded quantifiers
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2,5} B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-----------------------
+ PATTERN (a{2,5} b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2,5} B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{2,5} b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 9 peak, 311 total, 0 merged
+ NFA Contexts: 7 peak, 101 total, 0 pruned
+ NFA: 10 matched (len 6/6/6.0), 40 mismatched (len 6/6/6.0)
+ NFA: 0 absorbed, 50 skipped (len 1/5/3.0)
+ -> Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Star quantifier
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B* C)
+ DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+---------------------
+ PATTERN (a b* c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B* C)
+ DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b* c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 91 total, 0 merged
+ NFA Contexts: 3 peak, 51 total, 40 pruned
+ NFA: 5 matched (len 9/9/9.0), 0 mismatched
+ NFA: 0 absorbed, 5 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Real-world Pattern Examples
+-- ============================================================
+-- Stock price pattern - V-shape (down then up)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_complex
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (D+ U+)
+ DEFINE D AS trend = 'D', U AS trend = 'U'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------
+ PATTERN (d+ u+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_complex
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (D+ U+)
+ DEFINE D AS trend = ''D'', U AS trend = ''U''
+);');
+ rpr_explain_filter
+-------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: d+" u+
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 4 peak, 58 total, 0 merged
+ NFA Contexts: 3 peak, 31 total, 3 pruned
+ NFA: 3 matched (len 3/14/8.0), 1 mismatched (len 3/3/3.0)
+ NFA: 9 absorbed (len 1/1/1.0), 14 skipped (len 1/1/1.0)
+ -> Seq Scan on nfa_complex (actual rows=30.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Stock price pattern - peak (up, stable, down)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_complex
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (U+ S* D+)
+ DEFINE U AS trend = 'U', S AS trend = 'S', D AS trend = 'D'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-----------------------
+ PATTERN (u+ s* d+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_complex
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (U+ S* D+)
+ DEFINE U AS trend = ''U'', S AS trend = ''S'', D AS trend = ''D''
+);');
+ rpr_explain_filter
+-------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: u+" s* d+
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 5 peak, 76 total, 0 merged
+ NFA Contexts: 3 peak, 31 total, 1 pruned
+ NFA: 4 matched (len 3/11/7.2), 0 mismatched
+ NFA: 12 absorbed (len 1/1/1.0), 13 skipped (len 1/1/1.0)
+ -> Seq Scan on nfa_complex (actual rows=30.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Consecutive increasing values (using PREV)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{3,})
+ DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------
+ PATTERN (a{3,})
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{3,})
+ DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{3,}"
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 99 total, 0 merged
+ NFA Contexts: 2 peak, 51 total, 0 pruned
+ NFA: 1 matched (len 50/50/50.0), 0 mismatched
+ NFA: 49 absorbed (len 1/1/1.0), 0 skipped
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Performance-oriented Tests
+-- ============================================================
+-- Large dataset with simple pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+------------------
+ PATTERN (a b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+ rpr_explain_filter
+------------------------------------------------------------------------
+ WindowAgg (actual rows=1000.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 2 peak, 1001 total, 0 merged
+ NFA Contexts: 2 peak, 1001 total, 0 pruned
+ NFA: 500 matched (len 2/2/2.0), 0 mismatched
+ NFA: 0 absorbed, 500 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=1000.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Large dataset with absorption
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
+);');
+ rpr_explain_filter
+------------------------------------------------------------------------
+ WindowAgg (actual rows=1000.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 1991 total, 0 merged
+ NFA Contexts: 2 peak, 1001 total, 0 pruned
+ NFA: 10 matched (len 100/100/100.0), 0 mismatched
+ NFA: 980 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=1000.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- High state merge ratio
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B)+ C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------------
+ PATTERN ((a | b)+ c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B)+ C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=500.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b)+ c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 8 peak, 2004 total, 0 merged
+ NFA Contexts: 3 peak, 501 total, 1 pruned
+ NFA: 166 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
+ NFA: 0 absorbed, 332 skipped (len 1/2/1.5)
+ -> Function Scan on generate_series s (actual rows=500.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- INITIAL vs no INITIAL comparison
+-- ============================================================
+-- With INITIAL keyword
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 91 total, 0 merged
+ NFA Contexts: 2 peak, 51 total, 0 pruned
+ NFA: 10 matched (len 5/5/5.0), 0 mismatched
+ NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Without INITIAL keyword (same behavior currently)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 91 total, 0 merged
+ NFA Contexts: 2 peak, 51 total, 0 pruned
+ NFA: 10 matched (len 5/5/5.0), 0 mismatched
+ NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Quantifier Variations
+-- ============================================================
+-- Plus quantifier
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS v % 4 <> 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-----------------
+ PATTERN (a+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS v % 4 <> 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 71 total, 0 merged
+ NFA Contexts: 3 peak, 41 total, 10 pruned
+ NFA: 10 matched (len 3/3/3.0), 0 mismatched
+ NFA: 20 absorbed (len 1/1/1.0), 0 skipped
+ -> Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Star quantifier (zero or more)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A* B)
+ DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a* b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A* B)
+ DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a*" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 4 peak, 102 total, 0 merged
+ NFA Contexts: 2 peak, 41 total, 10 pruned
+ NFA: 10 matched (len 3/3/3.0), 0 mismatched
+ NFA: 20 absorbed (len 1/1/1.0), 0 skipped
+ -> Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Question mark (zero or one)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A? B C)
+ DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+---------------------
+ PATTERN (a? b c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A? B C)
+ DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a? b c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 4 peak, 82 total, 0 merged
+ NFA Contexts: 3 peak, 41 total, 10 pruned
+ NFA: 10 matched (len 3/3/3.0), 0 mismatched
+ NFA: 0 absorbed, 20 skipped (len 1/2/1.5)
+ -> Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Exact count {n}
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{3} B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+---------------------
+ PATTERN (a{3} b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{3} B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{3} b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 4 peak, 51 total, 0 merged
+ NFA Contexts: 5 peak, 51 total, 0 pruned
+ NFA: 10 matched (len 4/4/4.0), 10 mismatched (len 4/4/4.0)
+ NFA: 0 absorbed, 30 skipped (len 1/3/2.0)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Range {n,m}
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2,4} B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-----------------------
+ PATTERN (a{2,4} b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2,4} B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{2,4} b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 7 peak, 101 total, 0 merged
+ NFA Contexts: 5 peak, 51 total, 0 pruned
+ NFA: 10 matched (len 5/5/5.0), 0 mismatched
+ NFA: 0 absorbed, 40 skipped (len 1/4/2.5)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- At least {n,}
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{3,} B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+----------------------
+ PATTERN (a{3,} b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{3,} B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a{3,}" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 86 total, 0 merged
+ NFA Contexts: 2 peak, 51 total, 0 pruned
+ NFA: 5 matched (len 10/10/10.0), 0 mismatched
+ NFA: 40 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Regression Tests for Statistics Accuracy
+-- ============================================================
+-- Verify state count accuracy
+-- Pattern A+ B with 20 rows should show predictable state behavior
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 37 total, 0 merged
+ NFA Contexts: 2 peak, 21 total, 0 pruned
+ NFA: 4 matched (len 5/5/5.0), 0 mismatched
+ NFA: 12 absorbed (len 1/1/1.0), 4 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Verify context count with known absorption
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B C)
+ DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+---------------------
+ PATTERN (a+ b c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B C)
+ DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 52 total, 0 merged
+ NFA Contexts: 3 peak, 31 total, 6 pruned
+ NFA: 3 matched (len 9/9/9.0), 0 mismatched
+ NFA: 18 absorbed (len 1/1/1.0), 3 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=30.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Verify match length with fixed-length pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------
+ PATTERN (a b c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 2 peak, 31 total, 0 merged
+ NFA Contexts: 3 peak, 31 total, 10 pruned
+ NFA: 10 matched (len 3/3/3.0), 0 mismatched
+ NFA: 0 absorbed, 10 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=30.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Alternation Pattern Tests
+-- ============================================================
+-- Simple alternation
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) C)
+ DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+------------------------
+ PATTERN ((a | b) c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) C)
+ DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
+);');
+ rpr_explain_filter
+-------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b) c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 202 total, 0 merged
+ NFA Contexts: 3 peak, 101 total, 40 pruned
+ NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
+ NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
+ -> Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Multiple items in alternation
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B | C | D) E)
+ DEFINE
+ A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
+ D AS cat = 'D', E AS cat = 'E'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------------
+ PATTERN ((a | b | c | d) e)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B | C | D) E)
+ DEFINE
+ A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+ D AS cat = ''D'', E AS cat = ''E''
+);');
+ rpr_explain_filter
+-------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b | c | d) e
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 5 peak, 404 total, 0 merged
+ NFA Contexts: 3 peak, 101 total, 0 pruned
+ NFA: 20 matched (len 2/2/2.0), 60 mismatched (len 2/2/2.0)
+ NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
+ -> Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Alternation with quantifiers
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B)+ C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------------
+ PATTERN ((a | b)+ c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B)+ C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b)+ c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 8 peak, 204 total, 0 merged
+ NFA Contexts: 3 peak, 51 total, 1 pruned
+ NFA: 16 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
+ NFA: 0 absorbed, 32 skipped (len 1/2/1.5)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Multiple alternatives (4+)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A | B | C | D | E)
+ DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------------
+ PATTERN (a | b | c | d | e)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A | B | C | D | E)
+ DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b | c | d | e)
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 6 peak, 505 total, 0 merged
+ NFA Contexts: 2 peak, 101 total, 0 pruned
+ NFA: 100 matched (len 1/1/1.0), 0 mismatched
+ -> Function Scan on generate_series s (actual rows=100.00 loops=1)
+(8 rows)
+
+DROP VIEW rpr_v;
+-- Alternation at start
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B) C D)
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------
+ PATTERN ((a | b) c d)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B) C D)
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b) c d
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 122 total, 0 merged
+ NFA Contexts: 3 peak, 61 total, 16 pruned
+ NFA: 15 matched (len 3/3/3.0), 14 mismatched (len 2/2/2.0)
+ NFA: 0 absorbed, 15 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Multiple sequential alternations
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B) C (D | E) F)
+ DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+----------------------------------
+ PATTERN ((a | b) c (d | e) f)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B) C (D | E) F)
+ DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b) c (d | e) f
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 4 peak, 219 total, 0 merged
+ NFA Contexts: 3 peak, 101 total, 67 pruned
+ NFA: 0 matched, 33 mismatched (len 2/4/3.0)
+ -> Function Scan on generate_series s (actual rows=100.00 loops=1)
+(8 rows)
+
+DROP VIEW rpr_v;
+-- Quantified alternatives
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A+ | B+) C)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------
+ PATTERN ((a+ | b+) c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A+ | B+) C)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a+" | b+") c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 4 peak, 162 total, 0 merged
+ NFA Contexts: 3 peak, 61 total, 1 pruned
+ NFA: 20 matched (len 2/2/2.0), 19 mismatched (len 2/2/2.0)
+ NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Alternation at end
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B (C | D))
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------
+ PATTERN (a b (c | d))
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B (C | D))
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b (c | d)
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 75 total, 0 merged
+ NFA Contexts: 3 peak, 61 total, 32 pruned
+ NFA: 14 matched (len 3/3/3.0), 0 mismatched
+ NFA: 0 absorbed, 14 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Nested ALT at start of branch inside outer ALT
+-- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A ((B | C) D | E))
+ DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------------
+ PATTERN (a ((b | c) d | e))
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A ((B | C) D | E))
+ DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a ((b | c) d | e)
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 4 peak, 29 total, 0 merged
+ NFA Contexts: 3 peak, 21 total, 17 pruned
+ NFA: 0 matched, 3 mismatched (len 3/3/3.0)
+ -> Function Scan on generate_series s (actual rows=20.00 loops=1)
+(8 rows)
+
+DROP VIEW rpr_v;
+-- Nested ALT at end of branch inside outer ALT
+-- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (C (A | B) | D)
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+----------------------------
+ PATTERN (c (a | b) | d)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (C (A | B) | D)
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (c (a | b) | d)
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 4 peak, 47 total, 0 merged
+ NFA Contexts: 3 peak, 21 total, 10 pruned
+ NFA: 5 matched (len 1/1/1.0), 5 mismatched (len 2/2/2.0)
+ -> Function Scan on generate_series s (actual rows=20.00 loops=1)
+(8 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Group Pattern Tests
+-- ============================================================
+-- Simple group
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+---------------------
+ PATTERN ((a b)+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b')+"
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 61 total, 0 merged
+ NFA Contexts: 2 peak, 41 total, 0 pruned
+ NFA: 1 matched (len 40/40/40.0), 0 mismatched
+ NFA: 19 absorbed (len 1/1/1.0), 20 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Group with bounded quantifier
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B){2,4})
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------------
+ PATTERN ((a b){2,4})
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B){2,4})
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a b){2,4}
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 4 peak, 51 total, 0 merged
+ NFA Contexts: 3 peak, 41 total, 5 pruned
+ NFA: 5 matched (len 8/8/8.0), 0 mismatched
+ NFA: 0 absorbed, 30 skipped (len 1/2/1.5)
+ -> Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Nested groups
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A B){2})+)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------
+ PATTERN (((a b){2})+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A B){2})+)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: ((a b){2})+
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 5 peak, 76 total, 0 merged
+ NFA Contexts: 4 peak, 61 total, 15 pruned
+ NFA: 1 matched (len 60/60/60.0), 0 mismatched
+ NFA: 0 absorbed, 44 skipped (len 1/4/2.3)
+ -> Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Deep nesting (3+ levels)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((((A | B)+)+)+)
+ DEFINE A AS v % 2 = 0, B AS v % 2 = 1
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-----------------------------
+ PATTERN ((((a | b)+)+)+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((((A | B)+)+)+)
+ DEFINE A AS v % 2 = 0, B AS v % 2 = 1
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b)+
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 5 peak, 162 total, 0 merged
+ NFA Contexts: 2 peak, 41 total, 0 pruned
+ NFA: 1 matched (len 40/40/40.0), 0 mismatched
+ NFA: 0 absorbed, 39 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Bounded quantifier on alternation
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B){2,3} C)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-----------------------------
+ PATTERN ((a | b){2,3} c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B){2,3} C)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a | b){2,3} c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 7 peak, 200 total, 0 merged
+ NFA Contexts: 3 peak, 61 total, 2 pruned
+ NFA: 19 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
+ NFA: 0 absorbed, 38 skipped (len 1/2/1.5)
+ -> Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Nested groups with quantifiers
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (((A B)+ C)*)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------
+ PATTERN (((a b)+ c)*)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (((A B)+ C)*)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: ((a' b')+" c)*
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 7 peak, 178 total, 0 merged
+ NFA Contexts: 4 peak, 61 total, 22 pruned
+ NFA: 1 matched (len 57/57/57.0), 0 mismatched
+ NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
+ -> Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Partial nested quantification
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A (B C)+)*)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------
+ PATTERN ((a (b c)+)*)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A (B C)+)*)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a (b c)+)*
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 5 peak, 160 total, 0 merged
+ NFA Contexts: 4 peak, 61 total, 22 pruned
+ NFA: 1 matched (len 57/57/57.0), 0 mismatched
+ NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
+ -> Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Window Function Combinations
+-- ============================================================
+-- count(*) with pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 55 total, 0 merged
+ NFA Contexts: 2 peak, 31 total, 0 pruned
+ NFA: 6 matched (len 5/5/5.0), 0 mismatched
+ NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=30.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- first_value with pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT first_value(v) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT first_value(v) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 55 total, 0 merged
+ NFA Contexts: 2 peak, 31 total, 0 pruned
+ NFA: 6 matched (len 5/5/5.0), 0 mismatched
+ NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=30.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- last_value with pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT last_value(v) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT last_value(v) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 55 total, 0 merged
+ NFA Contexts: 2 peak, 31 total, 0 pruned
+ NFA: 6 matched (len 5/5/5.0), 0 mismatched
+ NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=30.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Multiple window functions
+CREATE TEMP VIEW rpr_v AS
+SELECT
+ count(*) OVER w,
+ first_value(v) OVER w,
+ last_value(v) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT
+ count(*) OVER w,
+ first_value(v) OVER w,
+ last_value(v) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 55 total, 0 merged
+ NFA Contexts: 2 peak, 31 total, 0 pruned
+ NFA: 6 matched (len 5/5/5.0), 0 mismatched
+ NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=30.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- DEFINE Expression Variations
+-- ============================================================
+-- Complex boolean expressions
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS (v % 5 <> 0) AND (v % 3 <> 0),
+ B AS (v % 5 = 0) OR (v % 3 = 0)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS (v % 5 <> 0) AND (v % 3 <> 0),
+ B AS (v % 5 = 0) OR (v % 3 = 0)
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 78 total, 0 merged
+ NFA Contexts: 2 peak, 51 total, 6 pruned
+ NFA: 17 matched (len 2/3/2.6), 0 mismatched
+ NFA: 10 absorbed (len 1/1/1.0), 17 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Using PREV function
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (S U+ D+)
+ DEFINE
+ S AS TRUE,
+ U AS v > PREV(v),
+ D AS v < PREV(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+----------------------
+ PATTERN (s u+ d+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (S U+ D+)
+ DEFINE
+ S AS TRUE,
+ U AS v > PREV(v),
+ D AS v < PREV(v)
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: s u+ d+
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 60 peak, 466 total, 0 merged
+ NFA Contexts: 31 peak, 31 total, 1 pruned
+ NFA: 0 matched, 29 mismatched (len 2/30/16.0)
+ -> Function Scan on generate_series s (actual rows=30.00 loops=1)
+(8 rows)
+
+DROP VIEW rpr_v;
+-- Using NULL comparisons
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM (
+ SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
+ FROM generate_series(1, 30) v
+) t
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v IS NOT NULL, B AS v IS NULL
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM (
+ SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
+ FROM generate_series(1, 30) v
+) t
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v IS NOT NULL, B AS v IS NULL
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 55 total, 0 merged
+ NFA Contexts: 2 peak, 31 total, 0 pruned
+ NFA: 6 matched (len 5/5/5.0), 0 mismatched
+ NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series v (actual rows=30.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Large Scale Statistics Verification
+-- ============================================================
+-- 500 rows - verify statistics scale correctly
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B C)
+ DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+---------------------
+ PATTERN (a+ b c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B C)
+ DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=500.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 851 total, 0 merged
+ NFA Contexts: 3 peak, 501 total, 101 pruned
+ NFA: 50 matched (len 8/9/9.0), 0 mismatched
+ NFA: 299 absorbed (len 1/1/1.0), 50 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=500.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- High match count scenario
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+------------------
+ PATTERN (a b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=500.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 2 peak, 501 total, 0 merged
+ NFA Contexts: 2 peak, 501 total, 0 pruned
+ NFA: 250 matched (len 2/2/2.0), 0 mismatched
+ NFA: 0 absorbed, 250 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=500.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- High skip count scenario
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E)
+ DEFINE
+ A AS v % 100 = 1,
+ B AS v % 100 = 2,
+ C AS v % 100 = 3,
+ D AS v % 100 = 4,
+ E AS v % 100 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+------------------------
+ PATTERN (a b c d e)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E)
+ DEFINE
+ A AS v % 100 = 1,
+ B AS v % 100 = 2,
+ C AS v % 100 = 3,
+ D AS v % 100 = 4,
+ E AS v % 100 = 5
+);');
+ rpr_explain_filter
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=500.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b c d e
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 2 peak, 501 total, 0 merged
+ NFA Contexts: 3 peak, 501 total, 490 pruned
+ NFA: 5 matched (len 5/5/5.0), 0 mismatched
+ NFA: 0 absorbed, 5 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=500.00 loops=1)
+(9 rows)
+
+DROP VIEW rpr_v;
+-- Cleanup
+DROP TABLE nfa_test;
+DROP TABLE nfa_complex;
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
new file mode 100644
index 00000000000..46a463c2597
--- /dev/null
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -0,0 +1,2524 @@
+-- ============================================================
+-- RPR NFA Tests
+-- Tests for Row Pattern Recognition NFA Runtime Execution
+-- ============================================================
+--
+-- This test suite validates the NFA (Non-deterministic Finite
+-- Automaton) runtime execution engine in nodeWindowAgg.c,
+-- focusing on update_reduced_frame and related functions.
+--
+-- Test Strategy:
+-- Diagonal pattern style using ARRAY flags to explicitly
+-- control which pattern variables match at each row.
+--
+-- Test Coverage:
+-- Basic NFA Flow (match->absorb->advance)
+-- Absorption Optimization
+-- Context Lifecycle Management
+-- Advance Phase (Epsilon Transitions)
+-- Match Phase (Variable Matching)
+-- Frame Boundary Handling
+-- State Management (Deduplication)
+-- Statistics and Diagnostics
+-- Quantifier Runtime Behavior
+-- Pathological Pattern Protection
+-- Alternation Runtime Behavior
+-- Deep Nested Groups
+-- SKIP Options (Runtime)
+-- INITIAL Mode (Runtime)
+-- Frame Boundary Variations
+-- Special Partition Cases
+-- DEFINE Special Cases
+-- Absorption Dynamic Flags
+-- FIXME Issues (Known Limitations)
+--
+-- Responsibility:
+-- - NFA runtime execution paths
+-- - Context/State lifecycle management
+-- - Runtime boundary conditions and protections
+--
+-- NOT tested here (covered in other files):
+-- - Pattern parsing/optimization (rpr_base.sql)
+-- - EXPLAIN output (rpr_explain.sql)
+-- - PREV/NEXT semantics (rpr.sql)
+-- ============================================================
+-- ============================================================
+-- Basic NFA Flow
+-- ============================================================
+-- Simple sequential pattern
+WITH test_sequential AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['_']) -- No match
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_sequential
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {D} | |
+ 5 | {_} | |
+(5 rows)
+
+-- Quantified pattern (A+ B+ C+)
+WITH test_quantified AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['B']),
+ (6, ARRAY['C']),
+ (7, ARRAY['C']),
+ (8, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_quantified
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B+ C+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 7
+ 2 | {A} | 2 | 7
+ 3 | {A} | 3 | 7
+ 4 | {B} | |
+ 5 | {B} | |
+ 6 | {C} | |
+ 7 | {C} | |
+ 8 | {_} | |
+(8 rows)
+
+-- Optional pattern (A B? C)
+WITH test_optional AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['C']), -- B skipped
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']), -- B matched
+ (6, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_optional
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B? C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 2
+ 2 | {C} | |
+ 3 | {A} | 3 | 5
+ 4 | {B} | |
+ 5 | {C} | |
+ 6 | {_} | |
+(6 rows)
+
+-- Alternation pattern (A (B|C) D)
+WITH test_alternation AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']), -- First branch
+ (3, ARRAY['D']),
+ (4, ARRAY['A']),
+ (5, ARRAY['C']), -- Second branch
+ (6, ARRAY['D']),
+ (7, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_alternation
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A (B | C) D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {B} | |
+ 3 | {D} | |
+ 4 | {A} | 4 | 6
+ 5 | {C} | |
+ 6 | {D} | |
+ 7 | {_} | |
+(7 rows)
+
+-- ============================================================
+-- Absorption Optimization
+-- ============================================================
+-- Absorbable pattern (A+)
+WITH test_absorbable AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_absorbable
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | 2 | 4
+ 3 | {A} | 3 | 4
+ 4 | {A} | 4 | 4
+ 5 | {_} | |
+(5 rows)
+
+-- Mixed absorbable/non-absorbable ((A+) | B)
+WITH test_mixed_absorption AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_mixed_absorption
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A+) | B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {A} | 2 | 3
+ 3 | {A} | 3 | 3
+ 4 | {B} | 4 | 4
+ 5 | {_} | |
+(5 rows)
+
+-- State coverage (same elemIdx, different count)
+WITH test_state_coverage AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_state_coverage
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A{2,} B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | 2 | 4
+ 3 | {A} | |
+ 4 | {B} | |
+ 5 | {_} | |
+(5 rows)
+
+-- ============================================================
+-- Context Lifecycle
+-- ============================================================
+-- Multiple overlapping contexts (SKIP TO NEXT ROW)
+WITH test_overlapping_contexts AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_overlapping_contexts
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | 2 | 4
+ 3 | {A} | 3 | 4
+ 4 | {B} | |
+ 5 | {_} | |
+(5 rows)
+
+-- Failed context cleanup (early failure)
+WITH test_context_cleanup AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['_']), -- Pruned at first row
+ (2, ARRAY['A']),
+ (3, ARRAY['_']), -- Mismatched after row 2
+ (4, ARRAY['A']),
+ (5, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_context_cleanup
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {_} | |
+ 2 | {A} | |
+ 3 | {_} | |
+ 4 | {A} | 4 | 5
+ 5 | {B} | |
+(5 rows)
+
+-- Partition end (incomplete contexts)
+WITH test_partition_end AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A'])
+ -- Pattern requires B, but partition ends
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_partition_end
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | |
+ 2 | {A} | |
+ 3 | {A} | |
+(3 rows)
+
+-- Completed context encountered during processing
+-- Pattern (A | B C D): Ctx1 takes long B->C->D path, while Ctx2 takes
+-- short A path and completes first. Next row sees Ctx2
+-- with states=NULL and skips it.
+WITH test_completed_ctx AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['B', '_']),
+ (2, ARRAY['C', 'A']),
+ (3, ARRAY['D', '_']),
+ (4, ARRAY['_', '_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_completed_ctx
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A | B C D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {B,_} | 1 | 3
+ 2 | {C,A} | 2 | 2
+ 3 | {D,_} | |
+ 4 | {_,_} | |
+(4 rows)
+
+-- ============================================================
+-- Advance Phase (Epsilon Transitions)
+-- ============================================================
+-- Nested groups ((A B)+)
+WITH test_nested_groups AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']),
+ (7, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_nested_groups
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A B)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 6
+ 2 | {B} | |
+ 3 | {A} | 3 | 6
+ 4 | {B} | |
+ 5 | {A} | 5 | 6
+ 6 | {B} | |
+ 7 | {_} | |
+(7 rows)
+
+-- Multiple alternation branches (A (B|C|D) E)
+WITH test_multi_alt AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['E']),
+ (4, ARRAY['A']),
+ (5, ARRAY['C']),
+ (6, ARRAY['E']),
+ (7, ARRAY['A']),
+ (8, ARRAY['D']),
+ (9, ARRAY['E'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_multi_alt
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A (B | C | D) E)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {B} | |
+ 3 | {E} | |
+ 4 | {A} | 4 | 6
+ 5 | {C} | |
+ 6 | {E} | |
+ 7 | {A} | 7 | 9
+ 8 | {D} | |
+ 9 | {E} | |
+(9 rows)
+
+-- Optional VAR at start (A? B C)
+WITH test_optional_var AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['B']), -- A skipped
+ (2, ARRAY['C']),
+ (3, ARRAY['A']), -- A matched
+ (4, ARRAY['B']),
+ (5, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_optional_var
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A? B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {B} | 1 | 2
+ 2 | {C} | |
+ 3 | {A} | 3 | 5
+ 4 | {B} | 4 | 5
+ 5 | {C} | |
+(5 rows)
+
+-- Nested alternation ((A|B) (C|D))
+WITH test_nested_alt AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['C']), -- A C
+ (3, ARRAY['A']),
+ (4, ARRAY['D']), -- A D
+ (5, ARRAY['B']),
+ (6, ARRAY['C']), -- B C
+ (7, ARRAY['B']),
+ (8, ARRAY['D']) -- B D
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_nested_alt
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A | B) (C | D))
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 2
+ 2 | {C} | |
+ 3 | {A} | 3 | 4
+ 4 | {D} | |
+ 5 | {B} | 5 | 6
+ 6 | {C} | |
+ 7 | {B} | 7 | 8
+ 8 | {D} | |
+(8 rows)
+
+-- ============================================================
+-- Match Phase
+-- ============================================================
+-- Simple VAR with END next (A B C all min=max=1)
+WITH test_simple_var AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_simple_var
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {_} | |
+(4 rows)
+
+-- VAR max exceeded (A{2,3})
+WITH test_max_exceeded AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']), -- Max = 3
+ (4, ARRAY['A']), -- Exceeds max, state removed
+ (5, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_max_exceeded
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A{2,3} B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | |
+ 2 | {A} | 2 | 5
+ 3 | {A} | 3 | 5
+ 4 | {A} | |
+ 5 | {B} | |
+(5 rows)
+
+-- Non-matching VAR (DEFINE false)
+WITH test_non_matching AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['_']), -- B not matched (DEFINE false)
+ (3, ARRAY['A']),
+ (4, ARRAY['B']), -- B matched
+ (5, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_non_matching
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | |
+ 2 | {_} | |
+ 3 | {A} | 3 | 5
+ 4 | {B} | |
+ 5 | {C} | |
+(5 rows)
+
+-- ============================================================
+-- Frame Boundary Handling
+-- ============================================================
+-- Limited frame (ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING)
+WITH test_limited_frame AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']), -- Within 3 FOLLOWING
+ (5, ARRAY['B']), -- Beyond 3 FOLLOWING from row 1
+ (6, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_limited_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | 2 | 4
+ 3 | {A} | 3 | 4
+ 4 | {B} | |
+ 5 | {B} | |
+ 6 | {_} | |
+(6 rows)
+
+-- Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+WITH test_unbounded_frame AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']) -- Far from start, but unbounded
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_unbounded_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 6
+ 2 | {A} | 2 | 6
+ 3 | {A} | 3 | 6
+ 4 | {A} | 4 | 6
+ 5 | {A} | 5 | 6
+ 6 | {B} | |
+(6 rows)
+
+-- Match exceeds frame boundary
+WITH test_frame_exceeded AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A'])
+ -- Frame ends at row 3 (2 FOLLOWING), B never appears
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_frame_exceeded
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | |
+ 2 | {A} | |
+ 3 | {A} | |
+(3 rows)
+
+-- Frame boundary forced mismatch
+-- Limited frame with enough rows so that a context's frame boundary
+-- is exceeded while still processing.
+WITH test_frame_boundary AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_frame_boundary
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | |
+ 2 | {A} | |
+ 3 | {A} | |
+ 4 | {A} | 4 | 6
+ 5 | {A} | 5 | 6
+ 6 | {B} | |
+(6 rows)
+
+-- ============================================================
+-- State Management
+-- ============================================================
+-- Duplicate state creation
+WITH test_duplicate_states AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A', 'B']), -- Both A and B match (creates duplicate states via different paths)
+ (2, ARRAY['C', '_']),
+ (3, ARRAY['D', '_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_duplicate_states
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A | B) C D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A,B} | 1 | 3
+ 2 | {C,_} | |
+ 3 | {D,_} | |
+(3 rows)
+
+-- Large pattern (stress free list)
+WITH test_large_pattern AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E']),
+ (6, ARRAY['F']),
+ (7, ARRAY['G']),
+ (8, ARRAY['H']),
+ (9, ARRAY['I']),
+ (10, ARRAY['J'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_large_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C D E F G H I J)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags),
+ F AS 'F' = ANY(flags),
+ G AS 'G' = ANY(flags),
+ H AS 'H' = ANY(flags),
+ I AS 'I' = ANY(flags),
+ J AS 'J' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 10
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {D} | |
+ 5 | {E} | |
+ 6 | {F} | |
+ 7 | {G} | |
+ 8 | {H} | |
+ 9 | {I} | |
+ 10 | {J} | |
+(10 rows)
+
+-- Reduced frame map reallocation (> 1024 rows)
+WITH test_map_realloc AS (
+ SELECT id, CASE WHEN id % 2 = 1 THEN ARRAY['A'] ELSE ARRAY['B'] END AS flags
+ FROM generate_series(1, 1100) AS id
+)
+SELECT count(*), min(match_start), max(match_end)
+FROM (
+ SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+ FROM test_map_realloc
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+ )
+) sub;
+ count | min | max
+-------+-----+------
+ 1100 | 1 | 1100
+(1 row)
+
+-- ============================================================
+-- Statistics and Diagnostics
+-- ============================================================
+-- Matched contexts
+WITH test_matched AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_matched
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 2
+ 2 | {B} | |
+ 3 | {A} | 3 | 4
+ 4 | {B} | |
+(4 rows)
+
+-- Pruned contexts (failed at first row)
+WITH test_pruned AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['_']), -- Pruned
+ (2, ARRAY['_']), -- Pruned
+ (3, ARRAY['A']),
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_pruned
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {_} | |
+ 2 | {_} | |
+ 3 | {A} | 3 | 4
+ 4 | {B} | |
+(4 rows)
+
+-- Mismatched contexts (failed after multiple rows)
+WITH test_mismatched AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['_']), -- Mismatched after 2 rows
+ (4, ARRAY['A']),
+ (5, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_mismatched
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | |
+ 2 | {A} | |
+ 3 | {_} | |
+ 4 | {A} | 4 | 5
+ 5 | {B} | |
+(5 rows)
+
+-- Absorbed contexts
+WITH test_absorbed AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_absorbed
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | 2 | 4
+ 3 | {A} | 3 | 4
+ 4 | {A} | 4 | 4
+ 5 | {_} | |
+(5 rows)
+
+-- Skipped contexts (SKIP TO NEXT ROW)
+WITH test_skipped AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']) -- Completes match starting at row 1
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_skipped
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | 2 | 4
+ 3 | {A} | 3 | 4
+ 4 | {B} | |
+(4 rows)
+
+-- ============================================================
+-- Quantifier Runtime Behavior
+-- ============================================================
+-- Large count handling (A{100})
+WITH test_large_count AS (
+ SELECT i AS id, ARRAY['A'] AS flags
+ FROM generate_series(1, 105) i
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_large_count
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A{100})
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+-----+-------+-------------+-----------
+ 1 | {A} | 1 | 100
+ 2 | {A} | 2 | 101
+ 3 | {A} | 3 | 102
+ 4 | {A} | 4 | 103
+ 5 | {A} | 5 | 104
+ 6 | {A} | 6 | 105
+ 7 | {A} | |
+ 8 | {A} | |
+ 9 | {A} | |
+ 10 | {A} | |
+ 11 | {A} | |
+ 12 | {A} | |
+ 13 | {A} | |
+ 14 | {A} | |
+ 15 | {A} | |
+ 16 | {A} | |
+ 17 | {A} | |
+ 18 | {A} | |
+ 19 | {A} | |
+ 20 | {A} | |
+ 21 | {A} | |
+ 22 | {A} | |
+ 23 | {A} | |
+ 24 | {A} | |
+ 25 | {A} | |
+ 26 | {A} | |
+ 27 | {A} | |
+ 28 | {A} | |
+ 29 | {A} | |
+ 30 | {A} | |
+ 31 | {A} | |
+ 32 | {A} | |
+ 33 | {A} | |
+ 34 | {A} | |
+ 35 | {A} | |
+ 36 | {A} | |
+ 37 | {A} | |
+ 38 | {A} | |
+ 39 | {A} | |
+ 40 | {A} | |
+ 41 | {A} | |
+ 42 | {A} | |
+ 43 | {A} | |
+ 44 | {A} | |
+ 45 | {A} | |
+ 46 | {A} | |
+ 47 | {A} | |
+ 48 | {A} | |
+ 49 | {A} | |
+ 50 | {A} | |
+ 51 | {A} | |
+ 52 | {A} | |
+ 53 | {A} | |
+ 54 | {A} | |
+ 55 | {A} | |
+ 56 | {A} | |
+ 57 | {A} | |
+ 58 | {A} | |
+ 59 | {A} | |
+ 60 | {A} | |
+ 61 | {A} | |
+ 62 | {A} | |
+ 63 | {A} | |
+ 64 | {A} | |
+ 65 | {A} | |
+ 66 | {A} | |
+ 67 | {A} | |
+ 68 | {A} | |
+ 69 | {A} | |
+ 70 | {A} | |
+ 71 | {A} | |
+ 72 | {A} | |
+ 73 | {A} | |
+ 74 | {A} | |
+ 75 | {A} | |
+ 76 | {A} | |
+ 77 | {A} | |
+ 78 | {A} | |
+ 79 | {A} | |
+ 80 | {A} | |
+ 81 | {A} | |
+ 82 | {A} | |
+ 83 | {A} | |
+ 84 | {A} | |
+ 85 | {A} | |
+ 86 | {A} | |
+ 87 | {A} | |
+ 88 | {A} | |
+ 89 | {A} | |
+ 90 | {A} | |
+ 91 | {A} | |
+ 92 | {A} | |
+ 93 | {A} | |
+ 94 | {A} | |
+ 95 | {A} | |
+ 96 | {A} | |
+ 97 | {A} | |
+ 98 | {A} | |
+ 99 | {A} | |
+ 100 | {A} | |
+ 101 | {A} | |
+ 102 | {A} | |
+ 103 | {A} | |
+ 104 | {A} | |
+ 105 | {A} | |
+(105 rows)
+
+-- Unlimited quantifier (A{10,})
+WITH test_unlimited AS (
+ SELECT i AS id, ARRAY['A'] AS flags
+ FROM generate_series(1, 15) i
+ UNION ALL
+ SELECT 16, ARRAY['B']
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_unlimited
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A{10,} B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 16
+ 2 | {A} | 2 | 16
+ 3 | {A} | 3 | 16
+ 4 | {A} | 4 | 16
+ 5 | {A} | 5 | 16
+ 6 | {A} | 6 | 16
+ 7 | {A} | |
+ 8 | {A} | |
+ 9 | {A} | |
+ 10 | {A} | |
+ 11 | {A} | |
+ 12 | {A} | |
+ 13 | {A} | |
+ 14 | {A} | |
+ 15 | {A} | |
+ 16 | {B} | |
+(16 rows)
+
+-- Min boundary (A{3,5})
+WITH test_min_boundary AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']), -- Min=3 reached, exit path available
+ (4, ARRAY['B']), -- Match ends at min
+ (5, ARRAY['A']),
+ (6, ARRAY['A']),
+ (7, ARRAY['A']),
+ (8, ARRAY['A']),
+ (9, ARRAY['A']), -- Count=5, max reached
+ (10, ARRAY['B']) -- Match ends at max
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_min_boundary
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A{3,5} B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | |
+ 3 | {A} | |
+ 4 | {B} | |
+ 5 | {A} | 5 | 10
+ 6 | {A} | 6 | 10
+ 7 | {A} | 7 | 10
+ 8 | {A} | |
+ 9 | {A} | |
+ 10 | {B} | |
+(10 rows)
+
+-- Max boundary exceeded (A{3,5})
+WITH test_max_boundary AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['A']),
+ (6, ARRAY['A']), -- Count=6 > max=5, row 1 context removed
+ (7, ARRAY['B']) -- Row 1 context: no match (exceeded max)
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_max_boundary
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A{3,5} B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | |
+ 2 | {A} | 2 | 7
+ 3 | {A} | 3 | 7
+ 4 | {A} | 4 | 7
+ 5 | {A} | |
+ 6 | {A} | |
+ 7 | {B} | |
+(7 rows)
+
+-- ============================================================
+-- Pathological Pattern Runtime Protection
+-- ============================================================
+-- Complex nested nullable ((A* B*)*) - Runtime protection
+WITH test_complex_nested AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['B']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_complex_nested
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A* B*)*)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | 2 | 4
+ 3 | {B} | 3 | 4
+ 4 | {B} | 4 | 4
+ 5 | {C} | |
+(5 rows)
+
+-- Nested nullable with quantifier ((A{0,3})*)
+WITH test_nested_quantifier AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_nested_quantifier
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A{0,3})*)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {A} | 2 | 3
+ 3 | {A} | 3 | 3
+ 4 | {B} | |
+(4 rows)
+
+-- ============================================================
+-- Alternation Runtime Behavior
+-- ============================================================
+-- Multi-branch alternation (A (B|C|D|E) F)
+WITH test_multi_branch AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['F']),
+ (4, ARRAY['A']),
+ (5, ARRAY['C']),
+ (6, ARRAY['F']),
+ (7, ARRAY['A']),
+ (8, ARRAY['D']),
+ (9, ARRAY['F']),
+ (10, ARRAY['A']),
+ (11, ARRAY['E']),
+ (12, ARRAY['F'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_multi_branch
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A (B | C | D | E) F)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags),
+ F AS 'F' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {B} | |
+ 3 | {F} | |
+ 4 | {A} | 4 | 6
+ 5 | {C} | |
+ 6 | {F} | |
+ 7 | {A} | 7 | 9
+ 8 | {D} | |
+ 9 | {F} | |
+ 10 | {A} | 10 | 12
+ 11 | {E} | |
+ 12 | {F} | |
+(12 rows)
+
+-- Alternation with quantifiers (A+ | B+ | C+)
+WITH test_alt_quantifiers AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['B']),
+ (6, ARRAY['C']),
+ (7, ARRAY['C']),
+ (8, ARRAY['C']),
+ (9, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_alt_quantifiers
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ | B+ | C+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {A} | 2 | 3
+ 3 | {A} | 3 | 3
+ 4 | {B} | 4 | 5
+ 5 | {B} | 5 | 5
+ 6 | {C} | 6 | 9
+ 7 | {C} | 7 | 9
+ 8 | {C} | 8 | 9
+ 9 | {C} | 9 | 9
+(9 rows)
+
+-- altPriority replacement (A B C | D)
+-- D branch (higher altPriority) matches first at row 1,
+-- then A B C branch (lower altPriority) replaces it at row 3.
+WITH test_alt_replace AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A', 'D']),
+ (2, ARRAY['B', '_']),
+ (3, ARRAY['C', '_']),
+ (4, ARRAY['_', '_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_alt_replace
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C | D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A,D} | 1 | 3
+ 2 | {B,_} | |
+ 3 | {C,_} | |
+ 4 | {_,_} | |
+(4 rows)
+
+-- ============================================================
+-- Deep Nested Groups
+-- ============================================================
+-- Three-level nesting ((((A B)+)+)+)
+WITH test_deep_nesting AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']),
+ (7, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_deep_nesting
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((((A B)+)+)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 6
+ 2 | {B} | |
+ 3 | {A} | 3 | 6
+ 4 | {B} | |
+ 5 | {A} | 5 | 6
+ 6 | {B} | |
+ 7 | {_} | |
+(7 rows)
+
+-- Multiple groups in nesting (((A B) (C D))+)
+WITH test_nested_sequential AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']),
+ (7, ARRAY['C']),
+ (8, ARRAY['D']),
+ (9, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_nested_sequential
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (((A B) (C D))+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 8
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {D} | |
+ 5 | {A} | 5 | 8
+ 6 | {B} | |
+ 7 | {C} | |
+ 8 | {D} | |
+ 9 | {_} | |
+(9 rows)
+
+-- Nested END→END max reached
+-- Inner group (A B){2} reaches max=2 → exits to outer END
+WITH test_end_nested_max AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_end_nested_max
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (((A B){2})+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 8
+ 2 | {B} | |
+ 3 | {A} | 3 | 6
+ 4 | {B} | |
+ 5 | {A} | 5 | 8
+ 6 | {B} | |
+ 7 | {A} | |
+ 8 | {B} | |
+ 9 | {_} | |
+(9 rows)
+
+-- Nested END→END between min/max
+-- Inner group (A B){1,3} exits between min/max → outer END count++
+WITH test_end_nested_mid AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_end_nested_mid
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (((A B){1,3})+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 8
+ 2 | {B} | |
+ 3 | {A} | 3 | 8
+ 4 | {B} | |
+ 5 | {A} | 5 | 8
+ 6 | {B} | |
+ 7 | {A} | 7 | 8
+ 8 | {B} | |
+ 9 | {_} | |
+(9 rows)
+
+-- ============================================================
+-- SKIP Options (Runtime)
+-- ============================================================
+-- SKIP PAST LAST ROW (non-overlapping matches)
+WITH test_skip_past AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_skip_past
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | |
+ 3 | {A} | |
+ 4 | {A} | |
+ 5 | {_} | |
+(5 rows)
+
+-- SKIP TO NEXT ROW (overlapping matches)
+WITH test_skip_next AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_skip_next
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | 2 | 4
+ 3 | {A} | 3 | 4
+ 4 | {A} | 4 | 4
+ 5 | {_} | |
+(5 rows)
+
+-- SKIP difference verification
+WITH test_skip_diff AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT 'SKIP PAST' AS mode, id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_skip_diff
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+)
+UNION ALL
+SELECT 'SKIP NEXT' AS mode, id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_skip_diff
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+)
+ORDER BY mode, id;
+ mode | id | flags | match_start | match_end
+-----------+----+-------+-------------+-----------
+ SKIP NEXT | 1 | {A} | 1 | 2
+ SKIP NEXT | 2 | {B} | |
+ SKIP NEXT | 3 | {A} | 3 | 4
+ SKIP NEXT | 4 | {B} | |
+ SKIP PAST | 1 | {A} | 1 | 2
+ SKIP PAST | 2 | {B} | |
+ SKIP PAST | 3 | {A} | 3 | 4
+ SKIP PAST | 4 | {B} | |
+(8 rows)
+
+-- ============================================================
+-- INITIAL Mode (Runtime)
+-- ============================================================
+-- INITIAL mode (not yet supported - produces syntax error)
+WITH test_initial_mode AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['_']), -- Unmatched
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['_']), -- Unmatched
+ (5, ARRAY['A'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_initial_mode
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+ERROR: syntax error at or near "AFTER"
+LINE 18: AFTER MATCH SKIP TO NEXT ROW
+ ^
+-- Default mode (include all rows)
+WITH test_default_mode AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['_']), -- Unmatched, but included
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['_']), -- Unmatched, but included
+ (5, ARRAY['A'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_default_mode
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {_} | |
+ 2 | {A} | 2 | 3
+ 3 | {A} | 3 | 3
+ 4 | {_} | |
+ 5 | {A} | 5 | 5
+(5 rows)
+
+-- Mode difference verification (INITIAL not yet supported - produces syntax error)
+WITH test_mode_diff AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['_']),
+ (2, ARRAY['A']),
+ (3, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT 'INITIAL' AS mode, COUNT(*) AS row_count
+FROM (
+ SELECT id FROM test_mode_diff
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A)
+ DEFINE A AS 'A' = ANY(flags)
+ )
+) sub
+UNION ALL
+SELECT 'DEFAULT' AS mode, COUNT(*) AS row_count
+FROM (
+ SELECT id FROM test_mode_diff
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A)
+ DEFINE A AS 'A' = ANY(flags)
+ )
+) sub
+ORDER BY mode;
+ERROR: syntax error at or near "AFTER"
+LINE 15: AFTER MATCH SKIP TO NEXT ROW
+ ^
+-- ============================================================
+-- Frame Boundary Variations
+-- ============================================================
+-- Very limited frame (1 FOLLOWING)
+WITH test_one_following AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']), -- Within 1 FOLLOWING
+ (3, ARRAY['A']), -- Beyond 1 FOLLOWING from row 1
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_one_following
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 2
+ 2 | {B} | |
+ 3 | {A} | 3 | 4
+ 4 | {B} | |
+(4 rows)
+
+-- Medium frame (10 FOLLOWING)
+WITH test_ten_following AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['A']),
+ (6, ARRAY['A']),
+ (7, ARRAY['A']),
+ (8, ARRAY['A']),
+ (9, ARRAY['A']),
+ (10, ARRAY['A']),
+ (11, ARRAY['B']), -- Within 10 FOLLOWING from row 1
+ (12, ARRAY['A']),
+ (13, ARRAY['B']) -- Beyond 10 FOLLOWING from row 1
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_ten_following
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 11
+ 2 | {A} | 2 | 11
+ 3 | {A} | 3 | 11
+ 4 | {A} | 4 | 11
+ 5 | {A} | 5 | 11
+ 6 | {A} | 6 | 11
+ 7 | {A} | 7 | 11
+ 8 | {A} | 8 | 11
+ 9 | {A} | 9 | 11
+ 10 | {A} | 10 | 11
+ 11 | {B} | |
+ 12 | {A} | 12 | 13
+ 13 | {B} | |
+(13 rows)
+
+-- Exact boundary match
+WITH test_exact_boundary AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['B']) -- Exactly at 4 FOLLOWING (frame end)
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_exact_boundary
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 4 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 5
+ 2 | {A} | 2 | 5
+ 3 | {A} | 3 | 5
+ 4 | {A} | 4 | 5
+ 5 | {B} | |
+(5 rows)
+
+-- ============================================================
+-- Special Partition Cases
+-- ============================================================
+-- Empty partition (0 rows)
+WITH test_empty_partition AS (
+ SELECT * FROM (VALUES
+ (1, 1, ARRAY['A']),
+ (2, 2, ARRAY['_']) -- Different partition
+ ) AS t(id, part, flags)
+)
+SELECT id, part, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_empty_partition
+WHERE part = 99 -- No rows match
+WINDOW w AS (
+ PARTITION BY part
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+ id | part | flags | match_start | match_end
+----+------+-------+-------------+-----------
+(0 rows)
+
+-- Single row partition
+WITH test_single_row AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_single_row
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 1
+(1 row)
+
+-- All rows fail matching (all DEFINE false)
+WITH test_all_fail AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_all_fail
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE
+ A AS false -- All rows fail
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | |
+ 2 | {A} | |
+ 3 | {A} | |
+(3 rows)
+
+-- Partition end with absorbable pattern
+-- SKIP PAST LAST ROW + unbounded frame + all rows match A
+-- Triggers absorb in !rowExists path at partition boundary.
+WITH test_absorb_partition_end AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['A'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_absorb_partition_end
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 5
+ 2 | {A} | |
+ 3 | {A} | |
+ 4 | {A} | |
+ 5 | {A} | |
+(5 rows)
+
+-- ============================================================
+-- DEFINE Special Cases
+-- ============================================================
+-- Undefined variable in DEFINE
+WITH test_undefined_var AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['X']), -- B not defined, defaults to TRUE
+ (3, ARRAY['C']),
+ (4, ARRAY['A']),
+ (5, ARRAY['_']), -- B defaults to TRUE, but no flags
+ (6, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_undefined_var
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ -- B is undefined, defaults to TRUE
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {X} | |
+ 3 | {C} | |
+ 4 | {A} | 4 | 6
+ 5 | {_} | |
+ 6 | {C} | |
+(6 rows)
+
+-- ============================================================
+-- Absorption Dynamic Flags
+-- ============================================================
+-- Partial absorbable pattern ((A+) B)
+WITH test_partial_absorbable AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_partial_absorbable
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A+) B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 4
+ 2 | {A} | 2 | 4
+ 3 | {A} | 3 | 4
+ 4 | {B} | |
+ 5 | {_} | |
+(5 rows)
+
+-- Dynamic flag update ((A+) | B)
+WITH test_dynamic_flags AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_dynamic_flags
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A+) | B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {A} | 2 | 3
+ 3 | {A} | 3 | 3
+ 4 | {B} | 4 | 4
+ 5 | {A} | 5 | 5
+ 6 | {B} | 6 | 6
+(6 rows)
+
+-- Non-absorbable context during absorption
+-- Pattern (A B)+ C: A,B in absorbable group, C is not.
+-- When END exits to C via nfa_state_create, isAbsorbable becomes false.
+WITH test_non_absorbable AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']),
+ (6, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_non_absorbable
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+ C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 5
+ 2 | {B} | |
+ 3 | {A} | |
+ 4 | {B} | |
+ 5 | {C} | |
+ 6 | {_} | |
+(6 rows)
+
+-- Absorption flags early return (!hasAbsorbableState)
+-- Pattern (A B)+ C D with SKIP PAST LAST ROW
+-- After reaching C (non-absorbable), hasAbsorbableState becomes false.
+-- On next row (D), the early return fires.
+WITH test_absorption_early_return AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']),
+ (6, ARRAY['D']),
+ (7, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_absorption_early_return
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+ C D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 6
+ 2 | {B} | |
+ 3 | {A} | |
+ 4 | {B} | |
+ 5 | {C} | |
+ 6 | {D} | |
+ 7 | {_} | |
+(7 rows)
+
+-- Coverage failure: older can't cover newer's states
+-- Pattern A+ | B+ with SKIP PAST LAST ROW.
+-- Row 1: only A → Ctx1 takes A branch only (B fails).
+-- Row 2: A and B → Ctx2 takes both branches.
+-- Absorption: Ctx1 has A but no B → can't cover Ctx2's B state → fails.
+WITH test_coverage_fail AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A', '_']),
+ (2, ARRAY['A', 'B']),
+ (3, ARRAY['A', '_']),
+ (4, ARRAY['A', '_']),
+ (5, ARRAY['_', '_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_coverage_fail
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ | B+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A,_} | 1 | 4
+ 2 | {A,B} | |
+ 3 | {A,_} | |
+ 4 | {A,_} | |
+ 5 | {_,_} | |
+(5 rows)
+
+-- Absorb skips completed context (older->states==NULL)
+-- Pattern A+ | B+ with SKIP PAST LAST ROW.
+-- Row 1: A only → Ctx1 takes A branch. Row 2: B only → Ctx1 A fails (completed).
+-- Ctx2 takes B branch. Absorption: Ctx1 states==NULL → skip.
+WITH test_older_completed AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_older_completed
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ | B+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 1
+ 2 | {B} | 2 | 3
+ 3 | {B} | |
+ 4 | {_} | |
+(4 rows)
+
+-- Absorb skips non-absorbable context (!hasAbsorbableState)
+-- Pattern A+ | B C with SKIP PAST LAST ROW (only A+ branch absorbable).
+-- Row 1: B only → Ctx1 takes B branch (non-absorbable), advances to C.
+-- Row 2: C,A → Ctx1 C matches (hasAbsorbableState=false). Ctx2 takes A (absorbable).
+-- Absorption: Ctx1 !hasAbsorbableState → skip.
+WITH test_older_non_absorbable AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['B', '_']),
+ (2, ARRAY['C', 'A']),
+ (3, ARRAY['_', 'A']),
+ (4, ARRAY['_', '_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_older_non_absorbable
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ | B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {B,_} | 1 | 2
+ 2 | {C,A} | |
+ 3 | {_,A} | 3 | 3
+ 4 | {_,_} | |
+(4 rows)
+
+-- ============================================================
+-- FIXME Issues - Known Limitations
+-- ============================================================
+-- FIXME 1 - altPriority lexical order
+WITH test_alt_priority_repeated AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A','B']), -- Both A and B match
+ (2, ARRAY['A','B']),
+ (3, ARRAY['A','B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_alt_priority_repeated
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A | B)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A,B} | 1 | 3
+ 2 | {A,B} | 2 | 3
+ 3 | {A,B} | 3 | 3
+(3 rows)
+
+-- FIXME 1 - Nested ALT lexical order
+WITH test_alt_priority_nested AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A','B']),
+ (2, ARRAY['C','D']),
+ (3, ARRAY['A','B']),
+ (4, ARRAY['C','D'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_alt_priority_nested
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (((A | B) (C | D))+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A,B} | 1 | 4
+ 2 | {C,D} | |
+ 3 | {A,B} | 3 | 4
+ 4 | {C,D} | |
+(4 rows)
+
+-- FIXME 2 - Cycle prevention at count > 0
+WITH test_cycle_nonzero AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']) -- Inner A* matches 0, cycles at count=3
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_cycle_nonzero
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A*)*)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 3
+ 2 | {A} | 2 | 3
+ 3 | {A} | 3 | 3
+ 4 | {B} | |
+(4 rows)
+
+-- FIXME 2 - Cycle with mixed nullables
+WITH test_cycle_mixed AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_cycle_mixed
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A* B*)*)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 2
+ 2 | {B} | 2 | 2
+ 3 | {A} | 3 | 3
+ 4 | {C} | |
+(4 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 549e9b2d7be..d9f879a7624 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -104,6 +104,11 @@ test: publication subscription
# ----------
test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite
+# ----------
+# Row Pattern Recognition tests
+# ----------
+test: rpr rpr_base rpr_explain rpr_nfa
+
# ----------
# Another group of parallel tests (JSON related)
# ----------
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
new file mode 100644
index 00000000000..7690c5611c0
--- /dev/null
+++ b/src/test/regress/sql/rpr.sql
@@ -0,0 +1,2180 @@
+--
+-- Test for row pattern definition clause
+--
+
+CREATE TEMP TABLE stock (
+ company TEXT,
+ tdate DATE,
+ price INTEGER
+);
+INSERT INTO stock VALUES ('company1', '2023-07-01', 100);
+INSERT INTO stock VALUES ('company1', '2023-07-02', 200);
+INSERT INTO stock VALUES ('company1', '2023-07-03', 150);
+INSERT INTO stock VALUES ('company1', '2023-07-04', 140);
+INSERT INTO stock VALUES ('company1', '2023-07-05', 150);
+INSERT INTO stock VALUES ('company1', '2023-07-06', 90);
+INSERT INTO stock VALUES ('company1', '2023-07-07', 110);
+INSERT INTO stock VALUES ('company1', '2023-07-08', 130);
+INSERT INTO stock VALUES ('company1', '2023-07-09', 120);
+INSERT INTO stock VALUES ('company1', '2023-07-10', 130);
+INSERT INTO stock VALUES ('company2', '2023-07-01', 50);
+INSERT INTO stock VALUES ('company2', '2023-07-02', 2000);
+INSERT INTO stock VALUES ('company2', '2023-07-03', 1500);
+INSERT INTO stock VALUES ('company2', '2023-07-04', 1400);
+INSERT INTO stock VALUES ('company2', '2023-07-05', 1500);
+INSERT INTO stock VALUES ('company2', '2023-07-06', 60);
+INSERT INTO stock VALUES ('company2', '2023-07-07', 1100);
+INSERT INTO stock VALUES ('company2', '2023-07-08', 1300);
+INSERT INTO stock VALUES ('company2', '2023-07-09', 1200);
+INSERT INTO stock VALUES ('company2', '2023-07-10', 1300);
+
+SELECT * FROM stock;
+
+-- basic test using PREV
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
+ nth_value(tdate, 2) OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- basic test using PREV. UP appears twice
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
+ nth_value(tdate, 2) OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+ UP+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- basic test using PREV. Use '*'
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
+ nth_value(tdate, 2) OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP* DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- basic test using PREV. Use '?'
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
+ nth_value(tdate, 2) OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP? DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- test using alternation (|) with sequence
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START (UP | DOWN))
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- test using alternation (|) with group quantifier
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START (UP | DOWN)+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- test using nested alternation
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START ((UP DOWN) | FLAT)+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price),
+ FLAT AS price = PREV(price)
+);
+
+-- test using group with quantifier
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((UP DOWN)+)
+ DEFINE
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- test using absolute threshold values (not relative PREV)
+-- HIGH: price > 150, LOW: price < 100, MID: neutral range
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOW MID* HIGH)
+ DEFINE
+ LOW AS price < 100,
+ MID AS price >= 100 AND price <= 150,
+ HIGH AS price > 150
+);
+
+-- test threshold-based pattern with alternation
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOW (MID | HIGH)+)
+ DEFINE
+ LOW AS price < 100,
+ MID AS price >= 100 AND price <= 150,
+ HIGH AS price > 150
+);
+
+-- basic test with none-greedy pattern
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A A A)
+ DEFINE
+ A AS price >= 140 AND price <= 150
+);
+
+-- test using {n} quantifier (A A A should be optimized to A{3})
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A{3})
+ DEFINE
+ A AS price >= 140 AND price <= 150
+);
+
+-- test using {n,} quantifier (2 or more)
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A{2,})
+ DEFINE
+ A AS price > 100
+);
+
+-- test using {n,m} quantifier (2 to 4)
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A{2,4})
+ DEFINE
+ A AS price > 100
+);
+
+-- last_value() should remain consistent
+SELECT company, tdate, price, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- omit "START" in DEFINE but it is ok because "START AS TRUE" is
+-- implicitly defined. per spec.
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
+ nth_value(tdate, 2) OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- the first row start with less than or equal to 100
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOWPRICE UP+ DOWN+)
+ DEFINE
+ LOWPRICE AS price <= 100,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- second row raises 120%
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOWPRICE UP+ DOWN+)
+ DEFINE
+ LOWPRICE AS price <= 100,
+ UP AS price > PREV(price) * 1.2,
+ DOWN AS price < PREV(price)
+);
+
+-- using NEXT
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UPDOWN)
+ DEFINE
+ START AS TRUE,
+ UPDOWN AS price > PREV(price) AND price > NEXT(price)
+);
+
+-- using AFTER MATCH SKIP TO NEXT ROW
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (START UPDOWN)
+ DEFINE
+ START AS TRUE,
+ UPDOWN AS price > PREV(price) AND price > NEXT(price)
+);
+
+-- match everything
+
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (A+)
+ DEFINE
+ A AS TRUE
+);
+
+-- nth_value beyond reduced frame (no IGNORE NULLS)
+-- Tests WinGetSlotInFrame/WinGetFuncArgInFrame out-of-frame with RPR
+SELECT company, tdate, price,
+ nth_value(price, 5) OVER w AS nth_5
+FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- backtracking with reclassification of rows
+-- using AFTER MATCH SKIP PAST LAST ROW
+SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (A+ B+)
+ DEFINE
+ A AS price > 100,
+ B AS price > 100
+);
+
+-- backtracking with reclassification of rows
+-- using AFTER MATCH SKIP TO NEXT ROW
+SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (A+ B+)
+ DEFINE
+ A AS price > 100,
+ B AS price > 100
+);
+
+-- SKIP TO NEXT ROW with limited frame (Ishii-san's test case)
+-- Each row should produce its own match within its frame
+WITH data AS (
+ SELECT * FROM (VALUES
+ ('A', 1), ('A', 2),
+ ('B', 3), ('B', 4)
+ ) AS t(gid, id)
+)
+SELECT gid, id, array_agg(id) OVER w
+FROM data
+WINDOW w AS (
+ PARTITION BY gid
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE A AS id < 10
+);
+
+-- Limited frame with absorption test
+-- Row 0: frame [0,2], can't see B at row 3 -> no match
+-- Row 1: frame [1,3], can see A A B -> should match rows 1-3
+WITH frame_absorb_test AS (
+ SELECT * FROM (VALUES
+ (0, 'A'), (1, 'A'), (2, 'A'), (3, 'B')
+ ) AS t(id, flag)
+)
+SELECT id, flag, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM frame_absorb_test
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS flag = 'A',
+ B AS flag = 'B'
+);
+
+-- ROWS BETWEEN CURRENT ROW AND offset FOLLOWING
+SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w,
+ count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+--
+-- Aggregates
+--
+
+-- using AFTER MATCH SKIP PAST LAST ROW
+SELECT company, tdate, price,
+ first_value(price) OVER w,
+ last_value(price) OVER w,
+ max(price) OVER w,
+ min(price) OVER w,
+ sum(price) OVER w,
+ avg(price) OVER w,
+ count(price) OVER w
+FROM stock
+WINDOW w AS (
+PARTITION BY company
+ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+AFTER MATCH SKIP PAST LAST ROW
+INITIAL
+PATTERN (START UP+ DOWN+)
+DEFINE
+START AS TRUE,
+UP AS price > PREV(price),
+DOWN AS price < PREV(price)
+);
+
+-- using AFTER MATCH SKIP TO NEXT ROW
+SELECT company, tdate, price,
+ first_value(price) OVER w,
+ last_value(price) OVER w,
+ max(price) OVER w,
+ min(price) OVER w,
+ sum(price) OVER w,
+ avg(price) OVER w,
+ count(price) OVER w
+FROM stock
+WINDOW w AS (
+PARTITION BY company
+ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+AFTER MATCH SKIP TO NEXT ROW
+INITIAL
+PATTERN (START UP+ DOWN+)
+DEFINE
+START AS TRUE,
+UP AS price > PREV(price),
+DOWN AS price < PREV(price)
+);
+
+-- JOIN case
+CREATE TEMP TABLE t1 (i int, v1 int);
+CREATE TEMP TABLE t2 (j int, v2 int);
+INSERT INTO t1 VALUES(1,10);
+INSERT INTO t1 VALUES(1,11);
+INSERT INTO t1 VALUES(1,12);
+INSERT INTO t2 VALUES(2,10);
+INSERT INTO t2 VALUES(2,11);
+INSERT INTO t2 VALUES(2,12);
+
+SELECT * FROM t1, t2 WHERE t1.v1 <= 11 AND t2.v2 <= 11;
+
+SELECT *, count(*) OVER w FROM t1, t2
+WINDOW w AS (
+ PARTITION BY t1.i
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE
+ A AS v1 <= 11 AND v2 <= 11
+);
+
+-- WITH case
+WITH wstock AS (
+ SELECT * FROM stock WHERE tdate < '2023-07-08'
+)
+SELECT tdate, price,
+first_value(tdate) OVER w,
+count(*) OVER w
+ FROM wstock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- ReScan test: LATERAL join forces WindowAgg rescan with RPR
+-- Tests ExecReScanWindowAgg clearing prev_slot/next_slot
+SELECT g.x, sub.*
+FROM generate_series(1, 2) g(x),
+LATERAL (
+ SELECT id, price, count(*) OVER w AS c
+ FROM (VALUES (1, 100), (2, 200), (3, 150)) AS t(id, price)
+ WHERE id <= g.x + 1
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (START UP+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price)
+ )
+) sub
+ORDER BY g.x, sub.id;
+
+-- PREV has multiple column reference
+CREATE TEMP TABLE rpr1 (id INTEGER, i SERIAL, j INTEGER);
+INSERT INTO rpr1(id, j) SELECT 1, g*2 FROM generate_series(1, 10) AS g;
+SELECT id, i, j, count(*) OVER w
+ FROM rpr1
+ WINDOW w AS (
+ PARTITION BY id
+ ORDER BY i
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (START COND+)
+ DEFINE
+ START AS TRUE,
+ COND AS PREV(i + j + 1) < 10
+);
+
+-- Smoke test for larger partitions.
+WITH s AS (
+ SELECT v, count(*) OVER w AS c
+ FROM (SELECT generate_series(1, 5000) v)
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ( r+ )
+ DEFINE r AS TRUE
+ )
+)
+-- Should be exactly one long match across all rows.
+SELECT * FROM s WHERE c > 0;
+
+WITH s AS (
+ SELECT v, count(*) OVER w AS c
+ FROM (SELECT generate_series(1, 5000) v)
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ( r )
+ DEFINE r AS TRUE
+ )
+)
+-- Every row should be its own match.
+SELECT count(*) FROM s WHERE c > 0;
+
+-- Large partition test: 100K rows with A+ B* C{10000,} pattern
+-- Tests that int32 count doesn't overflow with large repetitions
+WITH data AS (
+ SELECT generate_series(0, 100000) AS v
+),
+result AS (
+ SELECT v,
+ count(*) OVER w AS match_len,
+ first_value(v) OVER w AS match_first,
+ last_value(v) OVER w AS match_last
+ FROM data
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (A+ B* C{10000,})
+ DEFINE
+ A AS v < 33333,
+ B AS v >= 33333 AND v < 66666,
+ C AS v >= 66666 AND v < 99999
+ )
+)
+-- Should match: A (33333 rows) + B (33333 rows) + C (33333 rows) = 99999 rows
+SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
+
+--
+-- Using IGNORE NULLS
+--
+-- no NULL rows case. The result should be identical with "basic test using PREV"
+SELECT company, tdate, price, first_value(price) IGNORE NULLS OVER w,
+ last_value(price) IGNORE NULLS OVER w,
+ nth_value(tdate, 2) IGNORE NULLS OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- nth_value with IGNORE NULLS option wants to find the second row but
+-- due a NULL in the midlle, it returns the third row.
+WITH data AS (
+ SELECT * FROM (VALUES
+ (10, 1), (11, NULL), (12, 3), (13, 4)
+ ) AS t(gid, id))
+ SELECT gid, id, nth_value(id, 2) IGNORE NULLS OVER w AS second_val,
+ array_agg(id) OVER w
+ FROM data
+ WINDOW w AS (
+ ORDER BY gid
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS gid < 13
+ );
+
+-- nth_value with IGNORE NULLS option wants to find the third row but
+-- due a NULL in the midlle, it reaches the end of reduced frame and
+-- return NULL
+WITH data AS (
+ SELECT * FROM (VALUES
+ (10, 1), (11, NULL), (12, 3), (13, 4)
+ ) AS t(gid, id))
+ SELECT gid, id, nth_value(id, 3) IGNORE NULLS OVER w AS thrid_val,
+ array_agg(id) OVER w
+ FROM data
+ WINDOW w AS (
+ ORDER BY gid
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS gid < 13
+ );
+
+-- nth_value beyond reduced frame with IGNORE NULLS
+-- Tests ignorenulls_getfuncarginframe early out-of-frame check
+SELECT company, tdate, price,
+ nth_value(price, 5) IGNORE NULLS OVER w AS nth_5_in
+FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- View and pg_get_viewdef tests.
+CREATE TEMP VIEW v_window AS
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
+ nth_value(tdate, 2) OVER w AS nth_second
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+SELECT * FROM v_window;
+SELECT pg_get_viewdef('v_window');
+
+--
+-- Pattern optimization tests
+-- VIEW shows original pattern, EXPLAIN shows optimized pattern
+--
+
+-- Test: duplicate alternatives removal (A | B | A)+ -> (A | B)+
+CREATE TEMP VIEW v_opt_dup AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A | B | A)+)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_dup'); -- original: ((a | b | a)+)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_dup; -- optimized: ((a | b)+)
+
+-- Test: duplicate group removal ((A | B)+ | (A | B)+) -> (A | B)+
+CREATE TEMP VIEW v_opt_dup_group AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A | B)+ | (A | B)+)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_dup_group'); -- original: ((a | b)+ | (a | b)+)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_dup_group; -- optimized: ((a | b)+)
+
+-- Test: consecutive vars merge (A A A) -> A{3}
+CREATE TEMP VIEW v_opt_merge AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A A A)
+ DEFINE
+ A AS price >= 140 AND price <= 150
+);
+SELECT pg_get_viewdef('v_opt_merge'); -- original: (a a a)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge; -- optimized: a{3}
+
+-- Test: quantified vars merge (A A+ A) -> A{3,}
+CREATE TEMP VIEW v_opt_merge_quant AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A A+ A)
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_merge_quant'); -- original: (a a+ a)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_quant; -- optimized: a{3,}
+
+-- Test: merge two unbounded (A+ A+) -> A{2,}
+CREATE TEMP VIEW v_opt_merge_unbounded AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A+ A+)
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_merge_unbounded'); -- original: (a+ a+)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_unbounded; -- optimized: a{2,}
+
+-- Test: merge with zero-min (A* A+) -> A+
+CREATE TEMP VIEW v_opt_merge_star AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A* A+)
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_merge_star'); -- original: (a* a+)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_star; -- optimized: a+
+
+-- Test: complex merge (A A{2} A+ A{3}) -> A{7,}
+CREATE TEMP VIEW v_opt_merge_complex AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A A{2} A+ A{3})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_merge_complex'); -- original: (a a{2} a+ a{3})
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_complex; -- optimized: a{7,}
+
+-- Test: group merge ((A B) (A B)+) -> (A B){2,}
+CREATE TEMP VIEW v_opt_merge_group AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A B) (A B)+)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_merge_group'); -- original: ((a b) (a b)+)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group; -- expected: (a b){2,}
+
+-- Test: group merge A B (A B)+ -> (A B){2,}
+CREATE TEMP VIEW v_opt_merge_group2 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A B (A B)+)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_merge_group2'); -- original: (a b (a b)+)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group2; -- expected: (a b){2,}
+
+-- Test: group merge (A B) (A B)+ (A B) -> (A B){3,}
+CREATE TEMP VIEW v_opt_merge_group3 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A B) (A B)+ (A B))
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_merge_group3'); -- original: ((a b) (a b)+ (a b))
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group3; -- expected: (a b){3,}
+
+-- Test: group merge A B A B (A B)+ A B A B -> (A B){5,}
+CREATE TEMP VIEW v_opt_merge_group4 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A B A B (A B)+ A B A B)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_merge_group4'); -- original: (a b a b (a b)+ a b a b)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group4; -- expected: (a b){5,}
+
+-- Test: group merge C A B (A B)+ A B C -> C (A B){3,} C
+CREATE TEMP VIEW v_opt_merge_group5 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (C A B (A B)+ A B C)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100,
+ C AS price > 200
+);
+SELECT pg_get_viewdef('v_opt_merge_group5'); -- original: (c a b (a b)+ a b c)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group5; -- expected: c (a b){3,} c
+
+-- Test: consecutive GROUP merge (A B)+ (A B)+ -> (A B){2,}
+CREATE TEMP VIEW v_opt_merge_consec_group AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A B)+ (A B)+)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_merge_consec_group'); -- original: ((a b)+ (a b)+)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_consec_group; -- expected: (a b){2,}
+
+-- Test: consecutive GROUP merge with different quantifiers (A B){2} (A B){3} -> (A B){5}
+CREATE TEMP VIEW v_opt_merge_consec_group2 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A B){2} (A B){3})
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_merge_consec_group2'); -- original: ((a b){2} (a b){3})
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_consec_group2; -- expected: (a b){5}
+
+-- Test {n} quantifier display
+CREATE TEMP VIEW v_quantifier_n AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A{3})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_quantifier_n');
+
+-- Test {n,} quantifier display
+CREATE TEMP VIEW v_quantifier_n_plus AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A{2,})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_quantifier_n_plus');
+
+-- Test: flatten nested SEQ (A (B C)) -> A B C
+CREATE TEMP VIEW v_opt_flatten_seq AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A (B C))
+ DEFINE
+ A AS price > 100,
+ B AS price > 150,
+ C AS price < 150
+);
+SELECT pg_get_viewdef('v_opt_flatten_seq'); -- original: (a (b c))
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_flatten_seq; -- optimized: a b c
+
+-- Test: flatten nested ALT (A | (B | C)) -> (A | B | C)
+CREATE TEMP VIEW v_opt_flatten_alt AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A | (B | C))+)
+ DEFINE
+ A AS price > 200,
+ B AS price > 100,
+ C AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_flatten_alt'); -- original: ((a | (b | c))+)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_flatten_alt; -- optimized: ((a | b | c))+
+
+-- Test: unwrap GROUP{1,1} ((A)) -> A
+CREATE TEMP VIEW v_opt_unwrap_group AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (((A)))
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_unwrap_group'); -- original: (((a)))
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_unwrap_group; -- optimized: a
+
+-- Test: quantifier multiplication (A{2}){3} -> A{6}
+CREATE TEMP VIEW v_opt_quant_mult AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A{2}){3})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_quant_mult'); -- original: ((a{2}){3})
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult; -- optimized: a{6}
+
+-- Test: quantifier multiplication (A{2,4}){3} -> A{6,12}
+CREATE TEMP VIEW v_opt_quant_mult_range AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A{2,4}){3})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_quant_mult_range'); -- original: ((a{2,4}){3})
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_range; -- optimized: a{6,12}
+
+-- Test: quantifier multiplication blocked (A{2}){3,5} -> no change
+-- outer range with child exact > 1 causes gaps (6,8,10 not 6,7,8,9,10)
+CREATE TEMP VIEW v_opt_quant_mult_range2 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A{2}){3,5})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_quant_mult_range2'); -- original: ((a{2}){3,5})
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_range2; -- NOT optimized: (a{2}){3,5}
+
+-- Test: quantifier multiplication blocked by INF (A+){3} -> no change
+CREATE TEMP VIEW v_opt_quant_mult_inf AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A+){3})
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_quant_mult_inf'); -- original: ((a+){3})
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_inf; -- no multiply: (a+){3}
+
+-- Test: unwrap single-item ALT after duplicate removal (A | A) -> A
+CREATE TEMP VIEW v_opt_unwrap_alt AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((A | A)+)
+ DEFINE
+ A AS price > 100
+);
+SELECT pg_get_viewdef('v_opt_unwrap_alt'); -- original: ((a | a)+)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_unwrap_alt; -- optimized: a+
+
+-- Test: GROUP{1,1} to SEQ with flatten ((A B)(C D)) -> A B C D
+CREATE TEMP VIEW v_opt_group_to_seq AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (((A B)(C D)))
+ DEFINE
+ A AS price > 200,
+ B AS price > 150,
+ C AS price > 100,
+ D AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_group_to_seq'); -- original: (((a b)(c d)))
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_group_to_seq; -- optimized: a b c d
+
+-- Test: combined consecutive GROUP + prefix merge A B (A B)+ (A B)+ -> (A B){3,}
+CREATE TEMP VIEW v_opt_combined_merge AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A B (A B)+ (A B)+)
+ DEFINE
+ A AS price > 100,
+ B AS price <= 100
+);
+SELECT pg_get_viewdef('v_opt_combined_merge'); -- original: (a b (a b)+ (a b)+)
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_combined_merge; -- expected: (a b){3,}
+
+-- Test: nested ALT pattern - bug reproduction
+CREATE TEMP VIEW v_opt_nested_alt AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (((A B) | C) D | A B C)
+ DEFINE
+ A AS price <= 100,
+ B AS price <= 150,
+ C AS price <= 200,
+ D AS price > 200
+);
+SELECT pg_get_viewdef('v_opt_nested_alt');
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_nested_alt;
+
+-- Test: nested ALT with unbounded - A+ inside
+CREATE TEMP VIEW v_opt_nested_alt2 AS
+SELECT company, tdate, price, count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (((A+ B) | C) D | A B C)
+ DEFINE
+ A AS price <= 100,
+ B AS price <= 150,
+ C AS price <= 200,
+ D AS price > 200
+);
+SELECT pg_get_viewdef('v_opt_nested_alt2');
+EXPLAIN (COSTS OFF) SELECT * FROM v_opt_nested_alt2;
+
+--
+-- Error cases
+--
+
+-- row pattern definition variable name must not appear more than once
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price),
+ UP AS price > PREV(price)
+);
+
+-- subqueries in DEFINE clause are not supported
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START LOWPRICE)
+ DEFINE
+ START AS TRUE,
+ LOWPRICE AS price < (SELECT 100)
+);
+
+-- aggregates in DEFINE clause are not supported
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START LOWPRICE)
+ DEFINE
+ START AS TRUE,
+ LOWPRICE AS price < count(*)
+);
+
+-- FRAME must start at current row when row pattern recognition is used
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- EXCLUDE is not permitted
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ EXCLUDE CURRENT ROW
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- SEEK is not supported
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ SEEK
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(price),
+ DOWN AS price < PREV(price)
+);
+
+-- PREV's argument must have at least 1 column reference
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(1),
+ DOWN AS price < PREV(1)
+);
+
+-- Unsupported quantifier
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (START UP~ DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(1),
+ DOWN AS price < PREV(1)
+);
+
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (START UP+? DOWN+)
+ DEFINE
+ START AS TRUE,
+ UP AS price > PREV(1),
+ DOWN AS price < PREV(1)
+);
+
+-- Maximum pattern variables is 251 (RPR_VARID_MAX)
+
+-- Error: 252 variables exceeds limit of 251
+DO $$
+DECLARE
+ pattern_vars text;
+ define_vars text;
+ query text;
+BEGIN
+ SELECT string_agg('v' || lpad(i::text, 3, '0'), ' '),
+ string_agg('v' || lpad(i::text, 3, '0') || ' AS TRUE', ', ')
+ INTO pattern_vars, define_vars
+ FROM generate_series(1, 252) i;
+
+ query := format('SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (%s)
+ DEFINE %s)', pattern_vars, define_vars);
+
+ EXECUTE query;
+END;
+$$;
+
+-- Error: 253 variables exceeds limit of 251
+DO $$
+DECLARE
+ pattern_vars text;
+ define_vars text;
+ query text;
+BEGIN
+ SELECT string_agg('v' || lpad(i::text, 3, '0'), ' '),
+ string_agg('v' || lpad(i::text, 3, '0') || ' AS TRUE', ', ')
+ INTO pattern_vars, define_vars
+ FROM generate_series(1, 253) i;
+
+ query := format('SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (%s)
+ DEFINE %s)', pattern_vars, define_vars);
+
+ EXECUTE query;
+END;
+$$;
+
+ CREATE TEMP TABLE stock_null (company TEXT, tdate DATE, price INTEGER);
+ INSERT INTO stock_null VALUES ('c1', '2023-07-01', 100);
+ INSERT INTO stock_null VALUES ('c1', '2023-07-02', NULL); -- NULL in middle
+ INSERT INTO stock_null VALUES ('c1', '2023-07-03', 200);
+ INSERT INTO stock_null VALUES ('c1', '2023-07-04', 150);
+
+ SELECT company, tdate, price, count(*) OVER w AS match_count
+ FROM stock_null
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (START UP DOWN)
+ DEFINE START AS TRUE, UP AS price > PREV(price), DOWN AS price <
+PREV(price)
+ );
+
+
+-- Overlapping match tests (requires multi-context for correct behavior)
+-- Using array flags: 'X' = ANY(flags) for multi-TRUE support
+
+-- Test 1: A B C D E | B C D | C D E F - three overlapping patterns
+-- Different end points: B C D (4), A B C D E (5), C D E F (6)
+WITH test_overlap1 AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E']),
+ (6, ARRAY['F'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E | B C D | C D E F)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags),
+ F AS 'F' = ANY(flags)
+);
+
+WITH test_overlap1 AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E']),
+ (6, ARRAY['F'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C D E | B C D | C D E F)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags),
+ F AS 'F' = ANY(flags)
+);
+-- PAST LAST: only one match
+-- TO NEXT ROW with multi-context: three matches
+-- Row 1: A B C D E (1-5)
+-- Row 2: B C D (2-4) <- ends first!
+-- Row 3: C D E F (3-6) <- ends last!
+
+-- Test 1b: Longer pattern FAILS, shorter pattern should survive
+-- Pattern: A+ B C D E | B+ C
+-- A+ B C D E fails (no E found in sequence)
+-- B+ C matches at rows 2-3
+-- Result: match 2-3 (B+ C)
+WITH test_overlap1b AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1b
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B C D E | B+ C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+
+-- Test 2: A B+ C | B+ D - long B sequence with different endings
+WITH test_overlap2 AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['B']),
+ (5, ARRAY['B']),
+ (6, ARRAY['C']),
+ (7, ARRAY['B']),
+ (8, ARRAY['B']),
+ (9, ARRAY['B']),
+ (10, ARRAY['D'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap2
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B+ C | B+ D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+-- Current result (correct):
+-- Row 1: A B+ C (1-6)
+-- Row 7-9: B+ D (7-10, 8-10, 9-10)
+-- Note: Row 2-6 cannot match B+ D because Row 6 is C, not D
+-- With absorption: 8-10 and 9-10 would be absorbed by 7-10 (earlier context covers later)
+
+-- Test 3: Greedy quantifier with late failure - A B C+ D | A B
+-- Pattern expects D after C+, but E comes instead ("betrayal")
+WITH test_betrayal AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['C']),
+ (5, ARRAY['C']),
+ (6, ARRAY['E'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_betrayal
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C+ D | A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+-- A B C+ D fails at Row 6 (E instead of D)
+-- Question: Does it fallback to A B (1-2)?
+
+-- Test 4: Lexical Order test - A B C | A B C D E
+-- SQL standard: first matching alternative wins
+WITH test_lexical AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_lexical
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C | A B C D E)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+-- SQL standard Lexical Order: A B C (1-3) wins (first alternative)
+
+-- Test 4b: Reversed pattern order - A B C D E | A B C
+WITH test_lexical2 AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_lexical2
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E | A B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+-- SQL standard Lexical Order: A B C D E (1-5) wins (first alternative)
+
+-- Test 5: Multiple TRUE in single row (overlapping pattern variables)
+-- Each row matches multiple DEFINE conditions simultaneously
+WITH test_multi_true AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A','B']), -- A and B both TRUE
+ (2, ARRAY['B','C']), -- B and C both TRUE
+ (3, ARRAY['C','D']), -- C and D both TRUE
+ (4, ARRAY['D','E']), -- D and E both TRUE
+ (5, ARRAY['E','_']) -- E only
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_multi_true
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+-- Row 1: A=T, B=T -> matches A
+-- Row 2: B=T, C=T -> matches B
+-- Row 3: C=T, D=T -> matches C
+-- Row 4: D=T, E=T -> matches D
+-- Row 5: E=T -> matches E
+-- Result: match 1-5 (A B C D E)
+
+-- Test 6: Diagonal pattern with multi-TRUE (shifted overlap)
+WITH test_diagonal AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A','_']),
+ (2, ARRAY['B','A']),
+ (3, ARRAY['C','B']),
+ (4, ARRAY['D','C']),
+ (5, ARRAY['_','D'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_diagonal
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+-- Possible matches:
+-- Start Row 1: A(1) B(2) C(3) D(4) -> 1-4
+-- Start Row 2: A(2) B(3) C(4) D(5) -> 2-5 (because Row 2 has A too!)
+
+-- ===================================================================
+-- Context Absorption Tests
+-- ===================================================================
+
+-- Test absorption 1: Basic A+ pattern - later contexts absorbed by earlier
+WITH test_absorb_basic AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_basic
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE A AS 'A' = ANY(flags)
+);
+-- Pattern A+ is absorbable (unbounded first element, only one unbounded)
+-- 4 matches: (1-4, 2-4, 3-4, 4-4)
+
+-- Test absorption 2: A+ B pattern - absorption with fixed suffix
+WITH test_absorb_suffix AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_suffix
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+-- Pattern A+ B is absorbable (A+ unbounded first, B bounded suffix)
+-- All potential matches end at same row (row 4 with B)
+-- 3 matches: (1-4, 2-4, 3-4)
+
+-- Test absorption 3: Per-branch absorption with ALT (B+ C | B+ D)
+WITH test_absorb_alt AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['B']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['D']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_alt
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (B+ C | B+ D)
+ DEFINE
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+-- Both branches B+ C and B+ D are absorbable (B+ unbounded first)
+-- B+ D branch matches: 3 matches (1-4, 2-4, 3-4)
+
+-- Test absorption 4: Non-absorbable pattern (A B+ - unbounded not first)
+WITH test_no_absorb AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['B']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_no_absorb
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+-- Pattern A B+ is NOT absorbable (A bounded first, B+ unbounded but not first)
+-- Only Row 1 can start match (only row with A), so only one match: 1-4
+
+-- Test absorption 5: GROUP merge enables absorption
+WITH test_absorb_group AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']),
+ (7, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_group
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A B) (A B)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+-- Pattern optimized: (A B) (A B)+ -> (A B){2,}
+-- 2 matches: 1-6 (3 reps), 3-6 (2 reps)
+
+-- Test absorption 6: Multiple unbounded - first element unbounded enables absorption
+WITH test_multi_unbounded AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['B']),
+ (4, ARRAY['B']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_multi_unbounded
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+-- 2 matches: 1-4, 2-4 (same endpoint 4)
+
+-- ============================================
+-- Jacob's RPR Patterns (from jacob branch)
+-- ============================================
+
+-- Test: A? (optional, greedy)
+WITH jacob_optional AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_optional
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A?)
+ DEFINE A AS 'A' = ANY(flags)
+);
+-- Expected: 1-1 (matches A)
+
+-- Test: A{2} (exact count)
+WITH jacob_exact AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_exact
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2})
+ DEFINE A AS 'A' = ANY(flags)
+);
+-- Expected: 1-2
+
+-- Test: A{1,3} (bounded range, greedy)
+WITH jacob_bounded AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_bounded
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{1,3})
+ DEFINE A AS 'A' = ANY(flags)
+);
+-- Expected: 1-3 (greedy takes max), then 4-4
+
+-- Test: A | B (simple alternation)
+WITH jacob_simple_alt AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_simple_alt
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A | B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+-- Expected: 1-1 (A), 2-2 (B)
+
+-- Test: A | B | C (three-way alternation)
+WITH jacob_three_alt AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['B']),
+ (2, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_three_alt
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A | B | C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+-- Expected: 1-1 (B)
+
+-- Test: A B C (concatenation)
+WITH jacob_concat AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_concat
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+-- Expected: 1-3
+
+-- Test: A B? C (optional middle)
+WITH jacob_optional_mid AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['C']),
+ (3, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_optional_mid
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B? C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+-- Expected: 1-2 (A C, B skipped)
+
+-- Test: (A B){2} (nested group with quantifier)
+WITH jacob_nested_group AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_nested_group
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B){2})
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+-- Expected: 1-4 (A B A B)
+
+-- Test: (A){3} (quantifier on grouped single element)
+WITH jacob_group_quant AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_group_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A){3})
+ DEFINE A AS 'A' = ANY(flags)
+);
+-- Expected: 1-3
+
+-- Test: A B C | A B C D E (lexical order - first alt wins)
+WITH jacob_lex_first AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_lex_first
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C | A B C D E)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+-- Expected: 1-3 (A B C wins by lexical order)
+
+-- Test: A B C D E | A B C (lexical order - longer first wins)
+WITH jacob_lex_long AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_lex_long
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E | A B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+-- Expected: 1-5 (A B C D E wins by lexical order)
+
+-- ============================================
+-- Alternation with quantifiers (BUG cases from Jacob's tests)
+-- ============================================
+
+-- Test: (A | B)+ C - alternation inside quantified group followed by C
+WITH jacob_alt_quant AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_alt_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B)+ C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+-- Expected: 1-4 (A B A C)
+
+-- Test: ((A | B) C)+ - alternation inside group with outer quantifier
+WITH jacob_alt_group AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['C']),
+ (3, ARRAY['B']),
+ (4, ARRAY['C']),
+ (5, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_alt_group
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A | B) C)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+-- Expected: 1-4 (A C B C)
+
+-- ============================================
+-- RELUCTANT quantifiers (not yet supported)
+-- ============================================
+
+-- Test: A+? B (reluctant) - parser rejects with ERROR
+WITH jacob_reluctant AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+? B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+-- Expected: ERROR (reluctant quantifiers not yet supported)
+
+-- Test: A{1,3}? B (reluctant bounded) - parser rejects with ERROR
+WITH jacob_reluctant_bounded AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM jacob_reluctant_bounded
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{1,3}? B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+-- Expected: ERROR (reluctant quantifiers not yet supported)
+
+-- ============================================
+-- Nested quantifiers (pathological patterns)
+-- ============================================
+-- These patterns previously caused segfault or infinite loop.
+-- Now they are either optimized at compile time or handled safely at runtime.
+
+-- Test: (A*)* - nested unbounded quantifiers (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A*)*)
+ DEFINE A AS TRUE
+);
+
+-- Test: (A*)+ - inner nullable, outer requires one (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A*)+)
+ DEFINE A AS TRUE
+);
+
+-- Test: (A+)* - outer nullable (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A+)*)
+ DEFINE A AS TRUE
+);
+
+-- Test: (A+)+ - both require match (optimized to A+)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A+)+)
+ DEFINE A AS TRUE
+);
+
+-- Test: (A* B*)* - complex nested pattern (runtime protection)
+-- Not optimized but handled safely by empty-match loop prevention
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A* B*)*)
+ DEFINE A AS TRUE, B AS TRUE
+);
+
+-- Test: (((A)*)*)* - triple nested (optimized through recursive optimization)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 3) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((((A)*)*)*)
+ DEFINE A AS TRUE
+);
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
new file mode 100644
index 00000000000..879f5fe48a2
--- /dev/null
+++ b/src/test/regress/sql/rpr_base.sql
@@ -0,0 +1,3658 @@
+-- ============================================================
+-- RPR Base Tests
+-- Tests for Row Pattern Recognition (ISO/IEC 19075-5:2016)
+-- ============================================================
+--
+-- Parser Layer:
+-- Keyword Usage Tests
+-- DEFINE Clause Tests
+-- FRAME Options Tests
+-- PARTITION BY + FRAME Tests
+-- PATTERN Syntax Tests
+-- Quantifiers Tests
+-- Navigation Functions Tests
+-- SKIP TO / INITIAL Tests
+-- Serialization/Deserialization Tests
+-- Error Cases Tests
+--
+-- Planner Layer:
+-- Pattern Optimization Tests
+-- Absorption Flag Display Tests
+-- Absorption Analysis Tests
+-- Edge Case Tests
+-- Optimization Fallback Tests
+-- Planner Integration Tests
+-- Subquery and CTE Tests
+-- JOIN Tests
+-- Complex Expression Tests
+-- Set Operations Tests
+-- Sorting and Grouping Tests
+-- Stress Tests
+-- Error Limit Tests
+--
+-- Contributed Tests:
+-- Jacob's Patterns
+-- Pathological Patterns
+-- ============================================================
+
+SET client_min_messages = WARNING;
+
+-- ============================================================
+-- Keyword Usage Tests
+-- ============================================================
+
+-- RPR keywords as column names
+-- Keywords: define, initial, past, pattern, seek
+
+CREATE TABLE rpr_keywords (
+ id INT,
+ define INT, -- DEFINE keyword
+ initial INT, -- INITIAL keyword
+ past INT, -- PAST keyword
+ pattern INT, -- PATTERN keyword
+ seek INT, -- SEEK keyword
+ skip INT -- SKIP keyword (pre-existing)
+);
+
+INSERT INTO rpr_keywords VALUES (1, 10, 20, 30, 40, 50, 60);
+
+SELECT id, define, initial, past, pattern, seek, skip
+FROM rpr_keywords
+ORDER BY id;
+
+DROP TABLE rpr_keywords;
+
+-- ============================================================
+-- DEFINE Clause Tests
+-- ============================================================
+
+
+-- Simple column references
+CREATE TABLE stock_price (
+ dt DATE,
+ symbol TEXT,
+ price NUMERIC,
+ volume INT
+);
+
+INSERT INTO stock_price VALUES
+ ('2024-01-01', 'AAPL', 150, 1000),
+ ('2024-01-02', 'AAPL', 155, 1200),
+ ('2024-01-03', 'AAPL', 152, 900),
+ ('2024-01-04', 'AAPL', 160, 1500),
+ ('2024-01-05', 'AAPL', 158, 1100);
+
+-- Simple column reference
+SELECT dt, price, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+ PARTITION BY symbol
+ ORDER BY dt
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (UP+)
+ DEFINE UP AS price > 150
+)
+ORDER BY dt;
+
+-- Multiple column references
+SELECT dt, price, volume, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+ PARTITION BY symbol
+ ORDER BY dt
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (GOOD+)
+ DEFINE GOOD AS price > 150 AND volume > 1000
+)
+ORDER BY dt;
+
+-- Expression in DEFINE
+SELECT dt, price, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+ PARTITION BY symbol
+ ORDER BY dt
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (HIGH+)
+ DEFINE HIGH AS price * 1.1 > 165
+)
+ORDER BY dt;
+
+-- Arithmetic and functions
+SELECT dt, price, volume, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+ PARTITION BY symbol
+ ORDER BY dt
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (CALC+)
+ DEFINE CALC AS (price + volume / 100) > 160
+)
+ORDER BY dt;
+
+DROP TABLE stock_price;
+
+-- Auto-generated DEFINE
+CREATE TABLE rpr_auto (id INT, val INT);
+INSERT INTO rpr_auto VALUES (1, 10), (2, 20), (3, 30), (4, 15);
+
+-- One variable undefined (B auto-generated as "B IS TRUE")
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_auto
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ B*)
+ DEFINE A AS val > 15
+)
+ORDER BY id;
+
+-- Multiple undefined variables
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_auto
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B C)
+ DEFINE A AS val > 0
+ -- B and C auto-generated as "B IS TRUE", "C IS TRUE"
+)
+ORDER BY id;
+
+-- All variables defined explicitly
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_auto
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (X Y Z)
+ DEFINE
+ X AS val > 10,
+ Y AS val > 20,
+ Z AS val < 20
+)
+ORDER BY id;
+
+DROP TABLE rpr_auto;
+
+-- Duplicate variable names
+CREATE TABLE rpr_dup (id INT);
+INSERT INTO rpr_dup VALUES (1), (2);
+
+-- Duplicate DEFINE entries
+SELECT COUNT(*) OVER w
+FROM rpr_dup
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS id > 0, A AS id < 10
+);
+-- Expected: ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
+
+DROP TABLE rpr_dup;
+
+-- Boolean coercion
+CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
+INSERT INTO rpr_bool VALUES (1, true), (2, false);
+
+-- Non-boolean expression
+SELECT COUNT(*) OVER w
+FROM rpr_bool
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS id
+);
+-- Expected: ERROR: argument of DEFINE must be type boolean
+
+-- Boolean column reference
+SELECT id, flag, COUNT(*) OVER w as cnt
+FROM rpr_bool
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (T+)
+ DEFINE T AS flag
+)
+ORDER BY id;
+
+-- NULL::boolean
+SELECT id, COUNT(*) OVER w as cnt
+FROM rpr_bool
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (N+)
+ DEFINE N AS NULL::boolean
+)
+ORDER BY id;
+
+DROP TABLE rpr_bool;
+
+-- Complex expressions
+CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
+INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
+
+-- CASE expression
+SELECT id, val1, val2, COUNT(*) OVER w as cnt
+FROM rpr_complex
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (C+)
+ DEFINE C AS CASE WHEN val1 > 10 THEN val2 > 20 ELSE false END
+)
+ORDER BY id;
+
+DROP TABLE rpr_complex;
+
+-- Pattern variable not in PATTERN (should be ignored)
+CREATE TABLE rpr_unused (id INT);
+INSERT INTO rpr_unused VALUES (1), (2);
+
+-- Extra DEFINE variable
+SELECT id, COUNT(*) OVER w as cnt
+FROM rpr_unused
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS id > 0, B AS id > 5 -- B not in pattern
+)
+ORDER BY id;
+
+DROP TABLE rpr_unused;
+
+-- ============================================================
+-- FRAME Options Tests
+-- ============================================================
+
+
+CREATE TABLE rpr_frame (id INT, val INT);
+INSERT INTO rpr_frame VALUES
+ (1, 10), (2, 10), (3, 10), -- Same val: 10
+ (4, 20), (5, 20), -- Same val: 20
+ (6, 30);
+
+-- Valid frame options
+
+-- ROWS: counts physical rows (1 FOLLOWING = next 1 physical row)
+-- Expected result: Each row can see 1 physical row ahead
+-- id=1,2,3 (val=10): can see next row -> cnt=2
+-- id=4,5 (val=20): can see next row -> cnt=2
+-- id=6 (val=30): no next row -> cnt=1
+-- Result: [2,2,2,2,2,1]
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY val
+ ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B?)
+ DEFINE A AS val >= 0, B AS val >= 0
+)
+ORDER BY id;
+
+-- Invalid frame start positions
+
+-- Not starting at CURRENT ROW
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
+
+-- EXCLUDE options
+
+-- EXCLUDE not permitted
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ EXCLUDE CURRENT ROW
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+
+-- EXCLUDE GROUP not permitted
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ EXCLUDE GROUP
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+
+-- EXCLUDE TIES not permitted
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ EXCLUDE TIES
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: EXCLUDE options are not permitted when row pattern recognition is used
+
+-- RANGE frame not starting at CURRENT ROW
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+
+-- GROUPS frame not starting at CURRENT ROW
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
+
+-- Starting with N PRECEDING
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
+
+-- Starting with N FOLLOWING
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
+
+-- Frame end bound edge cases
+
+-- End before start: CURRENT ROW AND 1 PRECEDING
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 1 PRECEDING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: frame starting from current row cannot have preceding rows
+
+-- End before start: CURRENT ROW AND UNBOUNDED PRECEDING
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
+
+-- Single row frame: CURRENT ROW AND CURRENT ROW
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND CURRENT ROW
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Zero offset: CURRENT ROW AND 0 FOLLOWING (equivalent to CURRENT ROW)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 0 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Large offset: CURRENT ROW AND 1000 FOLLOWING
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 1000 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Maximum offset: CURRENT ROW AND 2147483646 FOLLOWING (INT_MAX - 1)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 2147483646 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- RANGE frame with RPR (not permitted)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY val
+ RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B?)
+ DEFINE A AS val >= 0, B AS val >= 0
+)
+ORDER BY id;
+-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+
+-- GROUPS frame with RPR (not permitted)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+ ORDER BY val
+ GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B?)
+ DEFINE A AS val >= 0, B AS val >= 0
+)
+ORDER BY id;
+-- Expected: ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
+
+DROP TABLE rpr_frame;
+
+-- ============================================================
+-- PARTITION BY + FRAME Tests
+-- ============================================================
+
+-- Test PARTITION BY with RPR to ensure proper partitioning behavior
+CREATE TABLE rpr_partition (id INT, grp INT, val INT);
+INSERT INTO rpr_partition VALUES
+ (1, 1, 10), (2, 1, 20), (3, 1, 30),
+ (4, 2, 15), (5, 2, 25), (6, 2, 35);
+
+-- PARTITION BY with ROWS frame
+SELECT id, grp, val, COUNT(*) OVER w as cnt
+FROM rpr_partition
+WINDOW w AS (
+ PARTITION BY grp
+ ORDER BY val
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B+)
+ DEFINE A AS val >= 10, B AS val > 15
+)
+ORDER BY id;
+-- Expected: Pattern matching should reset for each partition
+
+-- PARTITION BY with RANGE frame
+SELECT id, grp, val, COUNT(*) OVER w as cnt
+FROM rpr_partition
+WINDOW w AS (
+ PARTITION BY grp
+ ORDER BY val
+ RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B?)
+ DEFINE A AS val >= 10, B AS val >= 20
+)
+ORDER BY id;
+-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+
+DROP TABLE rpr_partition;
+
+-- ============================================================
+-- PATTERN Syntax Tests
+-- ============================================================
+
+
+CREATE TABLE rpr_pattern (id INT, val INT);
+INSERT INTO rpr_pattern VALUES
+ (1, 5), (2, 10), (3, 15), (4, 20), (5, 25),
+ (6, 30), (7, 35), (8, 40), (9, 45), (10, 50);
+
+-- Alternation (|)
+
+-- Multiple alternatives
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ | B+ | C+)
+ DEFINE A AS val > 35, B AS val BETWEEN 15 AND 35, C AS val < 15
+)
+ORDER BY id;
+
+-- Grouping
+
+-- Nested grouping with quantifier
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (((A B) C)+)
+ DEFINE A AS val > 10, B AS val > 20, C AS val > 30
+)
+ORDER BY id;
+
+-- Sequence
+
+-- Multi-element sequence
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B C D E)
+ DEFINE
+ A AS val < 15,
+ B AS val BETWEEN 15 AND 25,
+ C AS val BETWEEN 25 AND 35,
+ D AS val BETWEEN 35 AND 45,
+ E AS val >= 45
+)
+ORDER BY id;
+
+-- Complex combinations
+
+-- Alternation with grouping
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B) | (C D))
+ DEFINE A AS val < 20, B AS val >= 20, C AS val < 30, D AS val >= 30
+)
+ORDER BY id;
+
+-- Alternation + sequence + grouping
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (START (UP{2,} DOWN? | FLAT+) FINISH)
+ DEFINE
+ START AS val >= 0,
+ UP AS val > 20,
+ DOWN AS val <= 30,
+ FLAT AS val BETWEEN 25 AND 35,
+ FINISH AS val > 40
+)
+ORDER BY id;
+
+-- Nested alternation in groups
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B) (C | D))
+ DEFINE A AS val < 15, B AS val BETWEEN 15 AND 25, C AS val BETWEEN 25 AND 35, D AS val > 35
+)
+ORDER BY id;
+
+DROP TABLE rpr_pattern;
+
+-- ============================================================
+-- Quantifiers Tests
+-- ============================================================
+
+
+CREATE TABLE rpr_quant (id INT, val INT);
+INSERT INTO rpr_quant VALUES
+ (1, 10), (2, 20), (3, 30), (4, 40), (5, 50),
+ (6, 60), (7, 70), (8, 80), (9, 90), (10, 100);
+
+-- Basic greedy quantifiers
+
+-- * (zero or more)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A*)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- + (one or more)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 50
+)
+ORDER BY id;
+
+-- ? (zero or one)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A?)
+ DEFINE A AS val = 50
+)
+ORDER BY id;
+
+-- Edge case quantifiers
+
+-- {0} is not allowed (min must be >= 1)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{0} B)
+ DEFINE A AS val > 1000, B AS val > 0
+)
+ORDER BY id;
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+
+-- {0,0} is not allowed (max must be >= 1)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{0,0} B)
+ DEFINE A AS val > 1000, B AS val > 0
+)
+ORDER BY id;
+-- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
+
+-- {0,1} (equivalent to ?)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{0,1})
+ DEFINE A AS val = 50
+)
+ORDER BY id;
+
+-- Exact quantifiers {n}
+
+-- {3} (representative exact quantifier)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{3})
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Range quantifiers {n,}
+
+-- {2,} (representative n or more)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2,})
+ DEFINE A AS val > 40
+)
+ORDER BY id;
+
+-- Upper bound quantifiers {,m}
+
+-- {,3} (representative up to m)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,3})
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Range quantifiers {n,m}
+
+-- {3,7} (representative range)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{3,7})
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+DROP TABLE rpr_quant;
+
+-- Reluctant quantifiers (not yet supported)
+CREATE TABLE rpr_reluctant (id INT, val INT);
+INSERT INTO rpr_reluctant VALUES (1, 10), (2, 20), (3, 30);
+
+-- *? (zero or more, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A*?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+
+-- +? (one or more, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+
+-- ?? (zero or one, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A??)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+
+-- {n,}? (n or more, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2,}?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+
+-- {n,m}? (n to m, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{1,3}?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+
+-- {n}? (exactly n, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2}?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+
+-- {,m}? (up to m, reluctant) - COMPLETELY UNTESTED RULE!
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,3}?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+
+-- Invalid reluctant patterns (wrong token after quantifier)
+
+-- {2}+ (should be {2}? not {2}+)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2}+)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "+"
+
+-- {2,}* (should be {2,}? not {2,}*)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2,}*)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "*"
+
+-- {,3}* (should be {,3}? not {,3}*)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,3}*)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "*"
+
+-- {1,3}+ (should be {1,3}? not {1,3}+)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{1,3}+)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "+"
+
+-- Boundary errors in reluctant quantifiers
+
+-- {-1}? (negative bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{-1}?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "-"
+
+-- {2147483647}? (INT_MAX)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2147483647}?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+
+-- {-1,}? (negative lower bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{-1,}?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "-"
+
+-- {2147483647,}? (INT_MAX lower bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2147483647,}?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: quantifier bound must be between 0 and 2147483646
+
+-- {,0}? (zero upper bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,0}?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+
+-- {,2147483647}? (INT_MAX upper bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,2147483647}?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+
+-- {-1,3}? (negative lower in range)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{-1,3}?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "-"
+
+-- {1,2147483647}? (INT_MAX upper in range)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{1,2147483647}?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
+
+-- {5,3}? (min > max)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{5,3}?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: quantifier minimum bound must not exceed maximum
+
+-- Token-separated reluctant quantifiers (space between quantifier and ?)
+-- These may be tokenized differently by the lexer
+
+-- * ? (token separated)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A* ?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+
+-- + ? (token separated)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ ?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+
+-- {2,} ? (token separated)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2,} ?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+
+-- Invalid token combinations
+
+-- * + (invalid combination)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A* +)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "+"
+
+-- + * (invalid combination)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ *)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "*"
+
+-- ? ? (invalid combination)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A? ?)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: reluctant quantifiers are not yet supported
+
+DROP TABLE rpr_reluctant;
+
+-- Quantifier boundary conditions
+
+CREATE TABLE rpr_bounds (id INT);
+INSERT INTO rpr_bounds VALUES (1), (2);
+
+-- min > max
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{5,3})
+ DEFINE A AS id > 0
+);
+-- Expected: ERROR: quantifier minimum bound must not exceed maximum
+
+-- Large bounds
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{1000,2000})
+ DEFINE A AS id > 0
+);
+
+-- Very large bound
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{100000})
+ DEFINE A AS id > 0
+);
+
+-- INT_MAX - 1 = 2147483646 (at limit)
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2147483646})
+ DEFINE A AS id > 0
+);
+
+-- INT_MAX = 2147483647 (over limit)
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2147483647})
+ DEFINE A AS id > 0
+);
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+
+-- {n,} boundary errors
+
+-- Negative lower bound in {n,}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{-1,})
+ DEFINE A AS id > 0
+);
+-- Expected: ERROR: syntax error at or near "-"
+
+-- INT_MAX in {n,}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2147483647,})
+ DEFINE A AS id > 0
+);
+-- Expected: ERROR: quantifier bound must be between 0 and 2147483646
+
+-- {,m} boundary errors
+
+-- Zero upper bound in {,m}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,0})
+ DEFINE A AS id > 0
+);
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+
+-- INT_MAX in {,m}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{,2147483647})
+ DEFINE A AS id > 0
+);
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+
+DROP TABLE rpr_bounds;
+
+-- ============================================================
+-- Navigation Functions Tests (PREV / NEXT)
+-- ============================================================
+
+
+CREATE TABLE rpr_nav (id INT, val INT);
+INSERT INTO rpr_nav VALUES
+ (1, 10), (2, 20), (3, 15), (4, 25), (5, 30);
+
+-- PREV function - reference previous row in pattern
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE
+ A AS val > 0,
+ B AS val > PREV(val)
+)
+ORDER BY id;
+
+-- NEXT function - reference next row in pattern
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ B)
+ DEFINE
+ A AS val < NEXT(val),
+ B AS val > 0
+)
+ORDER BY id;
+
+-- Combined PREV and NEXT
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B C)
+ DEFINE
+ A AS val > 0,
+ B AS val > PREV(val) AND val < NEXT(val),
+ C AS val > PREV(val)
+)
+ORDER BY id;
+
+DROP TABLE rpr_nav;
+
+-- ============================================================
+-- SKIP TO / INITIAL Tests
+-- ============================================================
+
+
+CREATE TABLE rpr_skip (id INT, val INT);
+INSERT INTO rpr_skip VALUES
+ (1, 1), (2, 2), (3, 3), (4, 4), (5, 5),
+ (6, 6), (7, 7), (8, 8);
+
+-- SKIP TO NEXT ROW
+
+-- SKIP TO NEXT ROW
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_skip
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C)
+ DEFINE A AS val > 0, B AS val > 2, C AS val > 4
+)
+ORDER BY id;
+
+-- SKIP PAST LAST ROW
+
+-- SKIP PAST LAST ROW
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_skip
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS val > 0, B AS val > 2, C AS val > 4
+)
+ORDER BY id;
+
+-- Default behavior (should be SKIP PAST LAST ROW)
+
+-- No SKIP TO clause (default)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_skip
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B)
+ DEFINE A AS val > 0, B AS val > 1
+)
+ORDER BY id;
+
+-- Compare default with explicit PAST LAST ROW
+-- Results should be identical
+WITH default_skip AS (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_skip
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B C)
+ DEFINE A AS val > 0, B AS val > 2, C AS val > 4
+ )
+),
+explicit_skip AS (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_skip
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS val > 0, B AS val > 2, C AS val > 4
+ )
+)
+SELECT 'default' as type, * FROM default_skip
+UNION ALL
+SELECT 'explicit' as type, * FROM explicit_skip
+ORDER BY type, id;
+
+DROP TABLE rpr_skip;
+
+-- INITIAL clause
+
+CREATE TABLE rpr_init (id INT, val INT);
+INSERT INTO rpr_init VALUES (1, 10), (2, 20), (3, 30), (4, 40);
+
+-- Explicit INITIAL
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_init
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Implicit INITIAL (default)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_init
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+DROP TABLE rpr_init;
+
+-- SEEK
+
+CREATE TABLE rpr_seek (id INT, val INT);
+INSERT INTO rpr_seek VALUES (1, 10);
+
+-- SEEK keyword
+SELECT COUNT(*) OVER w
+FROM rpr_seek
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ SEEK
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR: SEEK is not supported
+-- HINT: Use INITIAL instead.
+
+DROP TABLE rpr_seek;
+
+-- ============================================================
+-- Serialization/Deserialization Tests
+-- ============================================================
+
+
+-- View creation and deparsing
+
+CREATE TABLE rpr_serial (id INT, val INT);
+INSERT INTO rpr_serial VALUES
+ (1, 10), (2, 20), (3, 15), (4, 25), (5, 30);
+
+-- Simple pattern
+CREATE VIEW rpr_serial_v1 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+
+-- Verify view works (tests deserialization)
+SELECT * FROM rpr_serial_v1 ORDER BY id;
+
+-- Verify deparsing
+SELECT pg_get_viewdef('rpr_serial_v1'::regclass);
+
+DROP VIEW rpr_serial_v1;
+
+-- Complex pattern with alternation
+CREATE VIEW rpr_serial_v2 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ | B*)
+ DEFINE A AS val > 20, B AS val <= 20
+);
+
+SELECT * FROM rpr_serial_v2 ORDER BY id;
+SELECT pg_get_viewdef('rpr_serial_v2'::regclass);
+
+DROP VIEW rpr_serial_v2;
+
+-- Pattern with grouping and quantifiers
+CREATE VIEW rpr_serial_v3 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B){2,5} | C*)
+ DEFINE
+ A AS val > 10,
+ B AS val > 20,
+ C AS val <= 10
+);
+
+SELECT * FROM rpr_serial_v3 ORDER BY id;
+SELECT pg_get_viewdef('rpr_serial_v3'::regclass);
+
+DROP VIEW rpr_serial_v3;
+
+-- All features combined
+CREATE VIEW rpr_serial_v4 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (START (MID{1,3} | ALT+) FINISH)
+ DEFINE
+ START AS val > 5,
+ MID AS val BETWEEN 10 AND 25,
+ ALT AS val > 25,
+ FINISH AS val > 15
+);
+
+SELECT * FROM rpr_serial_v4 ORDER BY id;
+SELECT pg_get_viewdef('rpr_serial_v4'::regclass);
+
+DROP VIEW rpr_serial_v4;
+
+-- Additional quantifiers for deparsing coverage
+
+-- ? quantifier (zero or one)
+CREATE VIEW rpr_serial_v5 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B?)
+ DEFINE A AS val > 10, B AS val > 20
+);
+
+SELECT * FROM rpr_serial_v5 ORDER BY id;
+SELECT pg_get_viewdef('rpr_serial_v5'::regclass);
+
+DROP VIEW rpr_serial_v5;
+
+-- {n,} quantifier (n or more)
+CREATE VIEW rpr_serial_v6 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2,})
+ DEFINE A AS val > 15
+);
+
+SELECT * FROM rpr_serial_v6 ORDER BY id;
+SELECT pg_get_viewdef('rpr_serial_v6'::regclass);
+
+DROP VIEW rpr_serial_v6;
+
+-- {n} quantifier (exactly n)
+CREATE VIEW rpr_serial_v7 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{3})
+ DEFINE A AS val > 0
+);
+
+SELECT * FROM rpr_serial_v7 ORDER BY id;
+SELECT pg_get_viewdef('rpr_serial_v7'::regclass);
+
+DROP VIEW rpr_serial_v7;
+
+-- Nested ALT pattern (tests deparse of complex nested structure)
+CREATE VIEW rpr_serial_v8 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (((A+ B) | C) D | A B C)
+ DEFINE A AS val <= 15, B AS val <= 25, C AS val <= 30, D AS val > 30
+);
+
+SELECT * FROM rpr_serial_v8 ORDER BY id;
+SELECT pg_get_viewdef('rpr_serial_v8'::regclass);
+
+DROP VIEW rpr_serial_v8;
+
+DROP TABLE rpr_serial;
+
+-- Materialized view (if supported)
+
+CREATE TABLE rpr_mview (id INT, val INT);
+INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30);
+
+CREATE MATERIALIZED VIEW rpr_mview_v1 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_mview
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+);
+
+SELECT * FROM rpr_mview_v1 ORDER BY id;
+SELECT pg_get_viewdef('rpr_mview_v1'::regclass);
+
+-- Refresh test
+REFRESH MATERIALIZED VIEW rpr_mview_v1;
+SELECT * FROM rpr_mview_v1 ORDER BY id;
+
+DROP MATERIALIZED VIEW rpr_mview_v1;
+DROP TABLE rpr_mview;
+
+-- Prepared statements (tests outfuncs.c / readfuncs.c)
+
+CREATE TABLE rpr_prep (id INT, val INT);
+INSERT INTO rpr_prep VALUES (1, 10), (2, 20), (3, 30);
+
+-- Simple prepared statement
+PREPARE rpr_prep_simple AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_prep
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+EXECUTE rpr_prep_simple;
+EXECUTE rpr_prep_simple;
+
+DEALLOCATE rpr_prep_simple;
+
+-- Prepared statement with parameters
+PREPARE rpr_prep_param(int) AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_prep
+WHERE id <= $1
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 10
+)
+ORDER BY id;
+
+EXECUTE rpr_prep_param(2);
+EXECUTE rpr_prep_param(3);
+
+DEALLOCATE rpr_prep_param;
+
+-- Complex prepared statement
+PREPARE rpr_prep_complex AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_prep
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A B){1,2} | C+)
+ DEFINE
+ A AS val > 5,
+ B AS val > 15,
+ C AS val <= 15
+)
+ORDER BY id;
+
+EXECUTE rpr_prep_complex;
+EXECUTE rpr_prep_complex;
+
+DEALLOCATE rpr_prep_complex;
+
+DROP TABLE rpr_prep;
+
+-- CTE and Subquery (tests copyfuncs.c)
+
+CREATE TABLE rpr_copy (id INT, val INT);
+INSERT INTO rpr_copy VALUES (1, 10), (2, 20), (3, 30), (4, 40);
+
+-- Simple CTE
+WITH rpr_cte AS (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_copy
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+)
+SELECT * FROM rpr_cte ORDER BY id;
+
+-- CTE with multiple references (forces node copy)
+WITH rpr_cte AS (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_copy
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 15
+ )
+)
+SELECT c1.id, c1.cnt as cnt1, c2.cnt as cnt2
+FROM rpr_cte c1
+JOIN rpr_cte c2 ON c1.id = c2.id
+ORDER BY c1.id;
+
+-- Subquery in FROM clause
+SELECT *
+FROM (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_copy
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B?)
+ DEFINE A AS val > 10, B AS val > 20
+ )
+) sub
+WHERE cnt > 0
+ORDER BY id;
+
+-- Nested subqueries
+SELECT *
+FROM (
+ SELECT *
+ FROM (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_copy
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val >= 10
+ )
+ ) inner_sub
+ WHERE cnt > 0
+) outer_sub
+ORDER BY id;
+
+DROP TABLE rpr_copy;
+
+-- DISTINCT and set operations (tests equalfuncs.c)
+
+CREATE TABLE rpr_equal (id INT, val INT);
+INSERT INTO rpr_equal VALUES (1, 10), (2, 20), (3, 10), (4, 20);
+
+-- DISTINCT with RPR
+SELECT DISTINCT cnt
+FROM (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WINDOW w AS (
+ ORDER BY val
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+) sub
+ORDER BY cnt;
+
+-- UNION with RPR in both sides
+SELECT id, val, cnt FROM (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WHERE val = 10
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+) sub1
+UNION
+SELECT id, val, cnt FROM (
+ SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WHERE val = 20
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+) sub2
+ORDER BY id;
+
+-- UNION ALL
+SELECT id, cnt FROM (
+ SELECT id, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 10
+ )
+) sub
+UNION ALL
+SELECT id, cnt FROM (
+ SELECT id, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (B+)
+ DEFINE B AS val <= 10
+ )
+) sub
+ORDER BY id, cnt;
+
+-- INTERSECT
+SELECT id, cnt FROM (
+ SELECT id, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WHERE id <= 3
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+) sub1
+INTERSECT
+SELECT id, cnt FROM (
+ SELECT id, COUNT(*) OVER w as cnt
+ FROM rpr_equal
+ WHERE id >= 2
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+) sub2
+ORDER BY id;
+
+DROP TABLE rpr_equal;
+
+-- View with multiple window definitions
+
+CREATE TABLE rpr_multiwin (id INT, val INT);
+INSERT INTO rpr_multiwin VALUES (1, 10), (2, 20), (3, 30);
+
+CREATE VIEW rpr_multiwin_v AS
+SELECT
+ id,
+ val,
+ COUNT(*) OVER w1 as cnt1,
+ COUNT(*) OVER w2 as cnt2
+FROM rpr_multiwin
+WINDOW
+ w1 AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 15
+ ),
+ w2 AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (B*)
+ DEFINE B AS val <= 15
+ );
+
+SELECT * FROM rpr_multiwin_v ORDER BY id;
+SELECT pg_get_viewdef('rpr_multiwin_v'::regclass);
+
+DROP VIEW rpr_multiwin_v;
+DROP TABLE rpr_multiwin;
+
+-- ============================================================
+-- Error Cases Tests
+-- ============================================================
+
+
+DROP TABLE IF EXISTS rpr_err;
+CREATE TABLE rpr_err (id INT, val INT);
+INSERT INTO rpr_err VALUES (1, 10), (2, 20);
+
+-- Syntax errors
+
+-- Invalid quantifier syntax
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+!)
+ DEFINE A AS val > 0
+);
+-- Expected: Syntax error
+
+-- Unmatched parentheses
+SET client_min_messages = NOTICE;
+DO $$
+BEGIN
+ EXECUTE 'SELECT COUNT(*) OVER w FROM rpr_err WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A B) DEFINE A AS val > 0, B AS val > 10)';
+ RAISE NOTICE 'Unmatched parentheses: UNEXPECTED SUCCESS';
+EXCEPTION
+ WHEN syntax_error THEN
+ RAISE NOTICE 'Unmatched parentheses: EXPECTED ERROR - %', SQLERRM;
+ WHEN OTHERS THEN
+ RAISE NOTICE 'Unmatched parentheses: UNEXPECTED ERROR - %', SQLERRM;
+END $$;
+SET client_min_messages = WARNING;
+
+-- Empty DEFINE
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE
+);
+-- Expected: Syntax error
+
+-- Empty PATTERN
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ()
+ DEFINE A AS val > 0
+);
+-- Expected: Syntax error
+
+-- DEFINE without PATTERN (PATTERN and DEFINE must be used together)
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ DEFINE A AS val > 0
+);
+-- Expected: Syntax error
+
+-- Qualified column references (NOT SUPPORTED)
+-- Pattern variables in DEFINE clause cannot use qualified references (A.price)
+-- This gives a confusing error about missing FROM-clause entry
+
+-- Qualified reference in DEFINE clause
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS A.val > 0
+);
+-- Expected: ERROR: missing FROM-clause entry for table "a"
+
+-- Semantic errors
+
+-- Undefined column in DEFINE
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS nonexistent_column > 0
+);
+-- Expected: ERROR: column "nonexistent_column" does not exist
+
+-- Type mismatch
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 'string'
+);
+-- Expected: ERROR: invalid input syntax for type integer: "string"
+
+-- Aggregate function in DEFINE (if not allowed)
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS COUNT(*) > 0
+);
+-- Expected: ERROR or works depending on implementation
+
+-- Subquery in DEFINE (NOT SUPPORTED)
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > (SELECT max(val) FROM rpr_err)
+);
+-- Expected: ERROR: cannot use subquery in DEFINE expression
+
+-- Edge cases
+
+-- Pattern variable not used (should work, extra vars ignored)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_err
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0, B AS val > 5, C AS val > 10
+)
+ORDER BY id;
+
+DROP TABLE rpr_err;
+
+-- NULL handling
+
+CREATE TABLE rpr_null (id INT, val INT);
+INSERT INTO rpr_null VALUES (1, 10), (2, NULL), (3, 30);
+
+-- NULL in DEFINE expression
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_null
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 15
+)
+ORDER BY id;
+
+-- IS NULL in DEFINE
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_null
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (N+)
+ DEFINE N AS val IS NULL
+)
+ORDER BY id;
+
+-- IS NOT NULL in DEFINE
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_null
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (NN+)
+ DEFINE NN AS val IS NOT NULL
+)
+ORDER BY id;
+
+DROP TABLE rpr_null;
+
+-- ============================================================
+-- Pattern Optimization Tests
+-- ============================================================
+-- Tests for pattern optimization in optimizer/plan/rpr.c
+-- Use EXPLAIN to verify optimized pattern (shown as "Pattern: ...")
+
+CREATE TABLE rpr_plan (id INT, val INT);
+INSERT INTO rpr_plan VALUES
+ (1, 10), (2, 20), (3, 30), (4, 40), (5, 50),
+ (6, 60), (7, 70), (8, 80), (9, 90), (10, 100);
+
+-- Consecutive VAR merge: A A A -> a{3}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A A A) DEFINE A AS val > 0);
+
+-- Consecutive VAR merge: A{2} A{3} -> a{5}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2} A{3}) DEFINE A AS val > 0);
+
+-- Consecutive VAR merge: A+ A* -> a+
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ A*) DEFINE A AS val > 0);
+
+-- Consecutive VAR merge: A A+ -> a{2,}
+-- Tests line 251: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars
+-- prev: A{1,1} (finite), child: A+ (infinite) triggers line 251 evaluation
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A A+) DEFINE A AS val > 0);
+
+-- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (((A B){5}) ((A B){10})) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Consecutive GROUP merge with unbounded: (A B)+ (A B)+ -> (a b){2,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B)+ (A B)+) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,}
+-- Tests line 325: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups
+-- prev: (A B){2,2} (finite), child: (A B)+ (infinite) triggers line 325 evaluation
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B){2} (A B)+) DEFINE A AS val <= 50, B AS val > 50);
+
+-- PREFIX merge: A B (A B)+ -> (a b){2,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B (A B)+) DEFINE A AS val <= 50, B AS val > 50);
+
+-- PREFIX and SUFFIX merge: A B (A B)+ A B -> (a b){3,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B (A B)+ A B) DEFINE A AS val <= 40, B AS val > 40);
+
+-- Flatten nested: A ((B) (C)) -> a b c
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A ((B) (C))) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+
+-- ALT flatten: (A | (B | C))+ -> (a | b | c)+
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | (B | C))+) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+
+-- ALT deduplicate: (A | B | A) -> (a | b)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B | A)+) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Quantifier multiply: (A{2}){3} -> a{6}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2}){3}) DEFINE A AS val > 0);
+
+-- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9}
+-- outer exact, child range - optimization applies
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2,3}){3}) DEFINE A AS val > 0);
+
+-- Quantifier NO multiply: (A{2}){2,3} stays as (a{2}){2,3}
+-- outer range - gaps would occur (4,6 not 4,5,6), no optimization
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2}){2,3}) DEFINE A AS val > 0);
+
+-- Quantifier NO multiply: (A{2}){2,} stays as (a{2}){2,}
+-- outer unbounded - gaps would occur (4,6,8,... not 4,5,6,...), no optimization
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2}){2,}) DEFINE A AS val > 0);
+
+-- Quantifier multiply: (A){2,} -> a{2,}
+-- child exact 1 - no gaps, optimization applies
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A){2,}) DEFINE A AS val > 0);
+
+-- Quantifier multiply: (A)+ -> a+
+-- child exact 1 - no gaps, optimization applies
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A)+) DEFINE A AS val > 0);
+
+-- Quantifier NO multiply: (A{2}){3,5} stays as (a{2}){3,5}
+-- outer range, child exact > 1 - gaps would occur (6,8,10 not 6,7,8,9,10)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2}){3,5}) DEFINE A AS val > 0);
+
+-- Quantifier NO multiply: (A{2,3}){2,3} stays as (a{2,3}){2,3}
+-- outer range, child range - gaps possible (e.g., (A{4,5}){2,3} misses 11)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2,3}){2,3}) DEFINE A AS val > 0);
+
+-- Nested unbounded: (A*)* -> a*
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A*)*) DEFINE A AS val > 0);
+
+-- Nested unbounded: (A+)* -> a*
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A+)*) DEFINE A AS val > 0);
+
+-- Nested unbounded: (A+)+ -> a+
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A+)+) DEFINE A AS val > 0);
+
+-- Unwrap GROUP{1,1}: (A) -> a
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A)) DEFINE A AS val > 0);
+
+-- Unwrap GROUP{1,1}: (A B) -> a b
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B)) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Combined optimization: A A (B B)+ B B C C C -> a{2} (b{2}){2,} c{3}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A A (B B)+ B B C C C)
+ DEFINE A AS val <= 20, B AS val > 20 AND val <= 70, C AS val > 70);
+
+-- Consecutive GROUP merge with unbounded: (A+) (A+) -> a{2,}
+-- Tests mergeConsecutiveGroups with child->max == INF
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A+) (A+)) DEFINE A AS val > 0);
+
+-- Consecutive GROUP merge finite: (A{10}){20} -> a{200}
+-- Tests mergeConsecutiveGroups with both finite
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{10}){20}) DEFINE A AS val > 0);
+
+-- Different GROUP prevents merge: (A B){2} (C D){3}
+-- Tests mergeConsecutiveGroups flush previous
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B){2} (C D){3})
+ DEFINE A AS val <= 25, B AS val > 25 AND val <= 50,
+ C AS val > 50 AND val <= 75, D AS val > 75);
+
+-- Different children count prevents merge: (A B)+ (A B C)+
+-- Tests rprPatternChildrenEqual length check
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B)+ (A B C)+)
+ DEFINE A AS val <= 33, B AS val > 33 AND val <= 66, C AS val > 66);
+
+-- PREFIX only merge: A B (A B)+ -> (a b){2,}
+-- Tests mergeGroupPrefixSuffix: absorb preceding elements into GROUP min
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B (A B)+) DEFINE A AS val <= 50, B AS val > 50);
+
+-- SUFFIX only merge: (A B)+ A B -> (a b){2,}
+-- Tests mergeGroupPrefixSuffix: absorb following elements into GROUP min
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B)+ A B) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Multiple SUFFIX absorption with skipUntil: (A B)+ A B A B C
+-- Tests mergeGroupPrefixSuffix: skip absorbed suffix elements
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B)+ A B A B C)
+ DEFINE A AS val <= 50, B AS val > 50 AND val <= 75, C AS val > 75);
+
+-- PREFIX merge with remaining prefix: A B C D (C D)+
+-- Tests mergeGroupPrefixSuffix: trimmed list reconstruction
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B C D (C D)+)
+ DEFINE A AS val <= 25, B AS val > 25 AND val <= 50,
+ C AS val > 50 AND val <= 75, D AS val > 75);
+
+-- PREFIX merge with quantifiers: A B* (A B*)+ -> (a b*){2,}
+-- Tests mergeGroupPrefixSuffix: quantifier comparison in rprPatternEqual
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B* (A B*)+)
+ DEFINE A AS val <= 50, B AS val > 50);
+
+-- PREFIX merge with multiple quantifiers: A+ B* C? (A+ B* C?)+ -> (a+ b* c?){2,}
+-- Tests mergeGroupPrefixSuffix: complex quantifier patterns
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ B* C? (A+ B* C?)+)
+ DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60);
+
+-- SUFFIX merge with quantifiers: (A B*)+ A B* -> (a b*){2,}
+-- Tests mergeGroupPrefixSuffix: suffix with quantifiers
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A B*)+ A B*)
+ DEFINE A AS val <= 50, B AS val > 50);
+
+-- Unwrap GROUP{1,1}: ((A | B | C)) -> (a | b | c)
+-- Tests tryUnwrapGroup removing redundant outer GROUP
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B | C)) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+
+-- ============================================================
+-- Absorption Flag Display Tests
+-- ============================================================
+-- Tests absorption marker display in EXPLAIN output
+-- Markers: ' = branch element, " = judgment point
+-- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern)
+
+-- Simple VAR: A+ -> a+" (judgment point)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0);
+
+-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B)+) DEFINE A AS val <= 50, B AS val > 50);
+
+-- ALT both absorbable: A+ | B+ -> (a+" | b+")
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ | B+) DEFINE A AS val <= 50, B AS val > 50);
+
+-- ALT one absorbable: A+ | B -> (a+" | b)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ | B) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Sequence with absorbable start: A+ B -> a+" b
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Complex nested: ((A+ B) | C) D | A B C - deeply nested ALT
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (((A+ B) | C) D | A B C)
+ DEFINE A AS val <= 30, B AS val <= 60, C AS val <= 80, D AS val > 80);
+
+-- Nested unbounded: (A+ | B)+ -> (a+" | b)+ (first iteration absorbable)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ | B)+)
+ DEFINE A AS val <= 50, B AS val > 50);
+
+-- ALT inside unbounded GROUP: (A+ B | A B)* -> (a+" b | a b)* (first iteration absorbable)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ B | A B)*)
+ DEFINE A AS val <= 50, B AS val > 50);
+
+-- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (A B+) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Non-absorbable (no unbounded branch): (A | B){2,} -> (a | b){2,} (no markers)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A | B){2,}) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Non-absorbable (SKIP TO NEXT ROW): A+ -> a+ (no markers)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW PATTERN (A+) DEFINE A AS val > 0);
+
+-- Non-absorbable (limited frame): A+ -> a+ (no markers)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0);
+
+-- ============================================================
+-- Absorption Analysis Tests
+-- ============================================================
+-- Tests context absorption optimization (O(n^2) -> O(n))
+-- Files: rpr.c (computeAbsorbability)
+
+-- Simple Absorbable Pattern: A+ B
+-- Pattern starts with unbounded VAR
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+
+-- Absorbable GROUP Pattern: (A B)+ C
+-- Pattern starts with unbounded GROUP
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+ C)
+ DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+
+-- Non-Absorbable: Unbounded Not at Start
+-- Pattern: A B+ (unbounded not at start)
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+
+-- ALT with Absorbable Branches
+-- Pattern: (A+ | B+) C - both branches absorbable
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A+ | B+) C)
+ DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+
+-- ALT with Mixed Branches
+-- Pattern: (A+ | B C) - only first branch absorbable
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A+ | B C)+)
+ DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+
+-- Non-Absorbable: ALT Inside GROUP
+-- Pattern: (A | B){2,} - ALT inside unbounded GROUP
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B){2,})
+ DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+
+-- Non-Absorbable: Nested Unbounded
+-- Pattern: ((A B)+ C)+ - nested GROUP structure
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A B)+ C)+)
+ DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+
+-- Non-Absorbable: Unbounded Element Inside GROUP
+-- Pattern: (A B+){2,} - unbounded inside GROUP
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B+){2,})
+ DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+
+-- Runtime Conditions: SKIP TO NEXT ROW
+-- Absorption disabled with SKIP TO NEXT ROW
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+
+-- Runtime Conditions: Limited Frame
+-- Absorption disabled with limited frame end
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 5 FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+
+-- ============================================================
+-- Edge Case Tests
+-- ============================================================
+-- Tests boundary conditions and complex scenarios
+
+-- Empty Match Prevention
+-- Pattern that could match empty: A*
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A*)
+ DEFINE A AS val > 1000 -- Never matches
+)
+ORDER BY id;
+
+-- All Rows Match
+-- Pattern where every row matches
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val >= 0 -- Always true
+)
+ORDER BY id;
+
+-- Large Quantifiers
+-- Pattern: A{100} (large exact quantifier)
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{100})
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Pattern: A{10,20} (large range quantifier)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{10,20})
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Complex Multi-Level Nesting
+-- Pattern: (((A B) | C)+ D)+
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((((A B) | C)+ D)+)
+ DEFINE A AS val <= 20, B AS val > 20 AND val <= 40,
+ C AS val > 40 AND val <= 60, D AS val > 60
+)
+ORDER BY id;
+
+-- Long Alternation Chain
+-- Pattern: A | B | C | D | E (5-way ALT)
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A | B | C | D | E)
+ DEFINE A AS val = 10, B AS val = 30, C AS val = 50,
+ D AS val = 70, E AS val = 90
+)
+ORDER BY id;
+
+-- Long Sequence
+-- Pattern: A B C D E F G H (8-element SEQ)
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B C D E F G H)
+ DEFINE A AS val >= 10, B AS val >= 20, C AS val >= 30,
+ D AS val >= 40, E AS val >= 50, F AS val >= 60,
+ G AS val >= 70, H AS val >= 80
+)
+ORDER BY id;
+
+-- Interleaved Quantifiers
+-- Pattern: A{2} B+ C{3,5} D* E{1,}
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A{2} B+ C{3,5} D* E{1,})
+ DEFINE A AS val > 0, B AS val > 0, C AS val > 0,
+ D AS val > 0, E AS val > 0
+)
+ORDER BY id;
+
+-- ============================================================
+-- Optimization Fallback Tests
+-- ============================================================
+-- Tests for optimization edge cases and fallback behavior
+
+CREATE TABLE rpr_fallback (id INT, val INT);
+INSERT INTO rpr_fallback VALUES (1, 10), (2, 20);
+
+-- Test: min quantifier overflow causes optimization fallback (min == max case)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2000000000}){2})
+ DEFINE A AS val > 0
+);
+-- Expected: Fallback - pattern not merged due to min overflow (4000000000 > INT32_MAX)
+
+-- Test: max-only quantifier overflow causes optimization fallback
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{1,2000000000}){2})
+ DEFINE A AS val > 0
+);
+-- Expected: Fallback - min OK (2*1=2), but max overflow (2*2000000000 > INT32_MAX)
+
+-- Test: max quantifier exceeds valid range (2147483647 = INT_MAX, limit is 2147483646)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2000000000,2147483647}){2})
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR at parse time before optimization
+
+-- Test: nested unbounded with large min causes overflow fallback
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A{2000000000,}){2000000000,})
+ DEFINE A AS val > 0
+);
+-- Expected: Fallback - min overflow (2000000000 * 2000000000 > INT32_MAX)
+
+-- Test: prefix mismatch causes optimization fallback
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B (C D)+)
+ DEFINE A AS val > 0, B AS val > 5, C AS val > 10, D AS val > 15
+);
+-- Expected: Fallback - prefix elements don't match GROUP content
+
+DROP TABLE rpr_fallback;
+
+-- ============================================================
+-- Planner Integration Tests
+-- ============================================================
+-- Tests full planning pipeline and WindowAgg plan node creation
+-- Files: planner.c, createplan.c
+
+CREATE TABLE rpr_planner (id INT, category VARCHAR(10), val INT);
+INSERT INTO rpr_planner VALUES
+ (1, 'A', 10), (2, 'A', 20), (3, 'A', 30),
+ (4, 'B', 40), (5, 'B', 50), (6, 'B', 60),
+ (7, 'C', 70), (8, 'C', 80), (9, 'C', 90);
+
+-- Multiple Window Functions in Same Query
+SELECT id, category, val,
+ COUNT(*) OVER w1 as cnt1,
+ COUNT(*) OVER w2 as cnt2
+FROM rpr_planner
+WINDOW w1 AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+),
+w2 AS (
+ PARTITION BY category
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (B+)
+ DEFINE B AS val >= 40
+)
+ORDER BY id;
+
+-- Window Function with PARTITION BY
+
+SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_planner
+WINDOW w AS (
+ PARTITION BY category
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY category, id;
+
+-- Window Function with Complex ORDER BY
+
+SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_planner
+WINDOW w AS (
+ ORDER BY category DESC, val ASC
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY category DESC, val ASC;
+
+-- Named Window Reference
+
+SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_planner
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Inline Window Definition
+
+SELECT id, category, val,
+ COUNT(*) OVER (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ) as cnt
+FROM rpr_planner
+ORDER BY id;
+
+-- Window with Aggregate Functions
+SELECT category,
+ COUNT(*) OVER w as window_cnt,
+ COUNT(*) as agg_cnt
+FROM rpr_planner
+WINDOW w AS (
+ PARTITION BY category
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+GROUP BY category
+ORDER BY category;
+-- Expected: ERROR (GROUP BY with window RPR not supported)
+
+-- ============================================================
+-- Subquery and CTE Tests
+-- Files: planner.c, prepjointree.c
+-- ============================================================
+-- Tests RPR with subqueries and CTEs
+
+-- RPR in Subquery (FROM clause)
+
+SELECT * FROM (
+ SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+ FROM rpr_planner
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+) sub
+WHERE cnt > 5
+ORDER BY id;
+
+-- RPR with Subquery in WHERE
+
+SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_planner
+WHERE val > (SELECT AVG(val) FROM rpr_planner)
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 50
+)
+ORDER BY id;
+
+-- CTE with RPR
+
+WITH rpr_cte AS (
+ SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+ FROM rpr_planner
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+)
+SELECT * FROM rpr_cte WHERE cnt > 5 ORDER BY id;
+
+-- Multiple CTE References
+
+WITH rpr_cte AS (
+ SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+ FROM rpr_planner
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+)
+SELECT c1.id, c1.cnt, c2.cnt as cnt2
+FROM rpr_cte c1
+JOIN rpr_cte c2 ON c1.id = c2.id
+ORDER BY c1.id;
+
+-- Nested CTEs
+
+WITH cte1 AS (
+ SELECT id, category, val FROM rpr_planner WHERE val > 30
+),
+cte2 AS (
+ SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+ FROM cte1
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+)
+SELECT * FROM cte2 ORDER BY id;
+
+-- ============================================================
+-- JOIN Tests
+-- Files: prepjointree.c, setrefs.c
+-- ============================================================
+-- Tests RPR with JOINs and multiple table references
+
+CREATE TABLE rpr_join1 (id INT, val1 INT);
+CREATE TABLE rpr_join2 (id INT, val2 INT);
+
+INSERT INTO rpr_join1 VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50);
+INSERT INTO rpr_join2 VALUES (1, 100), (2, 200), (3, 300), (4, 400), (5, 500);
+
+-- RPR After INNER JOIN
+
+SELECT t1.id, t1.val1, t2.val2,
+ COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+INNER JOIN rpr_join2 t2 ON t1.id = t2.id
+WINDOW w AS (
+ ORDER BY t1.id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val1 + val2 > 100
+)
+ORDER BY t1.id;
+
+-- RPR After LEFT JOIN
+
+SELECT t1.id, t1.val1, t2.val2,
+ COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+LEFT JOIN rpr_join2 t2 ON t1.id = t2.id
+WINDOW w AS (
+ ORDER BY t1.id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val1 > 0
+)
+ORDER BY t1.id;
+
+-- RPR with Multiple Tables in DEFINE
+
+SELECT t1.id, t1.val1, t2.val2,
+ COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+INNER JOIN rpr_join2 t2 ON t1.id = t2.id
+WINDOW w AS (
+ ORDER BY t1.id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ B)
+ DEFINE A AS t1.val1 > 20,
+ B AS t2.val2 > 200
+)
+ORDER BY t1.id;
+
+-- RPR After Cross Join
+
+SELECT t1.id as id1, t2.id as id2, t1.val1, t2.val2,
+ COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+CROSS JOIN rpr_join2 t2
+WHERE t1.id <= 2 AND t2.id <= 2
+WINDOW w AS (
+ ORDER BY t1.id, t2.id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val1 + val2 > 0
+)
+ORDER BY t1.id, t2.id;
+
+-- Self-Join with RPR
+
+SELECT a.id, a.val1, b.val1 as val1_next,
+ COUNT(*) OVER w as cnt
+FROM rpr_join1 a
+INNER JOIN rpr_join1 b ON a.id + 1 = b.id
+WINDOW w AS (
+ ORDER BY a.id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (X+)
+ DEFINE X AS a.val1 < b.val1
+)
+ORDER BY a.id;
+
+DROP TABLE rpr_join1, rpr_join2;
+
+-- ============================================================
+-- Complex Expression Tests
+-- Files: createplan.c, setrefs.c
+-- ============================================================
+-- Tests complex target list expressions
+
+CREATE TABLE rpr_target (id INT, val INT);
+INSERT INTO rpr_target VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50);
+
+-- Expressions in Target List
+
+SELECT id,
+ val * 2 as doubled,
+ val + 10 as added,
+ COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- CASE Expression in Target List
+
+SELECT id, val,
+ CASE
+ WHEN val < 30 THEN 'low'
+ WHEN val < 50 THEN 'medium'
+ ELSE 'high'
+ END as category,
+ COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Subquery in Target List
+
+SELECT id, val,
+ (SELECT MAX(val) FROM rpr_target) as max_val,
+ COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Function Calls in Target List
+
+SELECT id, val,
+ COALESCE(val, 0) as coalesced,
+ ABS(val - 30) as distance,
+ COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Column Aliases and References
+
+SELECT id as row_id,
+ val as value,
+ COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY row_id;
+
+DROP TABLE rpr_target;
+
+-- ============================================================
+-- Set Operations Tests
+-- Files: planner.c
+-- ============================================================
+-- Tests RPR with UNION, INTERSECT, EXCEPT
+
+CREATE TABLE rpr_set1 (id INT, val INT);
+CREATE TABLE rpr_set2 (id INT, val INT);
+
+INSERT INTO rpr_set1 VALUES (1, 10), (2, 20), (3, 30);
+INSERT INTO rpr_set2 VALUES (2, 20), (3, 30), (4, 40);
+
+-- UNION with RPR
+
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+UNION
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+ORDER BY id;
+
+-- UNION ALL with RPR
+
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+UNION ALL
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+ORDER BY id, val;
+
+-- INTERSECT with RPR
+
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+INTERSECT
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+ORDER BY id;
+
+-- EXCEPT with RPR
+
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+EXCEPT
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ ))
+ORDER BY id;
+
+DROP TABLE rpr_set1, rpr_set2;
+
+-- ============================================================
+-- Sorting and Grouping Tests
+-- Files: planner.c, createplan.c
+-- ============================================================
+-- Tests RPR interaction with sorting and grouping
+
+CREATE TABLE rpr_sort (id INT, category VARCHAR(10), val INT);
+INSERT INTO rpr_sort VALUES
+ (1, 'A', 30), (2, 'B', 20), (3, 'A', 10),
+ (4, 'B', 40), (5, 'A', 50), (6, 'B', 60);
+
+-- RPR with GROUP BY
+
+SELECT category,
+ COUNT(*) as group_cnt,
+ MAX(val) as max_val,
+ COUNT(*) OVER w as window_cnt
+FROM rpr_sort
+GROUP BY category
+WINDOW w AS (
+ ORDER BY category
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS COUNT(*) > 0
+)
+ORDER BY category;
+
+-- RPR with HAVING
+
+SELECT category,
+ COUNT(*) as group_cnt,
+ COUNT(*) OVER w as window_cnt
+FROM rpr_sort
+GROUP BY category
+HAVING COUNT(*) > 2
+WINDOW w AS (
+ ORDER BY category
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS COUNT(*) > 0
+)
+ORDER BY category;
+
+-- RPR with DISTINCT
+
+SELECT DISTINCT category,
+ COUNT(*) OVER w as cnt
+FROM rpr_sort
+WINDOW w AS (
+ PARTITION BY category
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY category;
+
+-- RPR with ORDER BY (different from window ORDER BY)
+
+SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_sort
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY val DESC;
+
+-- RPR with LIMIT and OFFSET
+
+SELECT id, category, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_sort
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id
+LIMIT 3 OFFSET 1;
+
+DROP TABLE rpr_sort;
+
+DROP TABLE rpr_planner;
+
+-- ============================================================
+-- Stress Tests
+-- ============================================================
+-- Edge cases and stress scenarios
+
+CREATE TABLE rpr_stress (id INT, val INT);
+INSERT INTO rpr_stress SELECT i, i * 10 FROM generate_series(1, 20) i;
+
+-- Very Long Query with Many Windows
+SELECT id, val,
+ COUNT(*) OVER w1 as cnt1,
+ COUNT(*) OVER w2 as cnt2,
+ COUNT(*) OVER w3 as cnt3
+FROM rpr_stress
+WINDOW w1 AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+),
+w2 AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (B+)
+ DEFINE B AS val > 50
+),
+w3 AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (C+)
+ DEFINE C AS val > 100
+)
+ORDER BY id;
+
+-- Deeply Nested Subqueries with RPR
+
+SELECT * FROM (
+ SELECT * FROM (
+ SELECT * FROM (
+ SELECT id, val,
+ COUNT(*) OVER w as cnt
+ FROM rpr_stress
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+ )
+ ) sub1
+ ) sub2
+) sub3
+WHERE cnt > 10
+ORDER BY id;
+
+-- Complex Expression in DEFINE Clause
+
+SELECT id, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_stress
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+ B)
+ DEFINE A AS (val % 3 = 0 OR val % 5 = 0),
+ B AS (val * 2 > 100 AND val / 2 < 100)
+)
+ORDER BY id;
+
+-- Window with No Matching Rows
+
+SELECT id, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_stress
+WHERE val > 1000 -- No rows match
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Window on Single Row
+
+SELECT id, val,
+ COUNT(*) OVER w as cnt
+FROM rpr_stress
+WHERE id = 10
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > 0
+)
+ORDER BY id;
+
+DROP TABLE rpr_stress;
+
+-- ============================================================
+-- Error Limit Tests
+-- ============================================================
+-- Tests for error conditions in rpr.c
+
+CREATE TABLE rpr_errors (id INT, val INT);
+INSERT INTO rpr_errors VALUES (1, 10), (2, 20);
+
+-- Test: PATTERN variable without DEFINE (A), DEFINE variable not in PATTERN (B)
+SELECT id, val, COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A)
+ DEFINE
+ B AS TRUE
+);
+-- Expected: Success - A is implicitly TRUE, B is filtered out
+
+-- Test: 3 variables in PATTERN, 253 in DEFINE (DEFINE filtering test)
+SELECT COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (V1 V2 V3)
+ DEFINE
+ V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
+ V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
+ V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0,
+ V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0,
+ V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0,
+ V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0,
+ V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0,
+ V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0,
+ V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0,
+ V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0,
+ V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0,
+ V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0,
+ V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0,
+ V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0,
+ V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0,
+ V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0,
+ V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0,
+ V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0,
+ V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0,
+ V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0,
+ V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
+ V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
+ V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
+ V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
+ V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
+ V251 AS val > 0, V252 AS val > 0, V253 AS val > 0
+);
+-- Expected: Success - unused DEFINE variables are filtered out
+
+-- Test: 251 variables in PATTERN, 252 in DEFINE (boundary - should succeed)
+SELECT COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251)
+ DEFINE
+ V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
+ V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
+ V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0,
+ V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0,
+ V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0,
+ V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0,
+ V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0,
+ V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0,
+ V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0,
+ V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0,
+ V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0,
+ V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0,
+ V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0,
+ V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0,
+ V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0,
+ V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0,
+ V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0,
+ V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0,
+ V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0,
+ V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0,
+ V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
+ V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
+ V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
+ V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
+ V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
+ V251 AS val > 0, V252 AS val > 0
+);
+-- Expected: Success - unused DEFINE variables are filtered out
+
+-- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
+SELECT COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251 V252)
+ DEFINE
+ V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
+ V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
+ V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0,
+ V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0,
+ V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0,
+ V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0,
+ V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0,
+ V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0,
+ V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0,
+ V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0,
+ V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0,
+ V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0,
+ V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0,
+ V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0,
+ V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0,
+ V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0,
+ V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0,
+ V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0,
+ V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0,
+ V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0,
+ V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
+ V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
+ V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
+ V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
+ V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
+ V251 AS val > 0
+);
+-- Expected: ERROR - too many pattern variables (Maximum is 251)
+
+-- Test: Pattern nesting at maximum depth (depth 253)
+-- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
+SELECT id, val, COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+ DEFINE A AS val > 0
+);
+-- Expected: Should succeed
+
+-- Test: Pattern nesting depth exceeds maximum (depth 254)
+-- Note: 254 nested GROUP{3,7} quantifiers produce depth 254 after optimization
+SELECT id, val, COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+ DEFINE A AS val > 0
+);
+-- Expected: ERROR - pattern nesting too deep
+
+DROP TABLE rpr_errors;
+
+-- ============================================================
+-- Jacob's Patterns
+-- ============================================================
+-- Basic pattern matching tests from jacob branch
+
+-- Test: A? (optional, greedy)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A?)
+ DEFINE A AS val > 50
+);
+
+-- Test: A{2} (exact count)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2})
+ DEFINE A AS val <= 50
+);
+
+-- Test: A{1,3} (bounded range, greedy)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{1,3})
+ DEFINE A AS val <= 50
+);
+
+-- Test: A | B (simple alternation)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A | B)
+ DEFINE A AS val <= 30, B AS val > 70
+);
+
+-- Test: A | B | C (three-way alternation)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A | B | C)
+ DEFINE A AS val <= 20, B AS val BETWEEN 40 AND 60, C AS val > 80
+);
+
+-- Test: A B C (concatenation)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 60
+);
+
+-- Test: A B? C (optional middle)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B? C)
+ DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 60
+);
+
+-- Test: (A B)+ (grouped quantifier)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+)
+ DEFINE A AS val <= 50, B AS val > 50
+);
+
+-- Test: (A | B)+ C (alternation with quantifier)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B)+ C)
+ DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 80
+);
+
+-- Test: (A+ | (A | B)+)* - nested alternation inside quantified group
+-- Previously caused infinite recursion in nfa_advance_alt when the inner
+-- BEGIN(+)'s skip jump was followed as an ALT branch pointer.
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM (VALUES
+ (1, ARRAY['A', 'B']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C'])
+) AS t(id, flags)
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A+ | (A | B)+)*)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- ============================================================
+-- Pathological Patterns
+-- ============================================================
+-- These patterns previously caused issues. Now optimized or handled safely.
+
+-- Test: (A*)* - nested unbounded (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A*)*)
+ DEFINE A AS TRUE
+);
+
+-- Test: (A*)+ - inner nullable (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A*)+)
+ DEFINE A AS TRUE
+);
+
+-- Test: (A+)* - outer nullable (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A+)*)
+ DEFINE A AS TRUE
+);
+
+-- Test: (A+)+ - both require match (optimized to A+)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 5) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((A+)+)
+ DEFINE A AS TRUE
+);
+
+-- Test: (((A)*)*)* - triple nested (optimized to A*)
+SELECT v, count(*) OVER w AS c
+FROM (SELECT generate_series(1, 3) v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN ((((A)*)*)*)
+ DEFINE A AS TRUE
+);
+
+-- Optional group with alternation: A ((B | C) (D | E))* F?
+-- When only A matches, the * group matches 0 times and F? matches 0 times
+SELECT id, val, match_len
+FROM (SELECT id, val,
+ COUNT(*) OVER w AS match_len
+ FROM (VALUES (1, 1), (2, 99)) AS t(id, val)
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A ((B | C) (D | E))* F?)
+ DEFINE A AS val = 1,
+ B AS val = 2, C AS val = 3,
+ D AS val = 4, E AS val = 5,
+ F AS val = 6
+ )
+) s;
+
+DROP TABLE rpr_plan;
+
+-- ============================================================
+-- End of rpr_base.sql
+-- ============================================================
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
new file mode 100644
index 00000000000..f8c8f62e594
--- /dev/null
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -0,0 +1,2254 @@
+-- ============================================================
+-- RPR EXPLAIN Tests
+-- Tests for Row Pattern Recognition EXPLAIN output
+-- ============================================================
+--
+-- This test suite validates EXPLAIN output for RPR queries,
+-- including NFA statistics shown in EXPLAIN ANALYZE:
+-- - NFA States: peak, total, merged
+-- - NFA Contexts: peak, total, absorbed, skipped
+-- - NFA: matched (len min/max/avg), mismatched (len min/max/avg)
+-- - Pattern deparse formatting
+-- - Multiple output formats (text, JSON, XML)
+--
+-- Test Coverage:
+-- Basic NFA Statistics Tests
+-- State Statistics Tests
+-- Context Statistics Tests
+-- Match Length Statistics Tests
+-- Mismatch Length Statistics Tests
+-- JSON Format Tests
+-- XML Format Tests
+-- Multiple Partitions Tests
+-- Edge Cases
+-- Complex Pattern Tests
+-- Real-world Pattern Examples
+-- Performance-oriented Tests
+-- INITIAL vs no INITIAL comparison
+-- Quantifier Variations
+-- Regression Tests for Statistics Accuracy
+-- Alternation Pattern Tests
+-- Group Pattern Tests
+-- Window Function Combinations
+-- DEFINE Expression Variations
+-- Large Scale Statistics Verification
+-- ============================================================
+
+-- Filter function to normalize Storage memory values only (not NFA statistics).
+-- NFA statistics should not change between platforms; if they do, it could
+-- indicate issues such as uninitialized memory access.
+-- Works for text, JSON, and XML formats.
+create function rpr_explain_filter(text) returns setof text
+language plpgsql as
+$$
+declare
+ ln text;
+begin
+ for ln in execute $1
+ loop
+ -- Normalize memory size in Storage line only (platform-dependent)
+ -- Keep NFA statistics numbers unchanged (they are test assertions)
+
+ -- Text format: "Storage: Memory Maximum Storage: 18kB"
+ if ln ~ 'Storage:.*Maximum Storage:' then
+ ln := regexp_replace(ln, '\m\d+kB', 'NkB', 'g');
+ end if;
+
+ -- JSON format: "Maximum Storage": 17 (number in kB units)
+ if ln ~ '"Maximum Storage":' then
+ ln := regexp_replace(ln, '"Maximum Storage": \d+', '"Maximum Storage": 0', 'g');
+ end if;
+
+ -- XML format: <Maximum-Storage>17</Maximum-Storage> (number in kB units)
+ if ln ~ '<Maximum-Storage>' then
+ ln := regexp_replace(ln, '<Maximum-Storage>\d+</Maximum-Storage>', '<Maximum-Storage>0</Maximum-Storage>', 'g');
+ end if;
+
+ return next ln;
+ end loop;
+end;
+$$;
+
+-- Setup: Create test tables
+CREATE TEMP TABLE nfa_test (
+ id serial,
+ v int,
+ cat char(1)
+);
+
+-- Insert test data: 100 rows with predictable pattern
+INSERT INTO nfa_test (v, cat)
+SELECT i,
+ CASE
+ WHEN i % 5 = 1 THEN 'A'
+ WHEN i % 5 = 2 THEN 'B'
+ WHEN i % 5 = 3 THEN 'C'
+ WHEN i % 5 = 4 THEN 'D'
+ ELSE 'E'
+ END
+FROM generate_series(1, 100) i;
+
+-- Additional test table with more complex patterns
+CREATE TEMP TABLE nfa_complex (
+ id serial,
+ price int,
+ trend char(1) -- U=up, D=down, S=stable
+);
+
+INSERT INTO nfa_complex (price, trend)
+VALUES
+ (100, 'S'), (105, 'U'), (110, 'U'), (108, 'D'), (112, 'U'),
+ (115, 'U'), (113, 'D'), (111, 'D'), (109, 'D'), (110, 'U'),
+ (120, 'U'), (125, 'U'), (130, 'U'), (128, 'D'), (126, 'D'),
+ (124, 'D'), (122, 'D'), (120, 'D'), (118, 'D'), (119, 'U'),
+ (121, 'U'), (123, 'U'), (125, 'U'), (127, 'U'), (129, 'U'),
+ (131, 'U'), (133, 'U'), (130, 'D'), (127, 'D'), (124, 'D');
+
+-- ============================================================
+-- Basic NFA Statistics Tests
+-- ============================================================
+
+-- Simple pattern - should show basic statistics
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS cat = 'A', B AS cat = 'B'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS cat = ''A'', B AS cat = ''B''
+)');
+DROP VIEW rpr_v;
+
+-- Pattern with no matches - 0 matched
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (X Y Z)
+ DEFINE X AS cat = 'X', Y AS cat = 'Y', Z AS cat = 'Z'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (X Y Z)
+ DEFINE X AS cat = ''X'', Y AS cat = ''Y'', Z AS cat = ''Z''
+);');
+DROP VIEW rpr_v;
+
+-- Pattern matching every row - high match count
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (R)
+ DEFINE R AS TRUE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (R)
+ DEFINE R AS TRUE
+);');
+DROP VIEW rpr_v;
+
+-- Regression test: Space before parenthesis in pattern deparse
+-- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A (B | C))
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A (B | C))
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Regression test: Sequential alternations at same depth
+-- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)"
+-- Previously failed due to missing parentheses on ALT depth decrease
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A ((B | C) (D | E))*)
+ DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A ((B | C) (D | E))*)
+ DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- State Statistics Tests (peak, total, merged)
+-- ============================================================
+
+-- Simple quantifier pattern - A+ with short matches (no merging)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS v % 2 = 1
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS v % 2 = 1
+);');
+DROP VIEW rpr_v;
+
+-- Alternation pattern - multiple state branches
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B | C) (D | E))
+ DEFINE
+ A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
+ D AS cat = 'D', E AS cat = 'E'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B | C) (D | E))
+ DEFINE
+ A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+ D AS cat = ''D'', E AS cat = ''E''
+);');
+DROP VIEW rpr_v;
+
+-- Complex pattern with high state count
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B* C+)
+ DEFINE
+ A AS v % 3 = 1,
+ B AS v % 3 = 2,
+ C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B* C+)
+ DEFINE
+ A AS v % 3 = 1,
+ B AS v % 3 = 2,
+ C AS v % 3 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Grouped pattern with quantifier - state merging
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+DROP VIEW rpr_v;
+
+-- State explosion pattern - many alternations
+-- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Consecutive ALT merge followed by different ALT
+-- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) (C | D))
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) (C | D))
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+DROP VIEW rpr_v;
+
+-- Consecutive ALT merge followed by non-ALT element
+-- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) C)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) C)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+DROP VIEW rpr_v;
+
+-- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B)+ (A | B))
+ DEFINE A AS v % 2 = 0, B AS v % 2 = 1
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B)+ (A | B))
+ DEFINE A AS v % 2 = 0, B AS v % 2 = 1
+);');
+DROP VIEW rpr_v;
+
+-- High state merging - alternation with plus quantifier
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B | C)+ D)
+ DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B | C)+ D)
+ DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Nested quantifiers causing state growth
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A | B)+)+)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A | B)+)+)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Context Statistics Tests (peak, total, absorbed, skipped)
+-- ============================================================
+
+-- Context absorption with unbounded quantifier at start
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+DROP VIEW rpr_v;
+
+-- No absorption - bounded quantifier
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2,4} B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2,4} B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Contexts skipped by SKIP PAST LAST ROW
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
+);');
+DROP VIEW rpr_v;
+
+-- High context absorption - unbounded group
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+ C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+ C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Match Length Statistics Tests
+-- ============================================================
+
+-- Fixed length matches - all same length
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E)
+ DEFINE
+ A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
+ D AS cat = 'D', E AS cat = 'E'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E)
+ DEFINE
+ A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+ D AS cat = ''D'', E AS cat = ''E''
+);');
+DROP VIEW rpr_v;
+
+-- Variable length matches - min/max/avg differ
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Very long matches
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 200) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v <= 195, B AS v > 195
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 200) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v <= 195, B AS v > 195
+);');
+DROP VIEW rpr_v;
+
+-- Mix of short and long matches
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
+ B AS v % 20 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
+ B AS v % 20 = 0
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Mismatch Length Statistics Tests
+-- ============================================================
+
+-- Pattern that causes mismatches with length > 1
+-- Mismatch happens when partial match fails after processing multiple rows
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM (
+ SELECT v,
+ CASE WHEN v % 10 IN (1,2,3) THEN 'A'
+ WHEN v % 10 IN (4,5) THEN 'B'
+ WHEN v % 10 = 6 THEN 'C'
+ ELSE 'X' END AS cat
+ FROM generate_series(1, 100) AS s(v)
+) t
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B+ C)
+ DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM (
+ SELECT v,
+ CASE WHEN v % 10 IN (1,2,3) THEN ''A''
+ WHEN v % 10 IN (4,5) THEN ''B''
+ WHEN v % 10 = 6 THEN ''C''
+ ELSE ''X'' END AS cat
+ FROM generate_series(1, 100) AS s(v)
+) t
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B+ C)
+ DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
+);');
+DROP VIEW rpr_v;
+
+-- Long partial matches that fail
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM (
+ SELECT i AS v,
+ CASE
+ WHEN i <= 20 THEN 'A'
+ WHEN i <= 25 THEN 'B'
+ WHEN i = 26 THEN 'X' -- breaks the pattern
+ WHEN i <= 50 THEN 'A'
+ WHEN i <= 55 THEN 'B'
+ WHEN i = 56 THEN 'C' -- completes pattern
+ ELSE 'Y'
+ END AS cat
+ FROM generate_series(1, 60) i
+) t
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B+ C)
+ DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM (
+ SELECT i AS v,
+ CASE
+ WHEN i <= 20 THEN ''A''
+ WHEN i <= 25 THEN ''B''
+ WHEN i = 26 THEN ''X'' -- breaks the pattern
+ WHEN i <= 50 THEN ''A''
+ WHEN i <= 55 THEN ''B''
+ WHEN i = 56 THEN ''C'' -- completes pattern
+ ELSE ''Y''
+ END AS cat
+ FROM generate_series(1, 60) i
+) t
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B+ C)
+ DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- JSON Format Tests
+-- ============================================================
+
+-- JSON format output with all statistics
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B+)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B+)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2
+)');
+DROP VIEW rpr_v;
+
+-- JSON format with match length statistics
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+)');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- XML Format Tests
+-- ============================================================
+
+-- XML format output
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT XML)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+)');
+DROP VIEW rpr_v;
+
+-- JSON format with mismatch statistics
+-- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS v = 1, B AS v = 2, C AS v = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
+SELECT count(*) OVER w
+FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS v = 1, B AS v = 2, C AS v = 3
+)');
+DROP VIEW rpr_v;
+
+-- JSON format with skipped context statistics
+-- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+)');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Multiple Partitions Tests
+-- ============================================================
+
+-- Statistics across multiple partitions
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM (
+ SELECT p, v
+ FROM generate_series(1, 3) p,
+ generate_series(1, 30) v
+) t
+WINDOW w AS (
+ PARTITION BY p
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM (
+ SELECT p, v
+ FROM generate_series(1, 3) p,
+ generate_series(1, 30) v
+) t
+WINDOW w AS (
+ PARTITION BY p
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Different pattern behavior per partition
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM (
+ SELECT
+ CASE WHEN v <= 25 THEN 1 ELSE 2 END AS p,
+ v % 10 AS val
+ FROM generate_series(1, 50) v
+) t
+WINDOW w AS (
+ PARTITION BY p
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS val < 5, B AS val >= 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM (
+ SELECT
+ CASE WHEN v <= 25 THEN 1 ELSE 2 END AS p,
+ v % 10 AS val
+ FROM generate_series(1, 50) v
+) t
+WINDOW w AS (
+ PARTITION BY p
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS val < 5, B AS val >= 5
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Edge Cases
+-- ============================================================
+
+-- Empty result set
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 0) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v = 1, B AS v = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 0) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v = 1, B AS v = 2
+);');
+DROP VIEW rpr_v;
+
+-- Single row
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A)
+ DEFINE A AS TRUE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 1) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A)
+ DEFINE A AS TRUE
+);');
+DROP VIEW rpr_v;
+
+-- Pattern longer than data
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 5) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E F G H I J)
+ DEFINE
+ A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5,
+ F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 5) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E F G H I J)
+ DEFINE
+ A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5,
+ F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10
+);');
+DROP VIEW rpr_v;
+
+-- All rows match as single match
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS TRUE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS TRUE
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Complex Pattern Tests
+-- ============================================================
+
+-- Nested groups
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A B) C)+)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A B) C)+)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Multiple alternations
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (C | D | E))
+ DEFINE
+ A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
+ D AS cat = 'D', E AS cat = 'E'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) (C | D | E))
+ DEFINE
+ A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+ D AS cat = ''D'', E AS cat = ''E''
+);');
+DROP VIEW rpr_v;
+
+-- Optional elements
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B? C)
+ DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B? C)
+ DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
+);');
+DROP VIEW rpr_v;
+
+-- Bounded quantifiers
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2,5} B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2,5} B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Star quantifier
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B* C)
+ DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B* C)
+ DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Real-world Pattern Examples
+-- ============================================================
+
+-- Stock price pattern - V-shape (down then up)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_complex
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (D+ U+)
+ DEFINE D AS trend = 'D', U AS trend = 'U'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_complex
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (D+ U+)
+ DEFINE D AS trend = ''D'', U AS trend = ''U''
+);');
+DROP VIEW rpr_v;
+
+-- Stock price pattern - peak (up, stable, down)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_complex
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (U+ S* D+)
+ DEFINE U AS trend = 'U', S AS trend = 'S', D AS trend = 'D'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_complex
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (U+ S* D+)
+ DEFINE U AS trend = ''U'', S AS trend = ''S'', D AS trend = ''D''
+);');
+DROP VIEW rpr_v;
+
+-- Consecutive increasing values (using PREV)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{3,})
+ DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{3,})
+ DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Performance-oriented Tests
+-- ============================================================
+
+-- Large dataset with simple pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Large dataset with absorption
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
+);');
+DROP VIEW rpr_v;
+
+-- High state merge ratio
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B)+ C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B)+ C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- INITIAL vs no INITIAL comparison
+-- ============================================================
+
+-- With INITIAL keyword
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Without INITIAL keyword (same behavior currently)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Quantifier Variations
+-- ============================================================
+
+-- Plus quantifier
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS v % 4 <> 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS v % 4 <> 0
+);');
+DROP VIEW rpr_v;
+
+-- Star quantifier (zero or more)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A* B)
+ DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A* B)
+ DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
+);');
+DROP VIEW rpr_v;
+
+-- Question mark (zero or one)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A? B C)
+ DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A? B C)
+ DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
+);');
+DROP VIEW rpr_v;
+
+-- Exact count {n}
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{3} B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{3} B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Range {n,m}
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2,4} B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{2,4} B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+DROP VIEW rpr_v;
+
+-- At least {n,}
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{3,} B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A{3,} B)
+ DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Regression Tests for Statistics Accuracy
+-- ============================================================
+
+-- Verify state count accuracy
+-- Pattern A+ B with 20 rows should show predictable state behavior
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Verify context count with known absorption
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B C)
+ DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B C)
+ DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
+);');
+DROP VIEW rpr_v;
+
+-- Verify match length with fixed-length pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Alternation Pattern Tests
+-- ============================================================
+
+-- Simple alternation
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) C)
+ DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B) C)
+ DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
+);');
+DROP VIEW rpr_v;
+
+-- Multiple items in alternation
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B | C | D) E)
+ DEFINE
+ A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
+ D AS cat = 'D', E AS cat = 'E'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B | C | D) E)
+ DEFINE
+ A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+ D AS cat = ''D'', E AS cat = ''E''
+);');
+DROP VIEW rpr_v;
+
+-- Alternation with quantifiers
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B)+ C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A | B)+ C)
+ DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Multiple alternatives (4+)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A | B | C | D | E)
+ DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A | B | C | D | E)
+ DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
+);');
+DROP VIEW rpr_v;
+
+-- Alternation at start
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B) C D)
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B) C D)
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+DROP VIEW rpr_v;
+
+-- Multiple sequential alternations
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B) C (D | E) F)
+ DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B) C (D | E) F)
+ DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+DROP VIEW rpr_v;
+
+-- Quantified alternatives
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A+ | B+) C)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A+ | B+) C)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+DROP VIEW rpr_v;
+
+-- Alternation at end
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B (C | D))
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B (C | D))
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+DROP VIEW rpr_v;
+
+-- Nested ALT at start of branch inside outer ALT
+-- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A ((B | C) D | E))
+ DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A ((B | C) D | E))
+ DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
+);');
+DROP VIEW rpr_v;
+
+-- Nested ALT at end of branch inside outer ALT
+-- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (C (A | B) | D)
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (C (A | B) | D)
+ DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Group Pattern Tests
+-- ============================================================
+
+-- Simple group
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Group with bounded quantifier
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B){2,4})
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B){2,4})
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Nested groups
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A B){2})+)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A B){2})+)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Deep nesting (3+ levels)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((((A | B)+)+)+)
+ DEFINE A AS v % 2 = 0, B AS v % 2 = 1
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((((A | B)+)+)+)
+ DEFINE A AS v % 2 = 0, B AS v % 2 = 1
+);');
+DROP VIEW rpr_v;
+
+-- Bounded quantifier on alternation
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B){2,3} C)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A | B){2,3} C)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+DROP VIEW rpr_v;
+
+-- Nested groups with quantifiers
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (((A B)+ C)*)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (((A B)+ C)*)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+DROP VIEW rpr_v;
+
+-- Partial nested quantification
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A (B C)+)*)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ((A (B C)+)*)
+ DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Window Function Combinations
+-- ============================================================
+
+-- count(*) with pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+DROP VIEW rpr_v;
+
+-- first_value with pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT first_value(v) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT first_value(v) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+DROP VIEW rpr_v;
+
+-- last_value with pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT last_value(v) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT last_value(v) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+DROP VIEW rpr_v;
+
+-- Multiple window functions
+CREATE TEMP VIEW rpr_v AS
+SELECT
+ count(*) OVER w,
+ first_value(v) OVER w,
+ last_value(v) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT
+ count(*) OVER w,
+ first_value(v) OVER w,
+ last_value(v) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- DEFINE Expression Variations
+-- ============================================================
+
+-- Complex boolean expressions
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS (v % 5 <> 0) AND (v % 3 <> 0),
+ B AS (v % 5 = 0) OR (v % 3 = 0)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS (v % 5 <> 0) AND (v % 3 <> 0),
+ B AS (v % 5 = 0) OR (v % 3 = 0)
+);');
+DROP VIEW rpr_v;
+
+-- Using PREV function
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (S U+ D+)
+ DEFINE
+ S AS TRUE,
+ U AS v > PREV(v),
+ D AS v < PREV(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (S U+ D+)
+ DEFINE
+ S AS TRUE,
+ U AS v > PREV(v),
+ D AS v < PREV(v)
+);');
+DROP VIEW rpr_v;
+
+-- Using NULL comparisons
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM (
+ SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
+ FROM generate_series(1, 30) v
+) t
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v IS NOT NULL, B AS v IS NULL
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM (
+ SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
+ FROM generate_series(1, 30) v
+) t
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS v IS NOT NULL, B AS v IS NULL
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Large Scale Statistics Verification
+-- ============================================================
+
+-- 500 rows - verify statistics scale correctly
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B C)
+ DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B C)
+ DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
+);');
+DROP VIEW rpr_v;
+
+-- High match count scenario
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+DROP VIEW rpr_v;
+
+-- High skip count scenario
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E)
+ DEFINE
+ A AS v % 100 = 1,
+ B AS v % 100 = 2,
+ C AS v % 100 = 3,
+ D AS v % 100 = 4,
+ E AS v % 100 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B C D E)
+ DEFINE
+ A AS v % 100 = 1,
+ B AS v % 100 = 2,
+ C AS v % 100 = 3,
+ D AS v % 100 = 4,
+ E AS v % 100 = 5
+);');
+DROP VIEW rpr_v;
+
+-- Cleanup
+DROP TABLE nfa_test;
+DROP TABLE nfa_complex;
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
new file mode 100644
index 00000000000..9573d1dab3b
--- /dev/null
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -0,0 +1,1865 @@
+-- ============================================================
+-- RPR NFA Tests
+-- Tests for Row Pattern Recognition NFA Runtime Execution
+-- ============================================================
+--
+-- This test suite validates the NFA (Non-deterministic Finite
+-- Automaton) runtime execution engine in nodeWindowAgg.c,
+-- focusing on update_reduced_frame and related functions.
+--
+-- Test Strategy:
+-- Diagonal pattern style using ARRAY flags to explicitly
+-- control which pattern variables match at each row.
+--
+-- Test Coverage:
+-- Basic NFA Flow (match->absorb->advance)
+-- Absorption Optimization
+-- Context Lifecycle Management
+-- Advance Phase (Epsilon Transitions)
+-- Match Phase (Variable Matching)
+-- Frame Boundary Handling
+-- State Management (Deduplication)
+-- Statistics and Diagnostics
+-- Quantifier Runtime Behavior
+-- Pathological Pattern Protection
+-- Alternation Runtime Behavior
+-- Deep Nested Groups
+-- SKIP Options (Runtime)
+-- INITIAL Mode (Runtime)
+-- Frame Boundary Variations
+-- Special Partition Cases
+-- DEFINE Special Cases
+-- Absorption Dynamic Flags
+-- FIXME Issues (Known Limitations)
+--
+-- Responsibility:
+-- - NFA runtime execution paths
+-- - Context/State lifecycle management
+-- - Runtime boundary conditions and protections
+--
+-- NOT tested here (covered in other files):
+-- - Pattern parsing/optimization (rpr_base.sql)
+-- - EXPLAIN output (rpr_explain.sql)
+-- - PREV/NEXT semantics (rpr.sql)
+-- ============================================================
+
+-- ============================================================
+-- Basic NFA Flow
+-- ============================================================
+
+-- Simple sequential pattern
+WITH test_sequential AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['_']) -- No match
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_sequential
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+
+-- Quantified pattern (A+ B+ C+)
+WITH test_quantified AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['B']),
+ (6, ARRAY['C']),
+ (7, ARRAY['C']),
+ (8, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_quantified
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B+ C+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+
+-- Optional pattern (A B? C)
+WITH test_optional AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['C']), -- B skipped
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']), -- B matched
+ (6, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_optional
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B? C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+
+-- Alternation pattern (A (B|C) D)
+WITH test_alternation AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']), -- First branch
+ (3, ARRAY['D']),
+ (4, ARRAY['A']),
+ (5, ARRAY['C']), -- Second branch
+ (6, ARRAY['D']),
+ (7, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_alternation
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A (B | C) D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+
+-- ============================================================
+-- Absorption Optimization
+-- ============================================================
+
+-- Absorbable pattern (A+)
+WITH test_absorbable AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_absorbable
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+
+-- Mixed absorbable/non-absorbable ((A+) | B)
+WITH test_mixed_absorption AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_mixed_absorption
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A+) | B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- State coverage (same elemIdx, different count)
+WITH test_state_coverage AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_state_coverage
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A{2,} B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- ============================================================
+-- Context Lifecycle
+-- ============================================================
+
+-- Multiple overlapping contexts (SKIP TO NEXT ROW)
+WITH test_overlapping_contexts AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_overlapping_contexts
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Failed context cleanup (early failure)
+WITH test_context_cleanup AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['_']), -- Pruned at first row
+ (2, ARRAY['A']),
+ (3, ARRAY['_']), -- Mismatched after row 2
+ (4, ARRAY['A']),
+ (5, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_context_cleanup
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Partition end (incomplete contexts)
+WITH test_partition_end AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A'])
+ -- Pattern requires B, but partition ends
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_partition_end
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Completed context encountered during processing
+-- Pattern (A | B C D): Ctx1 takes long B->C->D path, while Ctx2 takes
+-- short A path and completes first. Next row sees Ctx2
+-- with states=NULL and skips it.
+WITH test_completed_ctx AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['B', '_']),
+ (2, ARRAY['C', 'A']),
+ (3, ARRAY['D', '_']),
+ (4, ARRAY['_', '_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_completed_ctx
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A | B C D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+
+-- ============================================================
+-- Advance Phase (Epsilon Transitions)
+-- ============================================================
+
+-- Nested groups ((A B)+)
+WITH test_nested_groups AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']),
+ (7, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_nested_groups
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A B)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Multiple alternation branches (A (B|C|D) E)
+WITH test_multi_alt AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['E']),
+ (4, ARRAY['A']),
+ (5, ARRAY['C']),
+ (6, ARRAY['E']),
+ (7, ARRAY['A']),
+ (8, ARRAY['D']),
+ (9, ARRAY['E'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_multi_alt
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A (B | C | D) E)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+
+-- Optional VAR at start (A? B C)
+WITH test_optional_var AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['B']), -- A skipped
+ (2, ARRAY['C']),
+ (3, ARRAY['A']), -- A matched
+ (4, ARRAY['B']),
+ (5, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_optional_var
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A? B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+
+-- Nested alternation ((A|B) (C|D))
+WITH test_nested_alt AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['C']), -- A C
+ (3, ARRAY['A']),
+ (4, ARRAY['D']), -- A D
+ (5, ARRAY['B']),
+ (6, ARRAY['C']), -- B C
+ (7, ARRAY['B']),
+ (8, ARRAY['D']) -- B D
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_nested_alt
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A | B) (C | D))
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+
+-- ============================================================
+-- Match Phase
+-- ============================================================
+
+-- Simple VAR with END next (A B C all min=max=1)
+WITH test_simple_var AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_simple_var
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+
+-- VAR max exceeded (A{2,3})
+WITH test_max_exceeded AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']), -- Max = 3
+ (4, ARRAY['A']), -- Exceeds max, state removed
+ (5, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_max_exceeded
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A{2,3} B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Non-matching VAR (DEFINE false)
+WITH test_non_matching AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['_']), -- B not matched (DEFINE false)
+ (3, ARRAY['A']),
+ (4, ARRAY['B']), -- B matched
+ (5, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_non_matching
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+
+-- ============================================================
+-- Frame Boundary Handling
+-- ============================================================
+
+-- Limited frame (ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING)
+WITH test_limited_frame AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']), -- Within 3 FOLLOWING
+ (5, ARRAY['B']), -- Beyond 3 FOLLOWING from row 1
+ (6, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_limited_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+WITH test_unbounded_frame AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']) -- Far from start, but unbounded
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_unbounded_frame
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Match exceeds frame boundary
+WITH test_frame_exceeded AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A'])
+ -- Frame ends at row 3 (2 FOLLOWING), B never appears
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_frame_exceeded
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Frame boundary forced mismatch
+-- Limited frame with enough rows so that a context's frame boundary
+-- is exceeded while still processing.
+WITH test_frame_boundary AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_frame_boundary
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- ============================================================
+-- State Management
+-- ============================================================
+
+-- Duplicate state creation
+WITH test_duplicate_states AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A', 'B']), -- Both A and B match (creates duplicate states via different paths)
+ (2, ARRAY['C', '_']),
+ (3, ARRAY['D', '_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_duplicate_states
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A | B) C D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+
+-- Large pattern (stress free list)
+WITH test_large_pattern AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['E']),
+ (6, ARRAY['F']),
+ (7, ARRAY['G']),
+ (8, ARRAY['H']),
+ (9, ARRAY['I']),
+ (10, ARRAY['J'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_large_pattern
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C D E F G H I J)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags),
+ F AS 'F' = ANY(flags),
+ G AS 'G' = ANY(flags),
+ H AS 'H' = ANY(flags),
+ I AS 'I' = ANY(flags),
+ J AS 'J' = ANY(flags)
+);
+
+-- Reduced frame map reallocation (> 1024 rows)
+WITH test_map_realloc AS (
+ SELECT id, CASE WHEN id % 2 = 1 THEN ARRAY['A'] ELSE ARRAY['B'] END AS flags
+ FROM generate_series(1, 1100) AS id
+)
+SELECT count(*), min(match_start), max(match_end)
+FROM (
+ SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+ FROM test_map_realloc
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+ )
+) sub;
+
+-- ============================================================
+-- Statistics and Diagnostics
+-- ============================================================
+
+-- Matched contexts
+WITH test_matched AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_matched
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Pruned contexts (failed at first row)
+WITH test_pruned AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['_']), -- Pruned
+ (2, ARRAY['_']), -- Pruned
+ (3, ARRAY['A']),
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_pruned
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Mismatched contexts (failed after multiple rows)
+WITH test_mismatched AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['_']), -- Mismatched after 2 rows
+ (4, ARRAY['A']),
+ (5, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_mismatched
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Absorbed contexts
+WITH test_absorbed AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_absorbed
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+
+-- Skipped contexts (SKIP TO NEXT ROW)
+WITH test_skipped AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']) -- Completes match starting at row 1
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_skipped
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- ============================================================
+-- Quantifier Runtime Behavior
+-- ============================================================
+
+-- Large count handling (A{100})
+WITH test_large_count AS (
+ SELECT i AS id, ARRAY['A'] AS flags
+ FROM generate_series(1, 105) i
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_large_count
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A{100})
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+
+-- Unlimited quantifier (A{10,})
+WITH test_unlimited AS (
+ SELECT i AS id, ARRAY['A'] AS flags
+ FROM generate_series(1, 15) i
+ UNION ALL
+ SELECT 16, ARRAY['B']
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_unlimited
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A{10,} B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Min boundary (A{3,5})
+WITH test_min_boundary AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']), -- Min=3 reached, exit path available
+ (4, ARRAY['B']), -- Match ends at min
+ (5, ARRAY['A']),
+ (6, ARRAY['A']),
+ (7, ARRAY['A']),
+ (8, ARRAY['A']),
+ (9, ARRAY['A']), -- Count=5, max reached
+ (10, ARRAY['B']) -- Match ends at max
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_min_boundary
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A{3,5} B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Max boundary exceeded (A{3,5})
+WITH test_max_boundary AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['A']),
+ (6, ARRAY['A']), -- Count=6 > max=5, row 1 context removed
+ (7, ARRAY['B']) -- Row 1 context: no match (exceeded max)
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_max_boundary
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A{3,5} B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- ============================================================
+-- Pathological Pattern Runtime Protection
+-- ============================================================
+
+-- Complex nested nullable ((A* B*)*) - Runtime protection
+WITH test_complex_nested AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['B']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_complex_nested
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A* B*)*)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Nested nullable with quantifier ((A{0,3})*)
+WITH test_nested_quantifier AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_nested_quantifier
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A{0,3})*)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+
+-- ============================================================
+-- Alternation Runtime Behavior
+-- ============================================================
+
+-- Multi-branch alternation (A (B|C|D|E) F)
+WITH test_multi_branch AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['F']),
+ (4, ARRAY['A']),
+ (5, ARRAY['C']),
+ (6, ARRAY['F']),
+ (7, ARRAY['A']),
+ (8, ARRAY['D']),
+ (9, ARRAY['F']),
+ (10, ARRAY['A']),
+ (11, ARRAY['E']),
+ (12, ARRAY['F'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_multi_branch
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A (B | C | D | E) F)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags),
+ F AS 'F' = ANY(flags)
+);
+
+-- Alternation with quantifiers (A+ | B+ | C+)
+WITH test_alt_quantifiers AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['B']),
+ (6, ARRAY['C']),
+ (7, ARRAY['C']),
+ (8, ARRAY['C']),
+ (9, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_alt_quantifiers
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ | B+ | C+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+
+-- altPriority replacement (A B C | D)
+-- D branch (higher altPriority) matches first at row 1,
+-- then A B C branch (lower altPriority) replaces it at row 3.
+WITH test_alt_replace AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A', 'D']),
+ (2, ARRAY['B', '_']),
+ (3, ARRAY['C', '_']),
+ (4, ARRAY['_', '_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_alt_replace
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C | D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+
+-- ============================================================
+-- Deep Nested Groups
+-- ============================================================
+
+-- Three-level nesting ((((A B)+)+)+)
+WITH test_deep_nesting AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']),
+ (7, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_deep_nesting
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((((A B)+)+)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Multiple groups in nesting (((A B) (C D))+)
+WITH test_nested_sequential AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['D']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']),
+ (7, ARRAY['C']),
+ (8, ARRAY['D']),
+ (9, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_nested_sequential
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (((A B) (C D))+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+
+-- Nested END→END max reached
+-- Inner group (A B){2} reaches max=2 → exits to outer END
+WITH test_end_nested_max AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_end_nested_max
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (((A B){2})+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Nested END→END between min/max
+-- Inner group (A B){1,3} exits between min/max → outer END count++
+WITH test_end_nested_mid AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_end_nested_mid
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (((A B){1,3})+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- ============================================================
+-- SKIP Options (Runtime)
+-- ============================================================
+
+-- SKIP PAST LAST ROW (non-overlapping matches)
+WITH test_skip_past AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_skip_past
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+
+-- SKIP TO NEXT ROW (overlapping matches)
+WITH test_skip_next AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_skip_next
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+
+-- SKIP difference verification
+WITH test_skip_diff AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT 'SKIP PAST' AS mode, id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_skip_diff
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+)
+UNION ALL
+SELECT 'SKIP NEXT' AS mode, id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_skip_diff
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+)
+ORDER BY mode, id;
+
+-- ============================================================
+-- INITIAL Mode (Runtime)
+-- ============================================================
+
+-- INITIAL mode (not yet supported - produces syntax error)
+WITH test_initial_mode AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['_']), -- Unmatched
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['_']), -- Unmatched
+ (5, ARRAY['A'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_initial_mode
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+
+-- Default mode (include all rows)
+WITH test_default_mode AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['_']), -- Unmatched, but included
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['_']), -- Unmatched, but included
+ (5, ARRAY['A'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_default_mode
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+
+-- Mode difference verification (INITIAL not yet supported - produces syntax error)
+WITH test_mode_diff AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['_']),
+ (2, ARRAY['A']),
+ (3, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT 'INITIAL' AS mode, COUNT(*) AS row_count
+FROM (
+ SELECT id FROM test_mode_diff
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A)
+ DEFINE A AS 'A' = ANY(flags)
+ )
+) sub
+UNION ALL
+SELECT 'DEFAULT' AS mode, COUNT(*) AS row_count
+FROM (
+ SELECT id FROM test_mode_diff
+ WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A)
+ DEFINE A AS 'A' = ANY(flags)
+ )
+) sub
+ORDER BY mode;
+
+-- ============================================================
+-- Frame Boundary Variations
+-- ============================================================
+
+-- Very limited frame (1 FOLLOWING)
+WITH test_one_following AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']), -- Within 1 FOLLOWING
+ (3, ARRAY['A']), -- Beyond 1 FOLLOWING from row 1
+ (4, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_one_following
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Medium frame (10 FOLLOWING)
+WITH test_ten_following AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['A']),
+ (6, ARRAY['A']),
+ (7, ARRAY['A']),
+ (8, ARRAY['A']),
+ (9, ARRAY['A']),
+ (10, ARRAY['A']),
+ (11, ARRAY['B']), -- Within 10 FOLLOWING from row 1
+ (12, ARRAY['A']),
+ (13, ARRAY['B']) -- Beyond 10 FOLLOWING from row 1
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_ten_following
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Exact boundary match
+WITH test_exact_boundary AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['B']) -- Exactly at 4 FOLLOWING (frame end)
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_exact_boundary
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 4 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+ B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- ============================================================
+-- Special Partition Cases
+-- ============================================================
+
+-- Empty partition (0 rows)
+WITH test_empty_partition AS (
+ SELECT * FROM (VALUES
+ (1, 1, ARRAY['A']),
+ (2, 2, ARRAY['_']) -- Different partition
+ ) AS t(id, part, flags)
+)
+SELECT id, part, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_empty_partition
+WHERE part = 99 -- No rows match
+WINDOW w AS (
+ PARTITION BY part
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+
+-- Single row partition
+WITH test_single_row AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_single_row
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+
+-- All rows fail matching (all DEFINE false)
+WITH test_all_fail AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_all_fail
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE
+ A AS false -- All rows fail
+);
+
+-- Partition end with absorbable pattern
+-- SKIP PAST LAST ROW + unbounded frame + all rows match A
+-- Triggers absorb in !rowExists path at partition boundary.
+WITH test_absorb_partition_end AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['A']),
+ (5, ARRAY['A'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_absorb_partition_end
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+
+-- ============================================================
+-- DEFINE Special Cases
+-- ============================================================
+
+-- Undefined variable in DEFINE
+WITH test_undefined_var AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['X']), -- B not defined, defaults to TRUE
+ (3, ARRAY['C']),
+ (4, ARRAY['A']),
+ (5, ARRAY['_']), -- B defaults to TRUE, but no flags
+ (6, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_undefined_var
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ -- B is undefined, defaults to TRUE
+ C AS 'C' = ANY(flags)
+);
+
+-- ============================================================
+-- Absorption Dynamic Flags
+-- ============================================================
+
+-- Partial absorbable pattern ((A+) B)
+WITH test_partial_absorbable AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_partial_absorbable
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A+) B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Dynamic flag update ((A+) | B)
+WITH test_dynamic_flags AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['A']),
+ (6, ARRAY['B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_dynamic_flags
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A+) | B)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Non-absorbable context during absorption
+-- Pattern (A B)+ C: A,B in absorbable group, C is not.
+-- When END exits to C via nfa_state_create, isAbsorbable becomes false.
+WITH test_non_absorbable AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']),
+ (6, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_non_absorbable
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+ C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+
+-- Absorption flags early return (!hasAbsorbableState)
+-- Pattern (A B)+ C D with SKIP PAST LAST ROW
+-- After reaching C (non-absorbable), hasAbsorbableState becomes false.
+-- On next row (D), the early return fires.
+WITH test_absorption_early_return AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']),
+ (6, ARRAY['D']),
+ (7, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_absorption_early_return
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B)+ C D)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+
+-- Coverage failure: older can't cover newer's states
+-- Pattern A+ | B+ with SKIP PAST LAST ROW.
+-- Row 1: only A → Ctx1 takes A branch only (B fails).
+-- Row 2: A and B → Ctx2 takes both branches.
+-- Absorption: Ctx1 has A but no B → can't cover Ctx2's B state → fails.
+WITH test_coverage_fail AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A', '_']),
+ (2, ARRAY['A', 'B']),
+ (3, ARRAY['A', '_']),
+ (4, ARRAY['A', '_']),
+ (5, ARRAY['_', '_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_coverage_fail
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ | B+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Absorb skips completed context (older->states==NULL)
+-- Pattern A+ | B+ with SKIP PAST LAST ROW.
+-- Row 1: A only → Ctx1 takes A branch. Row 2: B only → Ctx1 A fails (completed).
+-- Ctx2 takes B branch. Absorption: Ctx1 states==NULL → skip.
+WITH test_older_completed AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_older_completed
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ | B+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Absorb skips non-absorbable context (!hasAbsorbableState)
+-- Pattern A+ | B C with SKIP PAST LAST ROW (only A+ branch absorbable).
+-- Row 1: B only → Ctx1 takes B branch (non-absorbable), advances to C.
+-- Row 2: C,A → Ctx1 C matches (hasAbsorbableState=false). Ctx2 takes A (absorbable).
+-- Absorption: Ctx1 !hasAbsorbableState → skip.
+WITH test_older_non_absorbable AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['B', '_']),
+ (2, ARRAY['C', 'A']),
+ (3, ARRAY['_', 'A']),
+ (4, ARRAY['_', '_'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_older_non_absorbable
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ | B C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+
+-- ============================================================
+-- FIXME Issues - Known Limitations
+-- ============================================================
+
+-- FIXME 1 - altPriority lexical order
+WITH test_alt_priority_repeated AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A','B']), -- Both A and B match
+ (2, ARRAY['A','B']),
+ (3, ARRAY['A','B'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_alt_priority_repeated
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A | B)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- FIXME 1 - Nested ALT lexical order
+WITH test_alt_priority_nested AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A','B']),
+ (2, ARRAY['C','D']),
+ (3, ARRAY['A','B']),
+ (4, ARRAY['C','D'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_alt_priority_nested
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (((A | B) (C | D))+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags)
+);
+
+-- FIXME 2 - Cycle prevention at count > 0
+WITH test_cycle_nonzero AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['A']),
+ (3, ARRAY['A']),
+ (4, ARRAY['B']) -- Inner A* matches 0, cycles at count=3
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_cycle_nonzero
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A*)*)
+ DEFINE
+ A AS 'A' = ANY(flags)
+);
+
+-- FIXME 2 - Cycle with mixed nullables
+WITH test_cycle_mixed AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['A']),
+ (4, ARRAY['C'])
+ ) AS t(id, flags)
+)
+SELECT id, flags,
+ first_value(id) OVER w AS match_start,
+ last_value(id) OVER w AS match_end
+FROM test_cycle_mixed
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN ((A* B*)*)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
--
2.43.0
[application/octet-stream] v43-0008-Row-pattern-recognition-patch-typedefs.list.patch (1.1K, 9-v43-0008-Row-pattern-recognition-patch-typedefs.list.patch)
download | inline diff:
From f9b5c81931c704b697c83ae912539989d9fd1b84 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sun, 15 Feb 2026 17:47:49 +0900
Subject: [PATCH v43 8/8] Row pattern recognition patch (typedefs.list).
---
src/tools/pgindent/typedefs.list | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 241945734ec..e9b0239af96 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1774,6 +1774,7 @@ NamedLWLockTrancheRequest
NamedTuplestoreScan
NamedTuplestoreScanState
NamespaceInfo
+NavigationInfo
NestLoop
NestLoopParam
NestLoopState
@@ -1783,6 +1784,7 @@ NewConstraint
NextSampleBlock_function
NextSampleTuple_function
NextValueExpr
+NFALengthStats
Node
NodeTag
NonEmptyRange
@@ -2448,6 +2450,19 @@ RI_CompareKey
RI_ConstraintInfo
RI_QueryHashEntry
RI_QueryKey
+RPCommonSyntax
+RPRDepth
+RPRElemFlags
+RPRElemIdx
+RPRNFAContext
+RPRNFAState
+RPRPattern
+RPRPatternElement
+RPRPatternNode
+RPRPatternNodeType
+RPRQuantity
+RPRVarId
+RPSkipTo
RTEKind
RTEPermissionInfo
RWConflict
--
2.43.0
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected]
Subject: Re: Row pattern recognition
In-Reply-To: <[email protected]>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox