From 7d04f740fa3b6db7916f4efe1ee2b6ef9441865e Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <ishii@postgresql.org>
Date: Tue, 20 Jan 2026 19:00:01 +0900
Subject: [PATCH v41 7/8] Row pattern recognition patch (tests).

---
 src/test/regress/expected/rpr.out         | 3681 +++++++++++++++++++++
 src/test/regress/expected/rpr_explain.out | 1803 ++++++++++
 src/test/regress/parallel_schedule        |    2 +-
 src/test/regress/sql/rpr.sql              | 2026 ++++++++++++
 src/test/regress/sql/rpr_explain.sql      |  935 ++++++
 5 files changed, 8446 insertions(+), 1 deletion(-)
 create mode 100644 src/test/regress/expected/rpr.out
 create mode 100644 src/test/regress/expected/rpr_explain.out
 create mode 100644 src/test/regress/sql/rpr.sql
 create mode 100644 src/test/regress/sql/rpr_explain.sql

diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
new file mode 100644
index 00000000000..5a96ced4b52
--- /dev/null
+++ b/src/test/regress/expected/rpr.out
@@ -0,0 +1,3681 @@
+--
+-- 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)
+
+-- 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)
+
+-- 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)
+
+-- 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 (A{2}){3,5} -> A{6,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;  -- optimized: a{6,10}
+                                             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{6,10}
+         ->  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)
+
+--
+-- 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 3:  WINDOW w AS (
+                     ^
+-- 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 3:  WINDOW w AS (
+                     ^
+-- 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+)
+                          ^
+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
+-- Maximum pattern variables is 252 (RPR_VARID_MAX)
+-- Ok: 252 variables (maximum allowed)
+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 252
+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
+LINE 4: ...S TRUE, v250 AS TRUE, v251 AS TRUE, v252 AS TRUE, v253 AS TR...
+                                                             ^
+DETAIL:  Maximum is 252.
+QUERY:  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)
+CONTEXT:  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 | A+ B (greedy at front for absorption)
+-- Data: A B C X (D expected but X found)
+-- A+ B C D fails at row 4 (X instead of D)
+-- A+ B succeeds at row 2
+-- Result should be match 1-2 (A+ B)
+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)
+-- Without absorption: 4 matches (1-4, 2-4, 3-4, 4-4)
+-- With absorption: Row 1 match (1-4), rows 2-4 absorbed
+-- 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)
+-- With absorption: Row 1 match (1-4), rows 2-3 absorbed
+-- 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: potential 1-4, 2-4, 3-4
+-- Row 1 (1-4) absorbs Row 2 (2-4) and Row 3 (3-4) - same endpoint
+-- 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,}
+-- Potential matches: 1-6 (3 reps), 3-6 (2 reps), 5-6 needs 2 reps (fail)
+-- Row 1 (1-6) absorbs Row 3 (3-6) - same endpoint, top-level unbounded GROUP
+-- 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)
+
+-- Row 1: 1-4, Row 2: absorbed (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
+-- BUG: Currently returns NULL instead of matching 1-4
+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
+-- Currently returns two separate matches (1-2, 3-4) instead of single 1-4
+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) - CURRENTLY ACTS LIKE A+ (GREEDY)
+-- Parser accepts the syntax but executor doesn't differentiate
+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
+-- Current: 1-4 (A A A B) - greedy behavior
+-- Expected when RELUCTANT implemented: 3-4 (A B) - shortest A before B
+-- Test: A{1,3}? B (reluctant bounded)
+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
+-- Current: 1-4 (greedy, takes max A before B)
+-- Expected when RELUCTANT implemented: 3-4 (takes min A before B)
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
new file mode 100644
index 00000000000..a12359337ee
--- /dev/null
+++ b/src/test/regress/expected/rpr_explain.out
@@ -0,0 +1,1803 @@
+--
+-- Test: EXPLAIN ANALYZE output for Row Pattern Recognition NFA statistics
+--
+-- This file tests the NFA statistics shown in EXPLAIN ANALYZE output:
+-- - NFA States: peak, total, merged
+-- - NFA Contexts: peak, total, absorbed, skipped
+-- - NFA: matched (len min/max/avg), mismatched (len min/max/avg)
+--
+-- 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');
+--
+-- Section 1: Basic NFA Statistics Tests
+--
+-- Test 1.1: Simple pattern - should show basic statistics
+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'
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 2 peak, 101 total, 0 merged
+   NFA Contexts: 3 peak, 101 total, 80 pruned
+   NFA: 20 matched (len 2/2/2.0), 0 mismatched
+   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(8 rows)
+
+-- Test 1.2: Pattern with no matches - 0 matched
+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'
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ 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: 17kB
+   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)
+
+-- Test 1.3: Pattern matching every row - high match count
+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
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: r
+   Storage: Memory  Maximum Storage: 17kB
+   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)
+
+--
+-- Section 2: State Statistics Tests (peak, total, merged)
+--
+-- Test 2.1: Simple quantifier pattern - A+ with short matches (no merging)
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Storage: Memory  Maximum Storage: 17kB
+   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)
+
+-- Test 2.2: Alternation pattern - multiple state branches
+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'
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 5 peak, 363 total, 0 merged
+   NFA Contexts: 3 peak, 101 total, 40 pruned
+   NFA: 20 matched (len 2/2/2.0), 40 mismatched (len 2/2/2.0)
+   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(8 rows)
+
+-- Test 2.3: Complex pattern with high state count
+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
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 5 peak, 235 total, 0 merged
+   NFA Contexts: 3 peak, 101 total, 67 pruned
+   NFA: 33 matched (len 3/3/3.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
+(8 rows)
+
+-- Test 2.4: Grouped pattern with quantifier - state merging
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a b)+
+   Storage: Memory  Maximum Storage: 19kB
+   NFA States: 3 peak, 91 total, 0 merged
+   NFA Contexts: 3 peak, 61 total, 30 pruned
+   NFA: 1 matched (len 60/60/60.0), 0 mismatched
+   NFA: 29 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
+
+-- Test 2.5: State explosion pattern - many alternations
+-- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
+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
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a | b a | b a | b a | b a | b a | b a | b a | b)
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 17 peak, 632 total, 0 merged
+   NFA Contexts: 9 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)
+
+-- Test 2.6: High state merging - alternation with plus quantifier
+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
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 15 peak, 753 total, 0 merged
+   NFA Contexts: 5 peak, 101 total, 25 pruned
+   NFA: 25 matched (len 4/4/4.0), 0 mismatched
+   NFA: 0 absorbed, 50 skipped (len 2/3/2.5)
+   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+-- Test 2.7: Nested quantifiers causing state growth
+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
+);
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ WindowAgg (actual rows=1000.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a | b)+)+
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 16 peak, 7334 total, 0 merged
+   NFA Contexts: 4 peak, 1001 total, 333 pruned
+   NFA: 334 matched (len 1/2/2.0), 0 mismatched
+   NFA: 0 absorbed, 333 skipped (len 2/2/2.0)
+   ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
+(9 rows)
+
+--
+-- Section 3: Context Statistics Tests (peak, total, absorbed, skipped)
+--
+-- Test 3.1: Context absorption with unbounded quantifier at start
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 3 peak, 91 total, 0 merged
+   NFA Contexts: 3 peak, 51 total, 10 pruned
+   NFA: 10 matched (len 5/5/5.0), 0 mismatched
+   NFA: 30 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+-- Test 3.2: No absorption - bounded quantifier
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 7 peak, 101 total, 0 merged
+   NFA Contexts: 6 peak, 51 total, 10 pruned
+   NFA: 10 matched (len 5/5/5.0), 10 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 20 skipped (len 3/4/3.5)
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+-- Test 3.3: Contexts skipped by SKIP PAST LAST ROW
+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
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 2 peak, 101 total, 0 merged
+   NFA Contexts: 3 peak, 101 total, 90 pruned
+   NFA: 10 matched (len 3/3/3.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
+(8 rows)
+
+-- Test 3.4: High context absorption - unbounded group
+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
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 3 peak, 134 total, 0 merged
+   NFA Contexts: 3 peak, 101 total, 67 pruned
+   NFA: 33 matched (len 3/3/3.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
+(8 rows)
+
+--
+-- Section 4: Match Length Statistics Tests
+--
+-- Test 4.1: Fixed length matches - all same length
+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'
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 2 peak, 101 total, 0 merged
+   NFA Contexts: 3 peak, 101 total, 80 pruned
+   NFA: 20 matched (len 5/5/5.0), 0 mismatched
+   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(8 rows)
+
+-- Test 4.2: Variable length matches - min/max/avg differ
+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
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 3 peak, 191 total, 0 merged
+   NFA Contexts: 3 peak, 101 total, 10 pruned
+   NFA: 10 matched (len 10/10/10.0), 0 mismatched
+   NFA: 80 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+-- Test 4.3: Very long matches
+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
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=200.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 24kB
+   NFA States: 3 peak, 396 total, 0 merged
+   NFA Contexts: 3 peak, 201 total, 5 pruned
+   NFA: 1 matched (len 196/196/196.0), 0 mismatched
+   NFA: 194 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=200.00 loops=1)
+(9 rows)
+
+-- Test 4.4: Mix of short and long matches
+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
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=100.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 3 peak, 171 total, 0 merged
+   NFA Contexts: 3 peak, 101 total, 30 pruned
+   NFA: 5 matched (len 5/5/5.0), 5 mismatched (len 11/11/11.0)
+   NFA: 60 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+--
+-- Section 5: Mismatch Length Statistics Tests
+--
+-- Test 5.1: Pattern that causes mismatches with length > 1
+-- Mismatch happens when partial match fails after processing multiple rows
+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'
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 3 peak, 151 total, 0 merged
+   NFA Contexts: 3 peak, 101 total, 70 pruned
+   NFA: 10 matched (len 6/6/6.0), 0 mismatched
+   NFA: 20 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+-- Test 5.2: Long partial matches that fail
+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'
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 18kB
+   NFA States: 3 peak, 115 total, 0 merged
+   NFA Contexts: 3 peak, 61 total, 16 pruned
+   NFA: 1 matched (len 30/30/30.0), 1 mismatched (len 26/26/26.0)
+   NFA: 42 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series i (actual rows=60.00 loops=1)
+(9 rows)
+
+--
+-- Section 6: JSON Format Tests
+--
+-- Test 6.1: JSON format output with all statistics
+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
+);
+                                 QUERY PLAN                                 
+----------------------------------------------------------------------------
+ [                                                                         +
+   {                                                                       +
+     "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": 17,                                              +
+       "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": 0,                                          +
+       "NFA Contexts Pruned": 33,                                          +
+       "NFA Matched": 17,                                                  +
+       "NFA Mismatched": 0,                                                +
+       "NFA Match Length Min": 2,                                          +
+       "NFA Match Length Max": 2,                                          +
+       "NFA Match Length Avg": 2.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)
+
+-- Test 6.2: JSON format with match length statistics
+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
+);
+                                 QUERY PLAN                                 
+----------------------------------------------------------------------------
+ [                                                                         +
+   {                                                                       +
+     "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": 17,                                              +
+       "NFA States Peak": 3,                                               +
+       "NFA States Total": 191,                                            +
+       "NFA States Merged": 0,                                             +
+       "NFA Contexts Peak": 3,                                             +
+       "NFA Contexts Total": 101,                                          +
+       "NFA Contexts Absorbed": 80,                                        +
+       "NFA Contexts Skipped": 0,                                          +
+       "NFA Contexts Pruned": 10,                                          +
+       "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,                                     +
+       "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)
+
+--
+-- Section 7: XML Format Tests
+--
+-- Test 7.1: XML format output
+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
+);
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ <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>17</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>3</NFA-Contexts-Peak>                                +
+       <NFA-Contexts-Total>31</NFA-Contexts-Total>                             +
+       <NFA-Contexts-Absorbed>0</NFA-Contexts-Absorbed>                        +
+       <NFA-Contexts-Skipped>0</NFA-Contexts-Skipped>                          +
+       <NFA-Contexts-Pruned>15</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>                        +
+       <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)
+
+--
+-- Section 8: Multiple Partitions Tests
+--
+-- Test 8.1: Statistics across multiple partitions
+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
+);
+                                     QUERY PLAN                                     
+------------------------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 3 peak, 165 total, 0 merged
+   NFA Contexts: 3 peak, 93 total, 18 pruned
+   NFA: 18 matched (len 5/5/5.0), 0 mismatched
+   NFA: 54 absorbed (len 1/1/1.0), 0 skipped
+   ->  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)
+
+-- Test 8.2: Different pattern behavior per partition
+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
+);
+                                                        QUERY PLAN                                                        
+--------------------------------------------------------------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 3 peak, 77 total, 0 merged
+   NFA Contexts: 3 peak, 52 total, 26 pruned
+   NFA: 5 matched (len 5/6/5.8), 0 mismatched
+   NFA: 19 absorbed (len 1/1/1.0), 0 skipped
+   ->  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)
+
+--
+-- Section 9: Edge Cases
+--
+-- Test 9.1: Empty result set
+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
+);
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ 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)
+
+-- Test 9.2: Single row
+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
+);
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ WindowAgg (actual rows=1.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a
+   Storage: Memory  Maximum Storage: 17kB
+   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)
+
+-- Test 9.3: Pattern longer than data
+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
+);
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ 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: 17kB
+   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)
+
+-- Test 9.4: All rows match as single match
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Storage: Memory  Maximum Storage: 18kB
+   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)
+
+--
+-- Section 10: Complex Pattern Tests
+--
+-- Test 10.1: Nested groups
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 19kB
+   NFA States: 3 peak, 81 total, 0 merged
+   NFA Contexts: 3 peak, 61 total, 40 pruned
+   NFA: 1 matched (len 60/60/60.0), 0 mismatched
+   NFA: 19 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
+
+-- Test 10.2: Multiple alternations
+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'
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 5 peak, 282 total, 0 merged
+   NFA Contexts: 3 peak, 101 total, 60 pruned
+   NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
+   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(8 rows)
+
+-- Test 10.3: Optional elements
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 3 peak, 64 total, 0 merged
+   NFA Contexts: 3 peak, 51 total, 37 pruned
+   NFA: 12 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(8 rows)
+
+-- Test 10.4: Bounded quantifiers
+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
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 9 peak, 311 total, 0 merged
+   NFA Contexts: 7 peak, 101 total, 10 pruned
+   NFA: 10 matched (len 6/6/6.0), 50 mismatched (len 2/6/5.2)
+   NFA: 0 absorbed, 30 skipped (len 3/5/4.0)
+   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
+(9 rows)
+
+-- Test 10.5: Star quantifier
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 3 peak, 91 total, 0 merged
+   NFA Contexts: 3 peak, 51 total, 45 pruned
+   NFA: 5 matched (len 9/9/9.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(8 rows)
+
+--
+-- Section 11: Real-world Pattern Examples
+--
+-- Test 11.1: Stock price pattern - V-shape (down then up)
+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'
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: d+ u+
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 4 peak, 58 total, 0 merged
+   NFA Contexts: 3 peak, 31 total, 17 pruned
+   NFA: 3 matched (len 3/14/8.0), 1 mismatched (len 3/3/3.0)
+   NFA: 9 absorbed (len 1/1/1.0), 0 skipped
+   ->  Seq Scan on nfa_complex (actual rows=30.00 loops=1)
+(9 rows)
+
+-- Test 11.2: Stock price pattern - peak (up, stable, down)
+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'
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 5 peak, 76 total, 0 merged
+   NFA Contexts: 3 peak, 31 total, 14 pruned
+   NFA: 4 matched (len 3/11/7.2), 0 mismatched
+   NFA: 12 absorbed (len 1/1/1.0), 0 skipped
+   ->  Seq Scan on nfa_complex (actual rows=30.00 loops=1)
+(9 rows)
+
+-- Test 11.3: Consecutive increasing values (using PREV)
+-- FIXME: The original pattern was:
+--   DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
+-- This causes "ERROR: unrecognized node type: 15" (T_FuncExpr) because
+-- NullTest(FuncExpr(PREV)) is not properly handled somewhere in the planner.
+-- The expression v > PREV(v) works fine, but PREV(v) IS NULL fails.
+-- Using COALESCE(PREV(v), 0) as a workaround until the bug is fixed.
+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 > COALESCE(PREV(v), 0)
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{3,}
+   Storage: Memory  Maximum Storage: 18kB
+   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)
+
+--
+-- Section 12: Performance-oriented Tests
+--
+-- Test 12.1: Large dataset with simple pattern
+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
+);
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ WindowAgg (actual rows=1000.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 2 peak, 1001 total, 0 merged
+   NFA Contexts: 3 peak, 1001 total, 500 pruned
+   NFA: 500 matched (len 2/2/2.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
+(8 rows)
+
+-- Test 12.2: Large dataset with absorption
+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
+);
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ WindowAgg (actual rows=1000.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 21kB
+   NFA States: 3 peak, 1991 total, 0 merged
+   NFA Contexts: 3 peak, 1001 total, 10 pruned
+   NFA: 10 matched (len 100/100/100.0), 0 mismatched
+   NFA: 980 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
+(9 rows)
+
+-- Test 12.3: High state merge ratio
+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
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 8 peak, 2004 total, 0 merged
+   NFA Contexts: 4 peak, 501 total, 167 pruned
+   NFA: 166 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 166 skipped (len 2/2/2.0)
+   ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
+(9 rows)
+
+--
+-- Section 13: INITIAL vs no INITIAL comparison
+--
+-- Test 13.1: With INITIAL keyword
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 3 peak, 91 total, 0 merged
+   NFA Contexts: 3 peak, 51 total, 10 pruned
+   NFA: 10 matched (len 5/5/5.0), 0 mismatched
+   NFA: 30 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+-- Test 13.2: Without INITIAL keyword (same behavior currently)
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 3 peak, 91 total, 0 merged
+   NFA Contexts: 3 peak, 51 total, 10 pruned
+   NFA: 10 matched (len 5/5/5.0), 0 mismatched
+   NFA: 30 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+--
+-- Section 14: Quantifier Variations
+--
+-- Test 14.1: Plus quantifier
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Storage: Memory  Maximum Storage: 17kB
+   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)
+
+-- Test 14.2: Star quantifier (zero or more)
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a* b
+   Storage: Memory  Maximum Storage: 17kB
+   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)
+
+-- Test 14.3: Question mark (zero or one)
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 4 peak, 82 total, 0 merged
+   NFA Contexts: 4 peak, 41 total, 20 pruned
+   NFA: 10 matched (len 3/3/3.0), 0 mismatched
+   NFA: 0 absorbed, 10 skipped (len 2/2/2.0)
+   ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
+
+-- Test 14.4: Exact count {n}
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 4 peak, 51 total, 0 merged
+   NFA Contexts: 5 peak, 51 total, 10 pruned
+   NFA: 10 matched (len 4/4/4.0), 30 mismatched (len 2/4/3.0)
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(8 rows)
+
+-- Test 14.5: Range {n,m}
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 7 peak, 101 total, 0 merged
+   NFA Contexts: 6 peak, 51 total, 10 pruned
+   NFA: 10 matched (len 5/5/5.0), 10 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 20 skipped (len 3/4/3.5)
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+-- Test 14.6: At least {n,}
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 3 peak, 86 total, 0 merged
+   NFA Contexts: 3 peak, 51 total, 5 pruned
+   NFA: 5 matched (len 10/10/10.0), 0 mismatched
+   NFA: 40 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+--
+-- Section 15: Regression Tests for Statistics Accuracy
+--
+-- Test 15.1: Verify state count accuracy
+-- Pattern A+ B with 20 rows should show predictable state behavior
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 3 peak, 37 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 4 pruned
+   NFA: 4 matched (len 5/5/5.0), 0 mismatched
+   NFA: 12 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
+
+-- Test 15.2: Verify context count with known absorption
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 3 peak, 52 total, 0 merged
+   NFA Contexts: 3 peak, 31 total, 9 pruned
+   NFA: 3 matched (len 9/9/9.0), 0 mismatched
+   NFA: 18 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
+(9 rows)
+
+-- Test 15.3: Verify match length with fixed-length pattern
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 2 peak, 31 total, 0 merged
+   NFA Contexts: 3 peak, 31 total, 20 pruned
+   NFA: 10 matched (len 3/3/3.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
+(8 rows)
+
+--
+-- Section 16: Alternation Pattern Tests
+--
+-- Test 16.1: Simple alternation
+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'
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 3 peak, 202 total, 0 merged
+   NFA Contexts: 3 peak, 101 total, 60 pruned
+   NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
+   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(8 rows)
+
+-- Test 16.2: Multiple items in alternation
+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'
+);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 5 peak, 404 total, 0 merged
+   NFA Contexts: 3 peak, 101 total, 20 pruned
+   NFA: 20 matched (len 2/2/2.0), 60 mismatched (len 2/2/2.0)
+   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
+(8 rows)
+
+-- Test 16.3: Alternation with quantifiers
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 8 peak, 204 total, 0 merged
+   NFA Contexts: 4 peak, 51 total, 17 pruned
+   NFA: 16 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 16 skipped (len 2/2/2.0)
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+--
+-- Section 17: Group Pattern Tests
+--
+-- Test 17.1: Simple group
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a b)+
+   Storage: Memory  Maximum Storage: 18kB
+   NFA States: 3 peak, 61 total, 0 merged
+   NFA Contexts: 3 peak, 41 total, 20 pruned
+   NFA: 1 matched (len 40/40/40.0), 0 mismatched
+   NFA: 19 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
+
+-- Test 17.2: Group with bounded quantifier
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 7 peak, 66 total, 0 merged
+   NFA Contexts: 6 peak, 41 total, 20 pruned
+   NFA: 5 matched (len 8/8/8.0), 0 mismatched
+   NFA: 0 absorbed, 15 skipped (len 2/6/4.0)
+   ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
+
+-- Test 17.3: Nested groups
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 19kB
+   NFA States: 60 peak, 286 total, 0 merged
+   NFA Contexts: 32 peak, 61 total, 30 pruned
+   NFA: 1 matched (len 60/60/60.0), 1 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 28 skipped (len 4/58/31.0)
+   ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
+
+--
+-- Section 18: Window Function Combinations
+--
+-- Test 18.1: count(*) with pattern
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 3 peak, 55 total, 0 merged
+   NFA Contexts: 3 peak, 31 total, 6 pruned
+   NFA: 6 matched (len 5/5/5.0), 0 mismatched
+   NFA: 18 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
+(9 rows)
+
+-- Test 18.2: first_value with pattern
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 3 peak, 55 total, 0 merged
+   NFA Contexts: 3 peak, 31 total, 6 pruned
+   NFA: 6 matched (len 5/5/5.0), 0 mismatched
+   NFA: 18 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
+(9 rows)
+
+-- Test 18.3: last_value with pattern
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 18kB
+   NFA States: 3 peak, 55 total, 0 merged
+   NFA Contexts: 3 peak, 31 total, 6 pruned
+   NFA: 6 matched (len 5/5/5.0), 0 mismatched
+   NFA: 18 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
+(9 rows)
+
+-- Test 18.4: Multiple window functions
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 18kB
+   NFA States: 3 peak, 55 total, 0 merged
+   NFA Contexts: 3 peak, 31 total, 6 pruned
+   NFA: 6 matched (len 5/5/5.0), 0 mismatched
+   NFA: 18 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
+(9 rows)
+
+--
+-- Section 19: DEFINE Expression Variations
+--
+-- Test 19.1: Complex boolean expressions
+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)
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 3 peak, 78 total, 0 merged
+   NFA Contexts: 3 peak, 51 total, 23 pruned
+   NFA: 17 matched (len 2/3/2.6), 0 mismatched
+   NFA: 10 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(9 rows)
+
+-- Test 19.2: Using PREV function
+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)
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ 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: 18kB
+   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)
+
+-- Test 19.3: Using NULL comparisons
+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
+);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ WindowAgg (actual rows=30.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 3 peak, 55 total, 0 merged
+   NFA Contexts: 3 peak, 31 total, 6 pruned
+   NFA: 6 matched (len 5/5/5.0), 0 mismatched
+   NFA: 18 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series v (actual rows=30.00 loops=1)
+(9 rows)
+
+--
+-- Section 20: Large Scale Statistics Verification
+--
+-- Test 20.1: 500 rows - verify statistics scale correctly
+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
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 3 peak, 851 total, 0 merged
+   NFA Contexts: 3 peak, 501 total, 151 pruned
+   NFA: 50 matched (len 8/9/9.0), 0 mismatched
+   NFA: 299 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
+(9 rows)
+
+-- Test 20.2: High match count scenario
+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
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ WindowAgg (actual rows=500.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b
+   Storage: Memory  Maximum Storage: 17kB
+   NFA States: 2 peak, 501 total, 0 merged
+   NFA Contexts: 3 peak, 501 total, 250 pruned
+   NFA: 250 matched (len 2/2/2.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
+(8 rows)
+
+-- Test 20.3: High skip count scenario
+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
+);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ 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: 17kB
+   NFA States: 2 peak, 501 total, 0 merged
+   NFA Contexts: 3 peak, 501 total, 495 pruned
+   NFA: 5 matched (len 5/5/5.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
+(8 rows)
+
+-- Cleanup
+DROP TABLE nfa_test;
+DROP TABLE nfa_complex;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 021d57f66bb..f3f4ca7dfbe 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -102,7 +102,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-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
+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 rpr
 
 # ----------
 # 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..f9c161a791a
--- /dev/null
+++ b/src/test/regress/sql/rpr.sql
@@ -0,0 +1,2026 @@
+--
+-- 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
+);
+
+-- 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)
+);
+
+-- 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
+  );
+
+-- 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 (A{2}){3,5} -> A{6,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;  -- optimized: a{6,10}
+
+-- 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,}
+
+--
+-- 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 252 (RPR_VARID_MAX)
+
+-- Ok: 252 variables (maximum allowed)
+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 252
+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 | A+ B (greedy at front for absorption)
+-- Data: A B C X (D expected but X found)
+-- A+ B C D fails at row 4 (X instead of D)
+-- A+ B succeeds at row 2
+-- Result should be match 1-2 (A+ B)
+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)
+-- Without absorption: 4 matches (1-4, 2-4, 3-4, 4-4)
+-- With absorption: Row 1 match (1-4), rows 2-4 absorbed
+
+-- 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)
+-- With absorption: Row 1 match (1-4), rows 2-3 absorbed
+
+-- 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: potential 1-4, 2-4, 3-4
+-- Row 1 (1-4) absorbs Row 2 (2-4) and Row 3 (3-4) - same endpoint
+
+-- 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,}
+-- Potential matches: 1-6 (3 reps), 3-6 (2 reps), 5-6 needs 2 reps (fail)
+-- Row 1 (1-6) absorbs Row 3 (3-6) - same endpoint, top-level unbounded GROUP
+
+-- 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)
+);
+-- Row 1: 1-4, Row 2: absorbed (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
+-- BUG: Currently returns NULL instead of matching 1-4
+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
+-- Currently returns two separate matches (1-2, 3-4) instead of single 1-4
+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) - CURRENTLY ACTS LIKE A+ (GREEDY)
+-- Parser accepts the syntax but executor doesn't differentiate
+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)
+);
+-- Current: 1-4 (A A A B) - greedy behavior
+-- Expected when RELUCTANT implemented: 3-4 (A B) - shortest A before B
+
+-- Test: A{1,3}? B (reluctant bounded)
+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)
+);
+-- Current: 1-4 (greedy, takes max A before B)
+-- Expected when RELUCTANT implemented: 3-4 (takes min A before B)
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
new file mode 100644
index 00000000000..f070f05a497
--- /dev/null
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -0,0 +1,935 @@
+--
+-- Test: EXPLAIN ANALYZE output for Row Pattern Recognition NFA statistics
+--
+-- This file tests the NFA statistics shown in EXPLAIN ANALYZE output:
+-- - NFA States: peak, total, merged
+-- - NFA Contexts: peak, total, absorbed, skipped
+-- - NFA: matched (len min/max/avg), mismatched (len min/max/avg)
+--
+
+-- 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');
+
+--
+-- Section 1: Basic NFA Statistics Tests
+--
+
+-- Test 1.1: Simple pattern - should show basic statistics
+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'
+);
+
+-- Test 1.2: Pattern with no matches - 0 matched
+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'
+);
+
+-- Test 1.3: Pattern matching every row - high match count
+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
+);
+
+--
+-- Section 2: State Statistics Tests (peak, total, merged)
+--
+
+-- Test 2.1: Simple quantifier pattern - A+ with short matches (no merging)
+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
+);
+
+-- Test 2.2: Alternation pattern - multiple state branches
+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'
+);
+
+-- Test 2.3: Complex pattern with high state count
+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
+);
+
+-- Test 2.4: Grouped pattern with quantifier - state merging
+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
+);
+
+-- Test 2.5: State explosion pattern - many alternations
+-- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
+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
+);
+
+-- Test 2.6: High state merging - alternation with plus quantifier
+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
+);
+
+-- Test 2.7: Nested quantifiers causing state growth
+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
+);
+
+--
+-- Section 3: Context Statistics Tests (peak, total, absorbed, skipped)
+--
+
+-- Test 3.1: Context absorption with unbounded quantifier at start
+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
+);
+
+-- Test 3.2: No absorption - bounded quantifier
+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
+);
+
+-- Test 3.3: Contexts skipped by SKIP PAST LAST ROW
+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
+);
+
+-- Test 3.4: High context absorption - unbounded group
+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
+);
+
+--
+-- Section 4: Match Length Statistics Tests
+--
+
+-- Test 4.1: Fixed length matches - all same length
+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'
+);
+
+-- Test 4.2: Variable length matches - min/max/avg differ
+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
+);
+
+-- Test 4.3: Very long matches
+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
+);
+
+-- Test 4.4: Mix of short and long matches
+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
+);
+
+--
+-- Section 5: Mismatch Length Statistics Tests
+--
+
+-- Test 5.1: Pattern that causes mismatches with length > 1
+-- Mismatch happens when partial match fails after processing multiple rows
+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'
+);
+
+-- Test 5.2: Long partial matches that fail
+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'
+);
+
+--
+-- Section 6: JSON Format Tests
+--
+
+-- Test 6.1: JSON format output with all statistics
+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
+);
+
+-- Test 6.2: JSON format with match length statistics
+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
+);
+
+--
+-- Section 7: XML Format Tests
+--
+
+-- Test 7.1: XML format output
+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
+);
+
+--
+-- Section 8: Multiple Partitions Tests
+--
+
+-- Test 8.1: Statistics across multiple partitions
+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
+);
+
+-- Test 8.2: Different pattern behavior per partition
+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
+);
+
+--
+-- Section 9: Edge Cases
+--
+
+-- Test 9.1: Empty result set
+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
+);
+
+-- Test 9.2: Single row
+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
+);
+
+-- Test 9.3: Pattern longer than data
+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
+);
+
+-- Test 9.4: All rows match as single match
+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
+);
+
+--
+-- Section 10: Complex Pattern Tests
+--
+
+-- Test 10.1: Nested groups
+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
+);
+
+-- Test 10.2: Multiple alternations
+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'
+);
+
+-- Test 10.3: Optional elements
+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
+);
+
+-- Test 10.4: Bounded quantifiers
+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
+);
+
+-- Test 10.5: Star quantifier
+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
+);
+
+--
+-- Section 11: Real-world Pattern Examples
+--
+
+-- Test 11.1: Stock price pattern - V-shape (down then up)
+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'
+);
+
+-- Test 11.2: Stock price pattern - peak (up, stable, down)
+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'
+);
+
+-- Test 11.3: Consecutive increasing values (using PREV)
+-- FIXME: The original pattern was:
+--   DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
+-- This causes "ERROR: unrecognized node type: 15" (T_FuncExpr) because
+-- NullTest(FuncExpr(PREV)) is not properly handled somewhere in the planner.
+-- The expression v > PREV(v) works fine, but PREV(v) IS NULL fails.
+-- Using COALESCE(PREV(v), 0) as a workaround until the bug is fixed.
+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 > COALESCE(PREV(v), 0)
+);
+
+--
+-- Section 12: Performance-oriented Tests
+--
+
+-- Test 12.1: Large dataset with simple pattern
+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
+);
+
+-- Test 12.2: Large dataset with absorption
+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
+);
+
+-- Test 12.3: High state merge ratio
+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
+);
+
+--
+-- Section 13: INITIAL vs no INITIAL comparison
+--
+
+-- Test 13.1: With INITIAL keyword
+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
+);
+
+-- Test 13.2: Without INITIAL keyword (same behavior currently)
+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
+);
+
+--
+-- Section 14: Quantifier Variations
+--
+
+-- Test 14.1: Plus quantifier
+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
+);
+
+-- Test 14.2: Star quantifier (zero or more)
+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
+);
+
+-- Test 14.3: Question mark (zero or one)
+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
+);
+
+-- Test 14.4: Exact count {n}
+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
+);
+
+-- Test 14.5: Range {n,m}
+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
+);
+
+-- Test 14.6: At least {n,}
+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
+);
+
+--
+-- Section 15: Regression Tests for Statistics Accuracy
+--
+
+-- Test 15.1: Verify state count accuracy
+-- Pattern A+ B with 20 rows should show predictable state behavior
+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
+);
+
+-- Test 15.2: Verify context count with known absorption
+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
+);
+
+-- Test 15.3: Verify match length with fixed-length pattern
+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
+);
+
+--
+-- Section 16: Alternation Pattern Tests
+--
+
+-- Test 16.1: Simple alternation
+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'
+);
+
+-- Test 16.2: Multiple items in alternation
+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'
+);
+
+-- Test 16.3: Alternation with quantifiers
+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
+);
+
+--
+-- Section 17: Group Pattern Tests
+--
+
+-- Test 17.1: Simple group
+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
+);
+
+-- Test 17.2: Group with bounded quantifier
+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
+);
+
+-- Test 17.3: Nested groups
+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
+);
+
+--
+-- Section 18: Window Function Combinations
+--
+
+-- Test 18.1: count(*) with pattern
+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
+);
+
+-- Test 18.2: first_value with pattern
+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
+);
+
+-- Test 18.3: last_value with pattern
+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
+);
+
+-- Test 18.4: Multiple window functions
+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
+);
+
+--
+-- Section 19: DEFINE Expression Variations
+--
+
+-- Test 19.1: Complex boolean expressions
+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)
+);
+
+-- Test 19.2: Using PREV function
+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)
+);
+
+-- Test 19.3: Using NULL comparisons
+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
+);
+
+--
+-- Section 20: Large Scale Statistics Verification
+--
+
+-- Test 20.1: 500 rows - verify statistics scale correctly
+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
+);
+
+-- Test 20.2: High match count scenario
+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
+);
+
+-- Test 20.3: High skip count scenario
+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
+);
+
+-- Cleanup
+DROP TABLE nfa_test;
+DROP TABLE nfa_complex;
-- 
2.43.0

