From 1fd843f3979350b325508c31215c6f0346db663c Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii Date: Tue, 28 Jul 2026 11:04:32 +0900 Subject: [PATCH v50 8/9] Row pattern recognition patch (tests: expected). --- src/test/regress/expected/rpr.out | 4119 +++++++++ src/test/regress/expected/rpr_base.out | 7940 +++++++++++++++++ src/test/regress/expected/rpr_explain.out | 6404 +++++++++++++ src/test/regress/expected/rpr_integration.out | 1608 ++++ src/test/regress/expected/rpr_nfa.out | 4971 +++++++++++ src/test/regress/expected/window.out | 36 + 6 files changed, 25078 insertions(+) create mode 100644 src/test/regress/expected/rpr.out create mode 100644 src/test/regress/expected/rpr_base.out create mode 100644 src/test/regress/expected/rpr_explain.out create mode 100644 src/test/regress/expected/rpr_integration.out create mode 100644 src/test/regress/expected/rpr_nfa.out diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out new file mode 100644 index 00000000000..6080398f583 --- /dev/null +++ b/src/test/regress/expected/rpr.out @@ -0,0 +1,4119 @@ +-- +-- Test for row pattern recognition: WINDOW clause integration and +-- scenario tests using synthetic stock data. +-- +-- Parser/planner tests: rpr_base.sql +-- NFA engine tests: rpr_nfa.sql +-- EXPLAIN statistics tests: rpr_explain.sql +-- +\getenv abs_srcdir PG_ABS_SRCDIR +-- Synthetic stock data for RPR pattern matching tests +CREATE TABLE rpr_stock ( + part_id integer, + rn integer, + price numeric(10,3), + volume bigint, + open numeric(10,3), + low numeric(10,3), + high numeric(10,3) +); +\set filename :abs_srcdir '/data/stock.data' +COPY rpr_stock FROM :'filename'; +ANALYZE rpr_stock; +CREATE TEMP TABLE stock (company TEXT, tdate DATE, price INTEGER); +INSERT INTO stock VALUES +('company1', '2023-07-01', 100), ('company1', '2023-07-02', 200), +('company1', '2023-07-03', 150), ('company1', '2023-07-04', 140), +('company1', '2023-07-05', 150), ('company1', '2023-07-06', 90), +('company1', '2023-07-07', 110), ('company1', '2023-07-08', 130), +('company1', '2023-07-09', 120), ('company1', '2023-07-10', 130), +('company2', '2023-07-01', 50), ('company2', '2023-07-02', 2000), +('company2', '2023-07-03', 1500), ('company2', '2023-07-04', 1400), +('company2', '2023-07-05', 1500), ('company2', '2023-07-06', 60), +('company2', '2023-07-07', 1100), ('company2', '2023-07-08', 1300), +('company2', '2023-07-09', 1200), ('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 pattern matching with PREV/NEXT +-- +-- 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 fixed-length pattern (A A A = exactly 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 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) + +-- test prefix/suffix merge optimization with bounded quantifier +-- Pattern A B (A B){1,2} A B should be optimized to (A B){3,4} +CREATE TEMP TABLE rpr_t (id int, val text); +INSERT INTO rpr_t VALUES + (1,'A'),(2,'B'), + (3,'A'),(4,'B'), + (5,'A'),(6,'B'), + (7,'A'),(8,'B'), + (9,'X'); +SELECT id, val, count(*) OVER w AS match_count +FROM rpr_t +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + INITIAL + PATTERN (A B (A B){1,2} A B) + DEFINE + A AS val = 'A', + B AS val = 'B' +); + id | val | match_count +----+-----+------------- + 1 | A | 8 + 2 | B | 0 + 3 | A | 6 + 4 | B | 0 + 5 | A | 0 + 6 | B | 0 + 7 | A | 0 + 8 | B | 0 + 9 | X | 0 +(9 rows) + +DROP TABLE rpr_t; +-- 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 (same pattern as above; +-- match length is always 2, so result is identical to SKIP PAST LAST ROW. +-- SKIP TO NEXT ROW's distinct effect is tested in backtracking section.) +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) + +-- PREV returns NULL at partition's first row (null_slot path) +SELECT company, tdate, price, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (BOUNDARY REST+) + DEFINE + BOUNDARY AS PREV(price) IS NULL, + REST AS PREV(price) IS NOT NULL +); + company | tdate | price | count +----------+------------+-------+------- + company1 | 07-01-2023 | 100 | 10 + 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 | 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 | 10 + 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) + +-- NEXT returns NULL at partition's last row (null_slot path) +SELECT company, tdate, price, count(*) 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 + PATTERN (A+ BOUNDARY) + DEFINE + A AS NEXT(price) IS NOT NULL, + BOUNDARY AS NEXT(price) IS NULL +); + company | tdate | price | count +----------+------------+-------+------- + company1 | 07-01-2023 | 100 | 10 + 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 | 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 | 10 + 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) + +-- DESC order: PREV refers to the row with later date +SELECT company, tdate, price, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company + ORDER BY tdate DESC + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (START DOWN+ UP+) + DEFINE + START AS TRUE, + DOWN AS price < PREV(price), + UP AS price > PREV(price) +); + company | tdate | price | count +----------+------------+-------+------- + company1 | 07-10-2023 | 130 | 3 + company1 | 07-09-2023 | 120 | 0 + company1 | 07-08-2023 | 130 | 0 + company1 | 07-07-2023 | 110 | 3 + company1 | 07-06-2023 | 90 | 0 + company1 | 07-05-2023 | 150 | 0 + company1 | 07-04-2023 | 140 | 0 + company1 | 07-03-2023 | 150 | 0 + company1 | 07-02-2023 | 200 | 0 + company1 | 07-01-2023 | 100 | 0 + company2 | 07-10-2023 | 1300 | 3 + company2 | 07-09-2023 | 1200 | 0 + company2 | 07-08-2023 | 1300 | 0 + company2 | 07-07-2023 | 1100 | 3 + company2 | 07-06-2023 | 60 | 0 + company2 | 07-05-2023 | 1500 | 0 + company2 | 07-04-2023 | 1400 | 0 + company2 | 07-03-2023 | 1500 | 0 + company2 | 07-02-2023 | 2000 | 0 + company2 | 07-01-2023 | 50 | 0 +(20 rows) + +-- Multiple partitions with unequal sizes +WITH multi_part AS ( + SELECT * FROM (VALUES + ('a', 1, 10), ('a', 2, 20), ('a', 3, 15), + ('b', 1, 5), + ('c', 1, 100), ('c', 2, 200), ('c', 3, 150), ('c', 4, 140), ('c', 5, 300) + ) AS t(grp, id, val) +) +SELECT grp, id, val, count(*) OVER w +FROM multi_part +WINDOW w AS ( + PARTITION BY grp + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS val <= NEXT(val), + B AS val > PREV(val) OR val < PREV(val) +); + grp | id | val | count +-----+----+-----+------- + a | 1 | 10 | 3 + a | 2 | 20 | 0 + a | 3 | 15 | 0 + b | 1 | 5 | 0 + c | 1 | 100 | 5 + c | 2 | 200 | 0 + c | 3 | 150 | 0 + c | 4 | 140 | 0 + c | 5 | 300 | 0 +(9 rows) + +-- FLOAT/NUMERIC DEFINE conditions +WITH float_data AS ( + SELECT * FROM (VALUES + (1, 1.0::float8), (2, 1.5), (3, 1.4999), (4, 1.50001), (5, 0.1) + ) AS t(id, val) +) +SELECT id, val, count(*) OVER w +FROM float_data +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 TRUE, + B AS val > PREV(val) * 0.99 +); + id | val | count +----+---------+------- + 1 | 1 | 4 + 2 | 1.5 | 0 + 3 | 1.4999 | 0 + 4 | 1.50001 | 0 + 5 | 0.1 | 0 +(5 rows) + +-- +-- Error cases: PREV/NEXT usage restrictions +-- +-- Nested PREV +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > PREV(PREV(price)) +); +ERROR: PREV and NEXT cannot contain PREV or NEXT +LINE 7: DEFINE A AS price > PREV(PREV(price)) + ^ +HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. +-- Nested NEXT +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > NEXT(NEXT(price)) +); +ERROR: PREV and NEXT cannot contain PREV or NEXT +LINE 7: DEFINE A AS price > NEXT(NEXT(price)) + ^ +HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. +-- PREV nested inside NEXT +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > NEXT(PREV(price)) +); +ERROR: PREV and NEXT cannot contain PREV or NEXT +LINE 7: DEFINE A AS price > NEXT(PREV(price)) + ^ +HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. +-- PREV nested inside expression inside NEXT +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > NEXT(price * PREV(price)) +); +ERROR: PREV and NEXT cannot contain PREV or NEXT +LINE 7: DEFINE A AS price > NEXT(price * PREV(price)) + ^ +HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. +-- Triple nesting: error reported at outermost PREV +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > PREV(PREV(PREV(price))) +); +ERROR: PREV and NEXT cannot contain PREV or NEXT +LINE 7: DEFINE A AS price > PREV(PREV(PREV(price))) + ^ +HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. +-- No column reference in PREV/NEXT argument +-- PREV(1): constant only, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(1) > 0 +); +ERROR: argument of row pattern navigation operation must include at least one column reference +LINE 7: DEFINE A AS PREV(1) > 0 + ^ +-- NEXT(1 + 2): constant expression, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS NEXT(1 + 2) > 0 +); +ERROR: argument of row pattern navigation operation must include at least one column reference +LINE 7: DEFINE A AS NEXT(1 + 2) > 0 + ^ +-- 2-arg form: PREV(1, 1): constant expression as first arg +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(1, 1) > 0 +); +ERROR: argument of row pattern navigation operation must include at least one column reference +LINE 7: DEFINE A AS PREV(1, 1) > 0 + ^ +-- Compound navigation without a column reference must be rejected too, +-- consistent with the simple forms above. +-- PREV(FIRST(1)): compound, constant only, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(FIRST(1)) > 0 +); +ERROR: argument of row pattern navigation operation must include at least one column reference +LINE 7: DEFINE A AS PREV(FIRST(1)) > 0 + ^ +-- NEXT(LAST(1 + 2)): compound, constant expression, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS NEXT(LAST(1 + 2)) > 0 +); +ERROR: argument of row pattern navigation operation must include at least one column reference +LINE 7: DEFINE A AS NEXT(LAST(1 + 2)) > 0 + ^ +-- PREV(FIRST(1, 2)): compound, two-arg inner, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(FIRST(1, 2)) > 0 +); +ERROR: argument of row pattern navigation operation must include at least one column reference +LINE 7: DEFINE A AS PREV(FIRST(1, 2)) > 0 + ^ +-- PREV(FIRST(1), 2): compound, outer offset only, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(FIRST(1), 2) > 0 +); +ERROR: argument of row pattern navigation operation must include at least one column reference +LINE 7: DEFINE A AS PREV(FIRST(1), 2) > 0 + ^ +-- PREV(FIRST(1, 2), 3): compound, inner and outer offsets, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(FIRST(1, 2), 3) > 0 +); +ERROR: argument of row pattern navigation operation must include at least one column reference +LINE 7: DEFINE A AS PREV(FIRST(1, 2), 3) > 0 + ^ +-- Non-constant offset: column reference as offset +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(price, price) > 0 +); +ERROR: row pattern navigation offset must be a run-time constant +LINE 7: DEFINE A AS PREV(price, price) > 0 + ^ +-- Non-constant offset: column reference in compound inner offset +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(LAST(price, price), 2) > 0 +); +ERROR: row pattern navigation offset must be a run-time constant +LINE 7: DEFINE A AS PREV(LAST(price, price), 2) > 0 + ^ +-- Non-constant offset: column reference in compound outer offset +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(LAST(price, 1), price) > 0 +); +ERROR: row pattern navigation offset must be a run-time constant +LINE 7: DEFINE A AS PREV(LAST(price, 1), price) > 0 + ^ +-- Non-constant offset: volatile function as offset +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(price, random()::int) > 0 +); +ERROR: volatile functions are not allowed in DEFINE clause +LINE 7: DEFINE A AS PREV(price, random()::int) > 0 + ^ +-- Non-constant offset: subquery as offset +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(price, (SELECT 1)) > 0 +); +ERROR: cannot use subquery in DEFINE expression +LINE 7: DEFINE A AS PREV(price, (SELECT 1)) > 0 + ^ +-- First arg: subquery (caught by DEFINE-level subquery restriction) +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(price + (SELECT 1)) > 0 +); +ERROR: cannot use subquery in DEFINE expression +LINE 7: DEFINE A AS PREV(price + (SELECT 1)) > 0 + ^ +-- Volatile function inside nav.arg is rejected in the planner +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(price + random() * 0) >= 0 +); +ERROR: volatile functions are not allowed in DEFINE clause +LINE 8: DEFINE A AS PREV(price + random() * 0) >= 0 + ^ +-- nextval is volatile (per pg_proc), so it is rejected via the FuncExpr +-- path with the "volatile functions" message +CREATE SEQUENCE rpr_seq; +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > nextval('rpr_seq') +); +ERROR: volatile functions are not allowed in DEFINE clause +LINE 7: DEFINE A AS price > nextval('rpr_seq') + ^ +DROP SEQUENCE rpr_seq; +-- A volatile DEFINE is now rejected in the planner, not at parse time, so a +-- view that hides one is created successfully and only errors when read. +CREATE TEMP VIEW rpr_volatile_view AS +SELECT company, tdate, price, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A+) + DEFINE A AS price > random() * 0 +); +SELECT * FROM rpr_volatile_view; +ERROR: volatile functions are not allowed in DEFINE clause +DROP VIEW rpr_volatile_view; +-- DEFINE cannot reference an outer query's column. A correlated outer +-- reference must produce a clean error, not the internal "Upper-level Var" +-- elog that pull_var_clause would otherwise raise. +-- Qualified outer reference (o.threshold): +SELECT * FROM (VALUES (95)) AS o(threshold), +LATERAL ( + SELECT price FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > o.threshold + ) +) s; +ERROR: cannot use outer query column in DEFINE clause +LINE 9: DEFINE A AS price > o.threshold + ^ +-- Unqualified name resolving to the outer column (threshold): +SELECT * FROM (VALUES (95)) AS o(threshold), +LATERAL ( + SELECT price FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > threshold + ) +) s; +ERROR: cannot use outer query column in DEFINE clause +LINE 9: DEFINE A AS price > threshold + ^ +-- Outer reference inside a navigation argument is rejected too: +SELECT * FROM (VALUES (95)) AS o(threshold), +LATERAL ( + SELECT price FROM stock + WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(o.threshold, 1) > 0 + ) +) s; +ERROR: cannot use outer query column in DEFINE clause +LINE 8: DEFINE A AS PREV(o.threshold, 1) > 0 + ^ +-- DEFINE rejects a schema-qualified column reference (three or more name +-- parts) once it resolves; the qualified form itself is not allowed. (stock +-- is a temp table, so it is qualified with pg_temp here.) +-- 3-part (schema.table.column): +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS pg_temp.stock.price > 0 +); +ERROR: qualified expression "pg_temp.stock.price" is not allowed in DEFINE clause +LINE 7: DEFINE A AS pg_temp.stock.price > 0 + ^ +-- whole-row variant (schema.table.*): +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS (pg_temp.stock.*) IS NOT NULL +); +ERROR: qualified expression "pg_temp.stock.*" is not allowed in DEFINE clause +LINE 7: DEFINE A AS (pg_temp.stock.*) IS NOT NULL + ^ +-- A two-part table-qualified whole-row reference is rejected as well, through +-- a separate range-variable check (a bare relation name is instead accepted +-- as a whole-row Var). +-- 2-part (table.*): +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS (stock.*) IS NOT NULL +); +ERROR: range variable qualified expression "stock.*" is not allowed in DEFINE clause +LINE 7: DEFINE A AS (stock.*) IS NOT NULL + ^ +-- +-- 2-arg PREV/NEXT: functional tests +-- +-- PREV(price, 2): match rows where current price > price 2 rows back +-- stock: 100, 90, 80, 95, 110 +-- Pattern (A B+): A=any, B where price > PREV(price, 2) +-- At pos 2 (80): A matches. pos 3 (95): 95 > PREV(95,2)=90 TRUE. +-- pos 4 (110): 110 > PREV(110,2)=80 TRUE. Match! +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS price > PREV(price, 2) +); + company | tdate | price | first_value | last_value | count +----------+------------+-------+-------------+------------+------- + company1 | 07-01-2023 | 100 | | | 0 + company1 | 07-02-2023 | 200 | 200 | 150 | 2 + 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 | 110 | 120 | 3 + 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 | 2000 | 1500 | 2 + 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 | 1100 | 1200 | 3 + company2 | 07-08-2023 | 1300 | | | 0 + company2 | 07-09-2023 | 1200 | | | 0 + company2 | 07-10-2023 | 1300 | | | 0 +(20 rows) + +-- NEXT(price, 2): match rows where current price > price 2 rows ahead +-- pos 0 (100): NEXT(100,2)=80, 100>80 TRUE. pos 1 (90): NEXT(90,2)=95, 90>95 FALSE. Match ends. +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS price > NEXT(price, 2) +); + company | tdate | price | first_value | last_value | count +----------+------------+-------+-------------+------------+------- + company1 | 07-01-2023 | 100 | | | 0 + company1 | 07-02-2023 | 200 | 200 | 200 | 1 + company1 | 07-03-2023 | 150 | | | 0 + company1 | 07-04-2023 | 140 | 140 | 150 | 2 + 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 | 2000 | 2000 | 1 + company2 | 07-03-2023 | 1500 | | | 0 + company2 | 07-04-2023 | 1400 | 1400 | 1500 | 2 + 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) + +-- Expressions inside PREV/NEXT arg: expr is evaluated on target row +-- PREV(price - 50, 1): fetches (price - 50) from 1 row back +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS price > PREV(price - 50, 1) +); + company | tdate | price | first_value | last_value | count +----------+------------+-------+-------------+------------+------- + company1 | 07-01-2023 | 100 | | | 0 + company1 | 07-02-2023 | 200 | 200 | 200 | 1 + company1 | 07-03-2023 | 150 | | | 0 + company1 | 07-04-2023 | 140 | 140 | 150 | 2 + company1 | 07-05-2023 | 150 | | | 0 + company1 | 07-06-2023 | 90 | | | 0 + company1 | 07-07-2023 | 110 | 110 | 130 | 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 | 2000 | 2000 | 1 + company2 | 07-03-2023 | 1500 | | | 0 + company2 | 07-04-2023 | 1400 | | | 0 + company2 | 07-05-2023 | 1500 | 1500 | 1500 | 1 + company2 | 07-06-2023 | 60 | | | 0 + company2 | 07-07-2023 | 1100 | 1100 | 1300 | 2 + company2 | 07-08-2023 | 1300 | | | 0 + company2 | 07-09-2023 | 1200 | | | 0 + company2 | 07-10-2023 | 1300 | 1300 | 1300 | 1 +(20 rows) + +-- NEXT(price * 2, 1): fetches (price * 2) from 1 row ahead +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS price < NEXT(price * 2, 1) +); + company | tdate | price | first_value | last_value | count +----------+------------+-------+-------------+------------+------- + company1 | 07-01-2023 | 100 | 100 | 120 | 9 + 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 | | | 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 | 50 | 1400 | 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 | 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) + +-- Large offset: PREV(val, 999) on 1000-row series matches only last row +-- NEXT(val, 999) matches only first row +SELECT val, first_value(val) OVER w, last_value(val) OVER w, count(*) OVER w +FROM generate_series(1, 1000) AS t(val) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(val, 999) = 1 +) +ORDER BY val DESC LIMIT 3; + val | first_value | last_value | count +------+-------------+------------+------- + 1000 | 1000 | 1000 | 1 + 999 | | | 0 + 998 | | | 0 +(3 rows) + +SELECT val, first_value(val) OVER w, last_value(val) OVER w, count(*) OVER w +FROM generate_series(1, 1000) AS t(val) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(val, 999) = 1000 +) +LIMIT 3; + val | first_value | last_value | count +-----+-------------+------------+------- + 1 | 1 | 1 | 1 + 2 | | | 0 + 3 | | | 0 +(3 rows) + +-- PREV(price, 0): offset 0 means current row, always equal to price +-- A+ matches entire partition as one group; count = partition size +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(price, 0) = price +); + company | tdate | price | first_value | last_value | count +----------+------------+-------+-------------+------------+------- + company1 | 07-01-2023 | 100 | 100 | 130 | 10 + 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 | | | 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 | 50 | 1300 | 10 + 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) + +-- 2-arg PREV/NEXT: negative offset +SELECT company, tdate, price, first_value(price) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(price, -1) IS NOT NULL +); +ERROR: row pattern navigation offset must not be negative +-- 2-arg PREV/NEXT: NULL offset (typed) +SELECT company, tdate, price, first_value(price) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(price, NULL::int8) IS NOT NULL +); +ERROR: row pattern navigation offset must not be null +-- 2-arg PREV/NEXT: NULL offset (untyped) +SELECT company, tdate, price, first_value(price) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(price, NULL) IS NOT NULL +); +ERROR: row pattern navigation offset must not be null +-- 2-arg PREV/NEXT: host variable negative and NULL +PREPARE test_prev_offset(int8) AS +SELECT company, tdate, price, first_value(price) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS price > PREV(price, $1) +); +EXECUTE test_prev_offset(-1); +ERROR: row pattern navigation offset must not be negative +EXECUTE test_prev_offset(NULL); +ERROR: row pattern navigation offset must not be null +DEALLOCATE test_prev_offset; +-- 2-arg PREV/NEXT: host variable with expression (0 + $1) +PREPARE test_prev_offset(int8) AS +SELECT company, tdate, price, first_value(price) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS price > PREV(price, 0 + $1) +); +EXECUTE test_prev_offset(-1); +ERROR: row pattern navigation offset must not be negative +EXECUTE test_prev_offset(NULL); +ERROR: row pattern navigation offset must not be null +DEALLOCATE test_prev_offset; +-- 2-arg PREV/NEXT: host variable with positive value +-- Exercises RPR_NAV_OFFSET_NEEDS_EVAL -> eval_nav_max_offset() path +PREPARE test_prev_offset(int8) AS +SELECT company, tdate, price, first_value(price) OVER w, count(*) 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 + PATTERN (A B+) + DEFINE A AS TRUE, B AS price > PREV(price, $1) +); +EXECUTE test_prev_offset(1); + company | tdate | price | first_value | count +----------+------------+-------+-------------+------- + company1 | 07-01-2023 | 100 | 100 | 2 + company1 | 07-02-2023 | 200 | | 0 + company1 | 07-03-2023 | 150 | | 0 + company1 | 07-04-2023 | 140 | 140 | 2 + company1 | 07-05-2023 | 150 | | 0 + company1 | 07-06-2023 | 90 | 90 | 3 + company1 | 07-07-2023 | 110 | | 0 + company1 | 07-08-2023 | 130 | | 0 + company1 | 07-09-2023 | 120 | 120 | 2 + company1 | 07-10-2023 | 130 | | 0 + company2 | 07-01-2023 | 50 | 50 | 2 + company2 | 07-02-2023 | 2000 | | 0 + company2 | 07-03-2023 | 1500 | | 0 + company2 | 07-04-2023 | 1400 | 1400 | 2 + company2 | 07-05-2023 | 1500 | | 0 + company2 | 07-06-2023 | 60 | 60 | 3 + company2 | 07-07-2023 | 1100 | | 0 + company2 | 07-08-2023 | 1300 | | 0 + company2 | 07-09-2023 | 1200 | 1200 | 2 + company2 | 07-10-2023 | 1300 | | 0 +(20 rows) + +EXECUTE test_prev_offset(2); + company | tdate | price | first_value | count +----------+------------+-------+-------------+------- + company1 | 07-01-2023 | 100 | | 0 + company1 | 07-02-2023 | 200 | 200 | 2 + 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 | 110 | 3 + 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 | 2000 | 2 + 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 | 1100 | 3 + company2 | 07-08-2023 | 1300 | | 0 + company2 | 07-09-2023 | 1200 | | 0 + company2 | 07-10-2023 | 1300 | | 0 +(20 rows) + +DEALLOCATE test_prev_offset; +-- 2-arg: two PREV with different offsets in same DEFINE clause +-- B: price exceeds both 1-back and 2-back values +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) 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 + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS price > PREV(price, 1) AND price > PREV(price, 2) +); + company | tdate | price | first_value | last_value | count +----------+------------+-------+-------------+------------+------- + company1 | 07-01-2023 | 100 | | | 0 + 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 | | | 0 + company1 | 07-07-2023 | 110 | 110 | 130 | 2 + 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 | 1100 | 1300 | 2 + company2 | 07-08-2023 | 1300 | | | 0 + company2 | 07-09-2023 | 1200 | | | 0 + company2 | 07-10-2023 | 1300 | | | 0 +(20 rows) + +-- 2-arg: PREV and NEXT with explicit offsets in same DEFINE clause +-- A: price exceeds 1-back and is below 1-ahead (ascending interior point) +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) 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 + PATTERN (A+) + DEFINE A AS price > PREV(price, 1) AND price < NEXT(price, 1) +); + company | tdate | price | first_value | last_value | count +----------+------------+-------+-------------+------------+------- + company1 | 07-01-2023 | 100 | | | 0 + 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 | | | 0 + company1 | 07-07-2023 | 110 | 110 | 110 | 1 + 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 | 1100 | 1100 | 1 + company2 | 07-08-2023 | 1300 | | | 0 + company2 | 07-09-2023 | 1200 | | | 0 + company2 | 07-10-2023 | 1300 | | | 0 +(20 rows) + +-- Pass-by-ref types: two PREV calls targeting different positions. +-- Verifies that datumCopy in RESTORE prevents dangling pointers when +-- nav_slot is re-fetched for the second navigation. +-- tdate::text gives distinct text values per row (e.g. '07-01-2023'). +-- B matches when 1-back date text > 2-back date text (always true for +-- ascending dates), so B+ extends the full partition after A. +SELECT company, tdate, tdate::text AS tdate_text, + first_value(tdate::text) OVER w, last_value(tdate::text) OVER w, count(*) 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 + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS PREV(tdate::text, 1) > PREV(tdate::text, 2) +); + company | tdate | tdate_text | first_value | last_value | count +----------+------------+------------+-------------+------------+------- + company1 | 07-01-2023 | 07-01-2023 | | | 0 + company1 | 07-02-2023 | 07-02-2023 | 07-02-2023 | 07-10-2023 | 9 + company1 | 07-03-2023 | 07-03-2023 | | | 0 + company1 | 07-04-2023 | 07-04-2023 | | | 0 + company1 | 07-05-2023 | 07-05-2023 | | | 0 + company1 | 07-06-2023 | 07-06-2023 | | | 0 + company1 | 07-07-2023 | 07-07-2023 | | | 0 + company1 | 07-08-2023 | 07-08-2023 | | | 0 + company1 | 07-09-2023 | 07-09-2023 | | | 0 + company1 | 07-10-2023 | 07-10-2023 | | | 0 + company2 | 07-01-2023 | 07-01-2023 | | | 0 + company2 | 07-02-2023 | 07-02-2023 | 07-02-2023 | 07-10-2023 | 9 + company2 | 07-03-2023 | 07-03-2023 | | | 0 + company2 | 07-04-2023 | 07-04-2023 | | | 0 + company2 | 07-05-2023 | 07-05-2023 | | | 0 + company2 | 07-06-2023 | 07-06-2023 | | | 0 + company2 | 07-07-2023 | 07-07-2023 | | | 0 + company2 | 07-08-2023 | 07-08-2023 | | | 0 + company2 | 07-09-2023 | 07-09-2023 | | | 0 + company2 | 07-10-2023 | 07-10-2023 | | | 0 +(20 rows) + +-- numeric: PREV(price::numeric, 1) > PREV(price::numeric, 2) +-- B matches when price 1-back > price 2-back (ascending pair). +SELECT company, tdate, price::numeric AS nprice, + first_value(price::numeric) OVER w, last_value(price::numeric) OVER w, count(*) 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 + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS PREV(price::numeric, 1) > PREV(price::numeric, 2) +); + company | tdate | nprice | first_value | last_value | count +----------+------------+--------+-------------+------------+------- + company1 | 07-01-2023 | 100 | | | 0 + company1 | 07-02-2023 | 200 | 200 | 150 | 2 + company1 | 07-03-2023 | 150 | | | 0 + company1 | 07-04-2023 | 140 | | | 0 + company1 | 07-05-2023 | 150 | 150 | 90 | 2 + company1 | 07-06-2023 | 90 | | | 0 + company1 | 07-07-2023 | 110 | 110 | 120 | 3 + 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 | 2000 | 1500 | 2 + company2 | 07-03-2023 | 1500 | | | 0 + company2 | 07-04-2023 | 1400 | | | 0 + company2 | 07-05-2023 | 1500 | 1500 | 60 | 2 + company2 | 07-06-2023 | 60 | | | 0 + company2 | 07-07-2023 | 1100 | 1100 | 1200 | 3 + company2 | 07-08-2023 | 1300 | | | 0 + company2 | 07-09-2023 | 1200 | | | 0 + company2 | 07-10-2023 | 1300 | | | 0 +(20 rows) + +-- Typmod coercion over a navigation result: casting PREV(p) (a numeric(10,3) +-- column) to a narrower numeric(8,2) inside DEFINE forces coerce_type_typmod, +-- which calls exprTypmod() on the RPRNavExpr. +CREATE TEMP TABLE rpr_typmod (id int, p numeric(10,3)); +INSERT INTO rpr_typmod VALUES (1, 1.5), (2, 2.5), (3, 3.5); +SELECT id, count(*) OVER w AS cnt +FROM rpr_typmod +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS CAST(PREV(p) AS numeric(8,2)) > 0 +); + id | cnt +----+----- + 1 | 3 + 2 | 0 + 3 | 0 +(3 rows) + +DROP TABLE rpr_typmod; +-- +-- FIRST/LAST navigation +-- +-- Test data for FIRST/LAST: values cycle back so FIRST(val) = LAST(val) +-- at specific positions. +CREATE TEMP TABLE rpr_nav (id int, val int); +INSERT INTO rpr_nav VALUES (1,10),(2,20),(3,30),(4,10),(5,50),(6,10); +-- FIRST(val) = constant: B matches when match_start has val=10 +-- match_start=1(10): A=id1, B=id2, FIRST(val)=10 -> match {1,2} +-- match_start=3(30): A=id3, B=id4, FIRST(val)=30!=10 -> no match +-- match_start=4(10): A=id4, B=id5, FIRST(val)=10 -> match {4,5} +SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml +FROM rpr_nav 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 TRUE, B AS FIRST(val) = 10 +); + id | val | mf | ml +----+-----+----+---- + 1 | 10 | 1 | 2 + 2 | 20 | | + 3 | 30 | | + 4 | 10 | 4 | 5 + 5 | 50 | | + 6 | 10 | | +(6 rows) + +-- LAST(val): always equals current row's val (offset 0 default) +-- Equivalent to: B AS val > 15 +SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml +FROM rpr_nav 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 TRUE, B AS LAST(val) > 15 +); + id | val | mf | ml +----+-----+----+---- + 1 | 10 | 1 | 2 + 2 | 20 | | + 3 | 30 | | + 4 | 10 | 4 | 5 + 5 | 50 | | + 6 | 10 | | +(6 rows) + +-- Reluctant A+? with FIRST(val) = LAST(val): find shortest match where +-- first and last rows have the same val. +-- match_start=1(10): reluctant tries B early: +-- id2(20!=10), id3(30!=10), id4(10=10) -> match {1,2,3,4} +-- match_start=5(50): id6(10!=50) -> no match +SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml +FROM rpr_nav 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 TRUE, B AS FIRST(val) = LAST(val) +); + id | val | mf | ml +----+-----+----+---- + 1 | 10 | 1 | 4 + 2 | 20 | | + 3 | 30 | | + 4 | 10 | | + 5 | 50 | | + 6 | 10 | | +(6 rows) + +-- Greedy A+ with FIRST(val) = LAST(val): find longest match where +-- first and last rows have the same val. +-- match_start=1(10): greedy A eats all, B tries last: +-- id6(10=10) -> match {1,2,3,4,5,6} +SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml +FROM rpr_nav 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 TRUE, B AS FIRST(val) = LAST(val) +); + id | val | mf | ml +----+-----+----+---- + 1 | 10 | 1 | 6 + 2 | 20 | | + 3 | 30 | | + 4 | 10 | | + 5 | 50 | | + 6 | 10 | | +(6 rows) + +-- SKIP TO NEXT ROW with FIRST(val) = LAST(val): overlapping match attempts. +-- With ONE ROW PER MATCH, each row shows only its first match result. +SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml +FROM rpr_nav 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 TRUE, B AS FIRST(val) = LAST(val) +); + id | val | mf | ml +----+-----+----+---- + 1 | 10 | 1 | 4 + 2 | 20 | | + 3 | 30 | | + 4 | 10 | 4 | 6 + 5 | 50 | | + 6 | 10 | | +(6 rows) + +-- FIRST/LAST 2-arg offset form +-- +-- FIRST(val, 0) = FIRST(val): match_start row +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS FIRST(val, 0) = 10 +); + id | val | mf | cnt +----+-----+----+----- + 1 | 10 | 1 | 6 + 2 | 20 | | 0 + 3 | 30 | | 0 + 4 | 10 | | 0 + 5 | 50 | | 0 + 6 | 10 | | 0 +(6 rows) + +-- FIRST(val, 1): match_start + 1 row (second row of match) +-- match_start=1(10): FIRST(val,1)=20, B needs val=20 -> id2(20) match, id3(30) no +-- match_start=3(30): FIRST(val,1)=10, B needs val=10 -> id4(10) match +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS val = FIRST(val, 1) +); + id | val | mf | cnt +----+-----+----+----- + 1 | 10 | 1 | 2 + 2 | 20 | | 0 + 3 | 30 | 3 | 2 + 4 | 10 | | 0 + 5 | 50 | 5 | 2 + 6 | 10 | | 0 +(6 rows) + +-- FIRST(val, 99): offset beyond match range -> NULL, no match +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS FIRST(val, 99) IS NOT NULL +); + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 10 | 0 + 5 | 50 | 0 + 6 | 10 | 0 +(6 rows) + +-- LAST(val, 0) = LAST(val): current row +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS LAST(val, 0) > 15 +); + id | val | mf | cnt +----+-----+----+----- + 1 | 10 | 1 | 3 + 2 | 20 | | 0 + 3 | 30 | | 0 + 4 | 10 | 4 | 2 + 5 | 50 | | 0 + 6 | 10 | | 0 +(6 rows) + +-- LAST(val, 1): one row back from current (previous match row) +-- At B evaluation on id2: LAST(val,1) = val at id1 = 10 +-- B matches when previous row val < 30 +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS LAST(val, 1) < 30 +); + id | val | mf | cnt +----+-----+----+----- + 1 | 10 | 1 | 3 + 2 | 20 | | 0 + 3 | 30 | | 0 + 4 | 10 | 4 | 2 + 5 | 50 | | 0 + 6 | 10 | | 0 +(6 rows) + +-- LAST(val, 99): offset before match_start -> NULL +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS LAST(val, 99) IS NOT NULL +); + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 10 | 0 + 5 | 50 | 0 + 6 | 10 | 0 +(6 rows) + +-- Error: NULL offset +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS FIRST(val, NULL::int8) IS NULL +); +ERROR: row pattern navigation offset must not be null +-- Error: negative offset +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS LAST(val, -1) IS NULL +); +ERROR: row pattern navigation offset must not be negative +-- Functional notation: should access column, not RPR navigation +CREATE TEMP TABLE rpr_names (prev int, next int, first text, last text); +INSERT INTO rpr_names VALUES (1, 2, 'Joe', 'Blow'); +SELECT prev(f), next(f), first(f), last(f) FROM rpr_names f; + prev | next | first | last +------+------+-------+------ + 1 | 2 | Joe | Blow +(1 row) + +DROP TABLE rpr_names; +-- Compound navigation: PREV(FIRST(val), M) +-- rpr_nav: (1,10),(2,20),(3,30),(4,10),(5,50),(6,10) +-- PREV(FIRST(val), 1): target = match_start + 0 - 1 = match_start - 1 +-- At match_start=1: target=0 -> out of range -> NULL +-- At match_start=3: target=2(val=20) -> 20 > 0 -> true +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS PREV(FIRST(val), 1) > 0 +); + id | val | mf | cnt +----+-----+----+----- + 1 | 10 | | 0 + 2 | 20 | 2 | 5 + 3 | 30 | | 0 + 4 | 10 | | 0 + 5 | 50 | | 0 + 6 | 10 | | 0 +(6 rows) + +-- NEXT(FIRST(val, 1), 1): target = match_start + 1 + 1 = match_start + 2 +-- At match_start=1, B on id2: target=1+1+1=3(val=30), 30>0 -> true +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS NEXT(FIRST(val, 1), 1) > 0 +); + id | val | mf | cnt +----+-----+----+----- + 1 | 10 | 1 | 6 + 2 | 20 | | 0 + 3 | 30 | | 0 + 4 | 10 | | 0 + 5 | 50 | | 0 + 6 | 10 | | 0 +(6 rows) + +-- PREV(LAST(val), 2): LAST(val) is the current row (inner offset 0), so +-- target = currentpos - 0 - 2 = currentpos - 2. Same backward reach as PREV(val, 2). +-- At currentpos=2 (start id=1): target=0 -> out of range -> NULL -> B fails. +-- At currentpos=3 (start id=2): target=1(val=10) -> in range -> B runs on id3..id6, +-- so the match is id2..id6. +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS PREV(LAST(val), 2) IS NOT NULL +); + id | val | mf | cnt +----+-----+----+----- + 1 | 10 | | 0 + 2 | 20 | 2 | 5 + 3 | 30 | | 0 + 4 | 10 | | 0 + 5 | 50 | | 0 + 6 | 10 | | 0 +(6 rows) + +-- NEXT(LAST(val, 1), 2): LAST(val, 1) is one row back (inner offset 1), then +-- NEXT adds 2, so target = currentpos - 1 + 2 = currentpos + 1. Looks one row +-- ahead: same as NEXT(val, 1). +-- At currentpos=2 (start id=1): target=3(val=30) -> in range -> B true. +-- B stays true through id5 (target=6); at id6 target=7 -> out of range -> NULL, +-- so the match is id1..id5. +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS NEXT(LAST(val, 1), 2) IS NOT NULL +); + id | val | mf | cnt +----+-----+----+----- + 1 | 10 | 1 | 5 + 2 | 20 | | 0 + 3 | 30 | | 0 + 4 | 10 | | 0 + 5 | 50 | | 0 + 6 | 10 | | 0 +(6 rows) + +-- Compound: outer offset beyond partition (PREV far back) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(val), 99) IS NOT NULL +); + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 10 | 0 + 5 | 50 | 0 + 6 | 10 | 0 +(6 rows) + +-- Compound: outer offset beyond partition (NEXT far forward) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(FIRST(val), 99) IS NOT NULL +); + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 10 | 0 + 5 | 50 | 0 + 6 | 10 | 0 +(6 rows) + +-- Compound: inner offset beyond match range (FIRST offset too large) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(val, 99), 1) IS NOT NULL +); + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 10 | 0 + 5 | 50 | 0 + 6 | 10 | 0 +(6 rows) + +-- Compound: inner offset beyond match range (LAST offset too large) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(LAST(val, 99), 1) IS NOT NULL +); + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 10 | 0 + 5 | 50 | 0 + 6 | 10 | 0 +(6 rows) + +-- Compound: NULL outer offset (runtime error) +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(FIRST(val), NULL::int8) IS NULL +); +ERROR: row pattern navigation offset must not be null +-- Compound: negative outer offset (runtime error) +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(LAST(val), -1) IS NULL +); +ERROR: row pattern navigation offset must not be negative +-- Outer offset overflows int64: target position out of range -> NULL. +-- Plain NEXT(val, INT64_MAX): currentpos + INT64_MAX overflows. +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(val, 9223372036854775807) IS NULL +); + id | val | count +----+-----+------- + 1 | 10 | 6 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 10 | 0 + 5 | 50 | 0 + 6 | 10 | 0 +(6 rows) + +-- Compound NEXT(FIRST()): outer offset overflow. Inner offset 1 forces +-- inner_pos >= 1, so inner_pos + INT64_MAX overflows at every match. +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(FIRST(val, 1), 9223372036854775807) IS NULL +); + id | val | count +----+-----+------- + 1 | 10 | 6 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 10 | 0 + 5 | 50 | 0 + 6 | 10 | 0 +(6 rows) + +-- Compound NEXT(LAST()): outer offset overflow. +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(LAST(val), 9223372036854775807) IS NULL +); + id | val | count +----+-----+------- + 1 | 10 | 6 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 10 | 0 + 5 | 50 | 0 + 6 | 10 | 0 +(6 rows) + +-- Compound: default offsets on both sides +-- PREV(FIRST(val)): inner=0 (match_start), outer=1 -> target = match_start - 1 +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS PREV(FIRST(val)) IS NOT NULL +); + id | val | mf | cnt +----+-----+----+----- + 1 | 10 | | 0 + 2 | 20 | 2 | 5 + 3 | 30 | | 0 + 4 | 10 | | 0 + 5 | 50 | | 0 + 6 | 10 | | 0 +(6 rows) + +-- NEXT(LAST(val)): inner=0 (currentpos), outer=1 -> target = currentpos + 1 +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS NEXT(LAST(val)) IS NOT NULL +); + id | val | mf | cnt +----+-----+----+----- + 1 | 10 | 1 | 5 + 2 | 20 | | 0 + 3 | 30 | | 0 + 4 | 10 | | 0 + 5 | 50 | | 0 + 6 | 10 | | 0 +(6 rows) + +-- Compound: inner NULL offset (runtime error) +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(FIRST(val, NULL::int8), 1) IS NULL +); +ERROR: row pattern navigation offset must not be null +-- Compound: inner negative offset (runtime error) +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(LAST(val, -1), 1) IS NULL +); +ERROR: row pattern navigation offset must not be negative +-- Offset argument whose type has no implicit cast to bigint (parse error) +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS val > PREV(val, 1.5) +); +ERROR: offset argument of PREV must be type bigint, not type numeric +LINE 5: DEFINE A AS TRUE, B AS val > PREV(val, 1.5) + ^ +-- Compound + host variable offsets +PREPARE test_compound_offset(int8, int8) AS +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS PREV(FIRST(val, $1), $2) IS NOT NULL +); +EXECUTE test_compound_offset(0, 1); + id | val | mf | cnt +----+-----+----+----- + 1 | 10 | | 0 + 2 | 20 | 2 | 5 + 3 | 30 | | 0 + 4 | 10 | | 0 + 5 | 50 | | 0 + 6 | 10 | | 0 +(6 rows) + +EXECUTE test_compound_offset(1, 1); + id | val | mf | cnt +----+-----+----+----- + 1 | 10 | 1 | 6 + 2 | 20 | | 0 + 3 | 30 | | 0 + 4 | 10 | | 0 + 5 | 50 | | 0 + 6 | 10 | | 0 +(6 rows) + +DEALLOCATE test_compound_offset; +-- Compound + SKIP TO NEXT ROW: overlapping matches with PREV(FIRST()) +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav 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 TRUE, B AS PREV(FIRST(val), 1) > 0 +); + id | val | mf | cnt +----+-----+----+----- + 1 | 10 | | 0 + 2 | 20 | 2 | 5 + 3 | 30 | 3 | 4 + 4 | 10 | 4 | 3 + 5 | 50 | 5 | 2 + 6 | 10 | | 0 +(6 rows) + +-- Compound + multiple partitions +CREATE TEMP TABLE rpr_nav_part (gid int, id int, val int); +INSERT INTO rpr_nav_part VALUES + (1,1,10),(1,2,20),(1,3,30), + (2,1,40),(2,2,50),(2,3,60); +SELECT gid, id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav_part WINDOW w AS ( + PARTITION BY gid ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(FIRST(val), 1) > 0 +); + gid | id | val | mf | cnt +-----+----+-----+----+----- + 1 | 1 | 10 | 1 | 3 + 1 | 2 | 20 | | 0 + 1 | 3 | 30 | | 0 + 2 | 1 | 40 | 1 | 3 + 2 | 2 | 50 | | 0 + 2 | 3 | 60 | | 0 +(6 rows) + +DROP TABLE rpr_nav_part; +-- Reverse nesting: FIRST wrapping PREV is prohibited +SELECT id, val FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B) + DEFINE A AS TRUE, B AS FIRST(PREV(val)) > 0 +); +ERROR: FIRST and LAST cannot contain PREV or NEXT +LINE 5: DEFINE A AS TRUE, B AS FIRST(PREV(val)) > 0 + ^ +HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. +-- Reverse nesting: LAST wrapping NEXT is prohibited +SELECT id, val FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B) + DEFINE A AS TRUE, B AS LAST(NEXT(val)) > 0 +); +ERROR: FIRST and LAST cannot contain PREV or NEXT +LINE 5: DEFINE A AS TRUE, B AS LAST(NEXT(val)) > 0 + ^ +HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. +DROP TABLE rpr_nav; +-- +-- SKIP TO / Backtracking / Frame boundary +-- +-- match everything +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (A+) + DEFINE + A AS TRUE +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | 100 | 130 + company1 | 07-02-2023 | 200 | | + company1 | 07-03-2023 | 150 | | + company1 | 07-04-2023 | 140 | | + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | | + company1 | 07-08-2023 | 130 | | + company1 | 07-09-2023 | 120 | | + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | 50 | 1300 + company2 | 07-02-2023 | 2000 | | + company2 | 07-03-2023 | 1500 | | + company2 | 07-04-2023 | 1400 | | + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | | + company2 | 07-07-2023 | 1100 | | + company2 | 07-08-2023 | 1300 | | + company2 | 07-09-2023 | 1200 | | + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- nth_value beyond reduced frame (no IGNORE NULLS) +-- Tests WinGetSlotInFrame/WinGetFuncArgInFrame out-of-frame with RPR +SELECT company, tdate, price, + nth_value(price, 5) OVER w AS nth_5 +FROM stock +WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + company | tdate | price | nth_5 +----------+------------+-------+------- + company1 | 07-01-2023 | 100 | + company1 | 07-02-2023 | 200 | + company1 | 07-03-2023 | 150 | + company1 | 07-04-2023 | 140 | + company1 | 07-05-2023 | 150 | + company1 | 07-06-2023 | 90 | + company1 | 07-07-2023 | 110 | + company1 | 07-08-2023 | 130 | + company1 | 07-09-2023 | 120 | + company1 | 07-10-2023 | 130 | + company2 | 07-01-2023 | 50 | + company2 | 07-02-2023 | 2000 | + company2 | 07-03-2023 | 1500 | + company2 | 07-04-2023 | 1400 | + company2 | 07-05-2023 | 1500 | + company2 | 07-06-2023 | 60 | + company2 | 07-07-2023 | 1100 | + company2 | 07-08-2023 | 1300 | + company2 | 07-09-2023 | 1200 | + company2 | 07-10-2023 | 1300 | +(20 rows) + +-- backtracking with reclassification of rows +-- using AFTER MATCH SKIP PAST LAST ROW +SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (A+ B+) + DEFINE + A AS price > 100, + B AS price > 100 +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | | + company1 | 07-02-2023 | 200 | 07-02-2023 | 07-05-2023 + company1 | 07-03-2023 | 150 | | + company1 | 07-04-2023 | 140 | | + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | 07-07-2023 | 07-10-2023 + company1 | 07-08-2023 | 130 | | + company1 | 07-09-2023 | 120 | | + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | | + company2 | 07-02-2023 | 2000 | 07-02-2023 | 07-05-2023 + company2 | 07-03-2023 | 1500 | | + company2 | 07-04-2023 | 1400 | | + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | | + company2 | 07-07-2023 | 1100 | 07-07-2023 | 07-10-2023 + company2 | 07-08-2023 | 1300 | | + company2 | 07-09-2023 | 1200 | | + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- backtracking with reclassification of rows +-- using AFTER MATCH SKIP TO NEXT ROW +SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + INITIAL + PATTERN (A+ B+) + DEFINE + A AS price > 100, + B AS price > 100 +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | | + company1 | 07-02-2023 | 200 | 07-02-2023 | 07-05-2023 + company1 | 07-03-2023 | 150 | 07-03-2023 | 07-05-2023 + company1 | 07-04-2023 | 140 | 07-04-2023 | 07-05-2023 + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | 07-07-2023 | 07-10-2023 + company1 | 07-08-2023 | 130 | 07-08-2023 | 07-10-2023 + company1 | 07-09-2023 | 120 | 07-09-2023 | 07-10-2023 + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | | + company2 | 07-02-2023 | 2000 | 07-02-2023 | 07-05-2023 + company2 | 07-03-2023 | 1500 | 07-03-2023 | 07-05-2023 + company2 | 07-04-2023 | 1400 | 07-04-2023 | 07-05-2023 + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | | + company2 | 07-07-2023 | 1100 | 07-07-2023 | 07-10-2023 + company2 | 07-08-2023 | 1300 | 07-08-2023 | 07-10-2023 + company2 | 07-09-2023 | 1200 | 07-09-2023 | 07-10-2023 + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- SKIP TO NEXT ROW with limited frame (Ishii-san's test case) +-- Each row should produce its own match within its frame +WITH data AS ( + SELECT * FROM (VALUES + ('A', 1), ('A', 2), + ('B', 3), ('B', 4) + ) AS t(gid, id) +) +SELECT gid, id, array_agg(id) OVER w +FROM data +WINDOW w AS ( + PARTITION BY gid + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE A AS id < 10 +); + gid | id | array_agg +-----+----+----------- + A | 1 | {1,2} + A | 2 | {2} + B | 3 | {3,4} + B | 4 | {4} +(4 rows) + +-- Limited frame with absorption test +-- Row 0: frame [0,2], can't see B at row 3 -> no match +-- Row 1: frame [1,3], can see A A B -> should match rows 1-3 +WITH frame_absorb_test AS ( + SELECT * FROM (VALUES + (0, 'A'), (1, 'A'), (2, 'A'), (3, 'B') + ) AS t(id, flag) +) +SELECT id, flag, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM frame_absorb_test +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE + A AS flag = 'A', + B AS flag = 'B' +); + id | flag | match_start | match_end +----+------+-------------+----------- + 0 | A | | + 1 | A | 1 | 3 + 2 | A | | + 3 | B | | +(4 rows) + +-- ROWS BETWEEN CURRENT ROW AND offset FOLLOWING +SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w, + count(*) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + company | tdate | price | first_value | last_value | count +----------+------------+-------+-------------+------------+------- + company1 | 07-01-2023 | 100 | 07-01-2023 | 07-03-2023 | 3 + company1 | 07-02-2023 | 200 | | | 0 + company1 | 07-03-2023 | 150 | | | 0 + company1 | 07-04-2023 | 140 | 07-04-2023 | 07-06-2023 | 3 + company1 | 07-05-2023 | 150 | | | 0 + company1 | 07-06-2023 | 90 | | | 0 + company1 | 07-07-2023 | 110 | 07-07-2023 | 07-09-2023 | 3 + company1 | 07-08-2023 | 130 | | | 0 + company1 | 07-09-2023 | 120 | | | 0 + company1 | 07-10-2023 | 130 | | | 0 + company2 | 07-01-2023 | 50 | 07-01-2023 | 07-03-2023 | 3 + company2 | 07-02-2023 | 2000 | | | 0 + company2 | 07-03-2023 | 1500 | | | 0 + company2 | 07-04-2023 | 1400 | 07-04-2023 | 07-06-2023 | 3 + company2 | 07-05-2023 | 1500 | | | 0 + company2 | 07-06-2023 | 60 | | | 0 + company2 | 07-07-2023 | 1100 | 07-07-2023 | 07-09-2023 | 3 + company2 | 07-08-2023 | 1300 | | | 0 + company2 | 07-09-2023 | 1200 | | | 0 + company2 | 07-10-2023 | 1300 | | | 0 +(20 rows) + +-- +-- Aggregates +-- +-- using AFTER MATCH SKIP PAST LAST ROW +SELECT company, tdate, price, + first_value(price) OVER w, + last_value(price) OVER w, + max(price) OVER w, + min(price) OVER w, + sum(price) OVER w, + avg(price) OVER w, + count(price) OVER w +FROM stock +WINDOW w AS ( +PARTITION BY company +ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +AFTER MATCH SKIP PAST LAST ROW +INITIAL +PATTERN (START UP+ DOWN+) +DEFINE +START AS TRUE, +UP AS price > PREV(price), +DOWN AS price < PREV(price) +); + company | tdate | price | first_value | last_value | max | min | sum | avg | count +----------+------------+-------+-------------+------------+------+-----+------+-----------------------+------- + company1 | 07-01-2023 | 100 | 100 | 140 | 200 | 100 | 590 | 147.5000000000000000 | 4 + company1 | 07-02-2023 | 200 | | | | | | | 0 + company1 | 07-03-2023 | 150 | | | | | | | 0 + company1 | 07-04-2023 | 140 | | | | | | | 0 + company1 | 07-05-2023 | 150 | | | | | | | 0 + company1 | 07-06-2023 | 90 | 90 | 120 | 130 | 90 | 450 | 112.5000000000000000 | 4 + company1 | 07-07-2023 | 110 | | | | | | | 0 + company1 | 07-08-2023 | 130 | | | | | | | 0 + company1 | 07-09-2023 | 120 | | | | | | | 0 + company1 | 07-10-2023 | 130 | | | | | | | 0 + company2 | 07-01-2023 | 50 | 50 | 1400 | 2000 | 50 | 4950 | 1237.5000000000000000 | 4 + company2 | 07-02-2023 | 2000 | | | | | | | 0 + company2 | 07-03-2023 | 1500 | | | | | | | 0 + company2 | 07-04-2023 | 1400 | | | | | | | 0 + company2 | 07-05-2023 | 1500 | | | | | | | 0 + company2 | 07-06-2023 | 60 | 60 | 1200 | 1300 | 60 | 3660 | 915.0000000000000000 | 4 + company2 | 07-07-2023 | 1100 | | | | | | | 0 + company2 | 07-08-2023 | 1300 | | | | | | | 0 + company2 | 07-09-2023 | 1200 | | | | | | | 0 + company2 | 07-10-2023 | 1300 | | | | | | | 0 +(20 rows) + +-- using AFTER MATCH SKIP TO NEXT ROW +SELECT company, tdate, price, + first_value(price) OVER w, + last_value(price) OVER w, + max(price) OVER w, + min(price) OVER w, + sum(price) OVER w, + avg(price) OVER w, + count(price) OVER w +FROM stock +WINDOW w AS ( +PARTITION BY company +ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +AFTER MATCH SKIP TO NEXT ROW +INITIAL +PATTERN (START UP+ DOWN+) +DEFINE +START AS TRUE, +UP AS price > PREV(price), +DOWN AS price < PREV(price) +); + company | tdate | price | first_value | last_value | max | min | sum | avg | count +----------+------------+-------+-------------+------------+------+------+------+-----------------------+------- + company1 | 07-01-2023 | 100 | 100 | 140 | 200 | 100 | 590 | 147.5000000000000000 | 4 + company1 | 07-02-2023 | 200 | | | | | | | 0 + company1 | 07-03-2023 | 150 | | | | | | | 0 + company1 | 07-04-2023 | 140 | 140 | 90 | 150 | 90 | 380 | 126.6666666666666667 | 3 + company1 | 07-05-2023 | 150 | | | | | | | 0 + company1 | 07-06-2023 | 90 | 90 | 120 | 130 | 90 | 450 | 112.5000000000000000 | 4 + company1 | 07-07-2023 | 110 | 110 | 120 | 130 | 110 | 360 | 120.0000000000000000 | 3 + company1 | 07-08-2023 | 130 | | | | | | | 0 + company1 | 07-09-2023 | 120 | | | | | | | 0 + company1 | 07-10-2023 | 130 | | | | | | | 0 + company2 | 07-01-2023 | 50 | 50 | 1400 | 2000 | 50 | 4950 | 1237.5000000000000000 | 4 + company2 | 07-02-2023 | 2000 | | | | | | | 0 + company2 | 07-03-2023 | 1500 | | | | | | | 0 + company2 | 07-04-2023 | 1400 | 1400 | 60 | 1500 | 60 | 2960 | 986.6666666666666667 | 3 + company2 | 07-05-2023 | 1500 | | | | | | | 0 + company2 | 07-06-2023 | 60 | 60 | 1200 | 1300 | 60 | 3660 | 915.0000000000000000 | 4 + company2 | 07-07-2023 | 1100 | 1100 | 1200 | 1300 | 1100 | 3600 | 1200.0000000000000000 | 3 + company2 | 07-08-2023 | 1300 | | | | | | | 0 + company2 | 07-09-2023 | 1200 | | | | | | | 0 + company2 | 07-10-2023 | 1300 | | | | | | | 0 +(20 rows) + +-- row_number() within RPR reduced frame +SELECT company, tdate, price, row_number() OVER w, count(*) 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 + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + company | tdate | price | row_number | count +----------+------------+-------+------------+------- + company1 | 07-01-2023 | 100 | 1 | 4 + company1 | 07-02-2023 | 200 | 2 | 0 + company1 | 07-03-2023 | 150 | 3 | 0 + company1 | 07-04-2023 | 140 | 4 | 0 + company1 | 07-05-2023 | 150 | 5 | 0 + company1 | 07-06-2023 | 90 | 6 | 4 + company1 | 07-07-2023 | 110 | 7 | 0 + company1 | 07-08-2023 | 130 | 8 | 0 + company1 | 07-09-2023 | 120 | 9 | 0 + company1 | 07-10-2023 | 130 | 10 | 0 + company2 | 07-01-2023 | 50 | 1 | 4 + company2 | 07-02-2023 | 2000 | 2 | 0 + company2 | 07-03-2023 | 1500 | 3 | 0 + company2 | 07-04-2023 | 1400 | 4 | 0 + company2 | 07-05-2023 | 1500 | 5 | 0 + company2 | 07-06-2023 | 60 | 6 | 4 + company2 | 07-07-2023 | 1100 | 7 | 0 + company2 | 07-08-2023 | 1300 | 8 | 0 + company2 | 07-09-2023 | 1200 | 9 | 0 + company2 | 07-10-2023 | 1300 | 10 | 0 +(20 rows) + +-- +-- SQL Integration: JOIN, CTE, LATERAL +-- +-- JOIN case +CREATE TEMP TABLE t1 (i int, v1 int); +CREATE TEMP TABLE t2 (j int, v2 int); +INSERT INTO t1 VALUES(1,10); +INSERT INTO t1 VALUES(1,11); +INSERT INTO t1 VALUES(1,12); +INSERT INTO t2 VALUES(2,10); +INSERT INTO t2 VALUES(2,11); +INSERT INTO t2 VALUES(2,12); +SELECT * FROM t1, t2 WHERE t1.v1 <= 11 AND t2.v2 <= 11; + i | v1 | j | v2 +---+----+---+---- + 1 | 10 | 2 | 10 + 1 | 10 | 2 | 11 + 1 | 11 | 2 | 10 + 1 | 11 | 2 | 11 +(4 rows) + +SELECT *, count(*) OVER w FROM t1, t2 +WINDOW w AS ( + PARTITION BY t1.i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE + A AS v1 <= 11 AND v2 <= 11 +); + i | v1 | j | v2 | count +---+----+---+----+------- + 1 | 10 | 2 | 10 | 1 + 1 | 10 | 2 | 11 | 1 + 1 | 10 | 2 | 12 | 0 + 1 | 11 | 2 | 10 | 1 + 1 | 11 | 2 | 11 | 1 + 1 | 11 | 2 | 12 | 0 + 1 | 12 | 2 | 10 | 0 + 1 | 12 | 2 | 11 | 0 + 1 | 12 | 2 | 12 | 0 +(9 rows) + +-- WITH case +WITH wstock AS ( + SELECT * FROM stock WHERE tdate < '2023-07-08' +) +SELECT tdate, price, +first_value(tdate) OVER w, +count(*) OVER w + FROM wstock + WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + tdate | price | first_value | count +------------+-------+-------------+------- + 07-01-2023 | 100 | 07-01-2023 | 4 + 07-02-2023 | 200 | | 0 + 07-03-2023 | 150 | | 0 + 07-04-2023 | 140 | | 0 + 07-05-2023 | 150 | | 0 + 07-06-2023 | 90 | | 0 + 07-07-2023 | 110 | | 0 + 07-01-2023 | 50 | 07-01-2023 | 4 + 07-02-2023 | 2000 | | 0 + 07-03-2023 | 1500 | | 0 + 07-04-2023 | 1400 | | 0 + 07-05-2023 | 1500 | | 0 + 07-06-2023 | 60 | | 0 + 07-07-2023 | 1100 | | 0 +(14 rows) + +-- ReScan test: LATERAL join forces WindowAgg rescan with RPR +-- Tests ExecReScanWindowAgg clearing nav_slot +SELECT g.x, sub.* +FROM generate_series(1, 2) g(x), +LATERAL ( + SELECT id, price, count(*) OVER w AS c + FROM (VALUES (1, 100), (2, 200), (3, 150)) AS t(id, price) + WHERE id <= g.x + 1 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (START UP+) + DEFINE + START AS TRUE, + UP AS price > PREV(price) + ) +) sub +ORDER BY g.x, sub.id; + x | id | price | c +---+----+-------+--- + 1 | 1 | 100 | 2 + 1 | 2 | 200 | 0 + 2 | 1 | 100 | 2 + 2 | 2 | 200 | 0 + 2 | 3 | 150 | 0 +(5 rows) + +-- PREV has multiple column reference +CREATE TEMP TABLE rpr1 (id INTEGER, i SERIAL, j INTEGER); +INSERT INTO rpr1(id, j) SELECT 1, g*2 FROM generate_series(1, 10) AS g; +SELECT id, i, j, count(*) OVER w + FROM rpr1 + WINDOW w AS ( + PARTITION BY id + ORDER BY i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (START COND+) + DEFINE + START AS TRUE, + COND AS PREV(i + j + 1) < 10 +); + id | i | j | count +----+----+----+------- + 1 | 1 | 2 | 3 + 1 | 2 | 4 | 0 + 1 | 3 | 6 | 0 + 1 | 4 | 8 | 0 + 1 | 5 | 10 | 0 + 1 | 6 | 12 | 0 + 1 | 7 | 14 | 0 + 1 | 8 | 16 | 0 + 1 | 9 | 18 | 0 + 1 | 10 | 20 | 0 +(10 rows) + +-- +-- Large-scale / scalability tests +-- +-- 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) + +-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE. +-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload) +-- at scale. V-shape: price rises then falls, repeated across partition. +SET jit = on; +SET jit_above_cost = 0; +WITH data AS ( + SELECT i, abs(50000 - i) AS price + FROM generate_series(1, 100000) i +), +result AS ( + SELECT i, price, + count(*) OVER w AS match_len, + first_value(price) OVER w AS match_first + FROM data + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (DOWN+ UP+) + DEFINE + DOWN AS price < PREV(price), + UP AS price > PREV(price) + ) +) +SELECT count(*) AS matched_rows, max(match_len) AS longest_match +FROM result WHERE match_len > 0; + matched_rows | longest_match +--------------+--------------- + 1 | 99999 +(1 row) + +RESET jit_above_cost; +RESET jit; +-- JIT compound navigation test +SET jit = on; +SET jit_above_cost = 0; +SELECT count(*) AS matched_rows +FROM ( + SELECT v, count(*) OVER w AS match_len + FROM generate_series(1, 1000) AS t(v) + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(v), 1) > 0 + ) +) sub WHERE match_len > 0; + matched_rows +-------------- + 1 +(1 row) + +RESET jit_above_cost; +RESET jit; +-- +-- 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 to a NULL in the middle, 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 to a NULL in the middle, it reaches the end of reduced frame and +-- returns NULL +WITH data AS ( + SELECT * FROM (VALUES + (10, 1), (11, NULL), (12, 3), (13, 4) + ) AS t(gid, id)) + SELECT gid, id, nth_value(id, 3) IGNORE NULLS OVER w AS thrid_val, + array_agg(id) OVER w + FROM data + WINDOW w AS ( + ORDER BY gid + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS gid < 13 + ); + gid | id | thrid_val | array_agg +-----+----+-----------+------------ + 10 | 1 | | {1,NULL,3} + 11 | | | + 12 | 3 | | + 13 | 4 | | +(4 rows) + +-- nth_value beyond reduced frame with IGNORE NULLS +-- Tests ignorenulls_getfuncarginframe early out-of-frame check +SELECT company, tdate, price, + nth_value(price, 5) IGNORE NULLS OVER w AS nth_5_in +FROM stock +WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + company | tdate | price | nth_5_in +----------+------------+-------+---------- + company1 | 07-01-2023 | 100 | + company1 | 07-02-2023 | 200 | + company1 | 07-03-2023 | 150 | + company1 | 07-04-2023 | 140 | + company1 | 07-05-2023 | 150 | + company1 | 07-06-2023 | 90 | + company1 | 07-07-2023 | 110 | + company1 | 07-08-2023 | 130 | + company1 | 07-09-2023 | 120 | + company1 | 07-10-2023 | 130 | + company2 | 07-01-2023 | 50 | + company2 | 07-02-2023 | 2000 | + company2 | 07-03-2023 | 1500 | + company2 | 07-04-2023 | 1400 | + company2 | 07-05-2023 | 1500 | + company2 | 07-06-2023 | 60 | + company2 | 07-07-2023 | 1100 | + company2 | 07-08-2023 | 1300 | + company2 | 07-09-2023 | 1200 | + company2 | 07-10-2023 | 1300 | +(20 rows) + +-- IGNORE NULLS + first_value where first value in reduced frame is NULL +WITH data AS ( + SELECT * FROM (VALUES + (1, NULL), (2, NULL), (3, 30), (4, 40) + ) AS t(id, val)) +SELECT id, val, + first_value(val) IGNORE NULLS OVER w AS fv_ignull, + count(*) OVER w +FROM data +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS TRUE +); + id | val | fv_ignull | count +----+-----+-----------+------- + 1 | | 30 | 4 + 2 | | | 0 + 3 | 30 | | 0 + 4 | 40 | | 0 +(4 rows) + +-- IGNORE NULLS + all values NULL in reduced frame +WITH data AS ( + SELECT * FROM (VALUES + (1, NULL), (2, NULL), (3, NULL) + ) AS t(id, val)) +SELECT id, val, + first_value(val) IGNORE NULLS OVER w AS fv_ignull, + last_value(val) IGNORE NULLS OVER w AS lv_ignull, + count(*) OVER w +FROM data +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS TRUE +); + id | val | fv_ignull | lv_ignull | count +----+-----+-----------+-----------+------- + 1 | | | | 3 + 2 | | | | 0 + 3 | | | | 0 +(3 rows) + +-- +-- last_value IGNORE NULLS with reduced frame containing all NULLs +-- Exercises ignorenulls_getfuncarginframe SEEK_TAIL out-of-frame path +-- when notnull_relpos >= num_reduced_frame. +-- +CREATE TEMP TABLE rpr_nullval (id INT, val INT); +INSERT INTO rpr_nullval VALUES (1, 10), (2, NULL), (3, NULL), (4, 20); +SELECT id, val, + last_value(val) IGNORE NULLS OVER w AS lv_ignull, + count(*) OVER w AS cnt +FROM rpr_nullval +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS val IS NOT NULL, + B AS val IS NULL +); + id | val | lv_ignull | cnt +----+-----+-----------+----- + 1 | 10 | 10 | 3 + 2 | | | 0 + 3 | | | 0 + 4 | 20 | | 0 +(4 rows) + +-- +-- nth_value with a NULL offset +-- +CREATE TABLE rpr_dormant (id int, price int); +INSERT INTO rpr_dormant SELECT g, g*10 FROM generate_series(1,60) g; +-- reference: first_value(id) is the start row of the match beginning at the +-- current row, count(*) is that match's length over the reduced frame +SELECT * FROM ( + SELECT id, first_value(id) OVER w AS match_start, count(*) OVER w AS match_len + FROM rpr_dormant + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50)) +) s WHERE id > 50 ORDER BY id; + id | match_start | match_len +----+-------------+----------- + 51 | 51 | 10 + 52 | | 0 + 53 | | 0 + 54 | | 0 + 55 | | 0 + 56 | | 0 + 57 | | 0 + 58 | | 0 + 59 | | 0 + 60 | | 0 +(10 rows) + +-- nth_value with a NULL offset; FIRST navigation in DEFINE, SKIP PAST LAST ROW +SELECT * FROM ( + SELECT id, nv FROM ( + SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv + FROM rpr_dormant + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50)) + ) s +) t WHERE id > 50 ORDER BY id; + id | nv +----+----- + 51 | 510 + 52 | + 53 | + 54 | + 55 | + 56 | + 57 | + 58 | + 59 | + 60 | +(10 rows) + +-- the same window with first_value and count alongside nth_value +SELECT * FROM ( + SELECT id, nv, fv, cnt FROM ( + SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv, + first_value(id) OVER w AS fv, count(*) OVER w AS cnt + FROM rpr_dormant + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50)) + ) s +) t WHERE id > 50 ORDER BY id; + id | nv | fv | cnt +----+-----+----+----- + 51 | 510 | 51 | 10 + 52 | | | 0 + 53 | | | 0 + 54 | | | 0 + 55 | | | 0 + 56 | | | 0 + 57 | | | 0 + 58 | | | 0 + 59 | | | 0 + 60 | | | 0 +(10 rows) + +-- the same nth_value with a non-navigation DEFINE +SELECT * FROM ( + SELECT id, nv FROM ( + SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv + FROM rpr_dormant + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) DEFINE A AS price > 0) + ) s +) t WHERE id > 50 ORDER BY id; + id | nv +----+---- + 51 | + 52 | + 53 | + 54 | + 55 | + 56 | + 57 | + 58 | + 59 | + 60 | +(10 rows) + +-- the same nth_value with a PREV-only DEFINE (no FIRST navigation) +SELECT * FROM ( + SELECT id, nv FROM ( + SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv + FROM rpr_dormant + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) DEFINE A AS price > PREV(price, 50)) + ) s +) t WHERE id > 50 ORDER BY id; + id | nv +----+----- + 51 | 510 + 52 | + 53 | + 54 | + 55 | + 56 | + 57 | + 58 | + 59 | + 60 | +(10 rows) + +-- nth_value with a NULL offset band in the middle of the partition +SELECT * FROM ( + SELECT id, nv FROM ( + SELECT id, nth_value(price, CASE WHEN id BETWEEN 20 AND 40 THEN NULL ELSE 1 END) OVER w AS nv + FROM rpr_dormant + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50)) + ) s +) t WHERE id BETWEEN 38 AND 46 ORDER BY id; + id | nv +----+---- + 38 | + 39 | + 40 | + 41 | + 42 | + 43 | + 44 | + 45 | + 46 | +(9 rows) + +DROP TABLE rpr_dormant; +-- +-- NULL handling +-- +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) + +-- Consecutive NULLs: PREV navigates through NULL values +CREATE TEMP TABLE rpr_consec_null (id INT, val INT); +INSERT INTO rpr_consec_null VALUES + (1, 100), (2, NULL), (3, NULL), (4, NULL), (5, 200), (6, 300); +-- PREV(val) IS NULL succeeds for both null_slot (first row) and actual NULL +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_consec_null +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+ C) + DEFINE + A AS val IS NULL, + B AS val IS NULL AND PREV(val) IS NULL, + C AS val IS NOT NULL +); + id | val | cnt +----+-----+----- + 1 | 100 | 0 + 2 | | 4 + 3 | | 0 + 4 | | 0 + 5 | 200 | 0 + 6 | 300 | 0 +(6 rows) + +-- NEXT(val) through consecutive NULLs +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_consec_null +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+ C) + DEFINE + A AS val IS NOT NULL, + B AS val IS NULL AND NEXT(val) IS NULL, + C AS val IS NULL AND NEXT(val) IS NOT NULL +); + id | val | cnt +----+-----+----- + 1 | 100 | 4 + 2 | | 0 + 3 | | 0 + 4 | | 0 + 5 | 200 | 0 + 6 | 300 | 0 +(6 rows) + +DROP TABLE rpr_consec_null; +-- ============================================================ +-- Stock Scenario Tests (1632 rows, partitioned regions) +-- ============================================================ +-- Consecutive rising days: find streaks of 7+ days +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (UP{7,}) + DEFINE UP AS price > PREV(price) + ) +) t WHERE days > 0 ORDER BY start_rn; + start_rn | end_rn | days +----------+--------+------ + 29 | 35 | 7 + 38 | 44 | 7 + 96 | 102 | 7 + 118 | 125 | 8 + 308 | 317 | 10 + 328 | 334 | 7 + 475 | 481 | 7 + 491 | 497 | 7 + 509 | 517 | 9 + 536 | 542 | 7 + 586 | 592 | 7 + 643 | 650 | 8 + 740 | 746 | 7 + 753 | 760 | 8 + 904 | 910 | 7 + 956 | 965 | 10 + 985 | 991 | 7 + 1095 | 1101 | 7 + 1104 | 1110 | 7 + 1181 | 1187 | 7 + 1221 | 1228 | 8 + 1262 | 1268 | 7 + 1272 | 1278 | 7 + 1373 | 1380 | 8 + 1434 | 1440 | 7 + 1485 | 1491 | 7 + 1553 | 1559 | 7 + 1576 | 1582 | 7 + 1624 | 1631 | 8 +(29 rows) + +-- V-shape recovery: 4+ days decline followed by 4+ days rise +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (DECLINE{4,} RISE{4,}) + DEFINE + DECLINE AS price < PREV(price), + RISE AS price > PREV(price) + ) +) t WHERE days > 0 ORDER BY start_rn; + start_rn | end_rn | start_price | end_price | days +----------+--------+-------------+-----------+------ + 84 | 93 | 301.000 | 309.750 | 10 + 114 | 125 | 394.500 | 418.750 | 12 + 173 | 183 | 260.000 | 282.000 | 11 + 204 | 214 | 179.000 | 171.500 | 11 + 262 | 271 | 277.625 | 275.500 | 10 + 337 | 344 | 308.250 | 308.125 | 8 + 436 | 444 | 56.630 | 60.250 | 9 + 567 | 575 | 126.500 | 127.500 | 9 + 598 | 607 | 112.250 | 116.500 | 10 + 653 | 660 | 129.375 | 128.125 | 8 + 663 | 671 | 125.630 | 130.250 | 9 + 685 | 693 | 120.130 | 124.000 | 9 + 710 | 719 | 141.125 | 144.130 | 10 + 833 | 841 | 106.500 | 111.125 | 9 + 853 | 862 | 105.375 | 107.750 | 10 + 930 | 937 | 92.380 | 96.375 | 8 + 1188 | 1197 | 105.250 | 108.380 | 10 + 1198 | 1206 | 100.125 | 100.562 | 9 + 1250 | 1259 | 131.437 | 130.875 | 10 + 1285 | 1295 | 176.940 | 182.190 | 11 + 1298 | 1307 | 181.000 | 178.562 | 10 + 1310 | 1322 | 186.310 | 212.000 | 13 + 1405 | 1412 | 117.800 | 117.500 | 8 + 1467 | 1474 | 70.400 | 71.830 | 8 + 1494 | 1502 | 79.760 | 86.000 | 9 + 1600 | 1618 | 90.440 | 77.050 | 19 +(26 rows) + +-- W-bottom: decline, bounce, re-decline, recovery +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (DECLINE{3,} BOUNCE{3,} DIP{3,} RECOVER{3,}) + DEFINE + DECLINE AS price < PREV(price), + BOUNCE AS price > PREV(price), + DIP AS price < PREV(price), + RECOVER AS price > PREV(price) + ) +) t WHERE days > 0 ORDER BY start_rn; + start_rn | end_rn | start_price | end_price | days +----------+--------+-------------+-----------+------ + 136 | 153 | 444.000 | 434.250 | 18 + 456 | 469 | 64.500 | 65.125 | 14 + 520 | 534 | 115.250 | 115.750 | 15 + 610 | 623 | 107.125 | 109.000 | 14 + 791 | 802 | 113.500 | 118.250 | 12 + 942 | 953 | 91.250 | 89.875 | 12 + 1188 | 1206 | 105.250 | 100.562 | 19 + 1560 | 1574 | 87.420 | 90.000 | 15 +(8 rows) + +-- Volume surge streak: 6+ consecutive days of increasing volume +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(volume) OVER w AS start_vol, + last_value(volume) OVER w AS end_vol, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (INIT SURGE{5,}) + DEFINE + SURGE AS volume > PREV(volume) + ) +) t WHERE days > 0 ORDER BY start_rn; + start_rn | end_rn | start_vol | end_vol | days +----------+--------+-----------+----------+------ + 186 | 191 | 25100 | 35300 | 6 + 291 | 296 | 52200 | 188300 | 6 + 408 | 413 | 163300 | 610300 | 6 + 439 | 444 | 438400 | 1089200 | 6 + 500 | 506 | 373700 | 1114200 | 7 + 551 | 558 | 691100 | 2097500 | 8 + 635 | 640 | 418300 | 1388100 | 6 + 783 | 788 | 769000 | 1564900 | 6 + 824 | 830 | 682200 | 2418700 | 7 + 968 | 974 | 993100 | 2341200 | 7 + 1072 | 1077 | 1257200 | 2327700 | 6 + 1078 | 1084 | 1748300 | 5514300 | 7 + 1093 | 1098 | 2664400 | 13145900 | 6 + 1334 | 1339 | 3220900 | 10552600 | 6 + 1524 | 1530 | 3562400 | 7012100 | 7 + 1533 | 1538 | 3548500 | 8560800 | 6 + 1575 | 1580 | 4226500 | 6952100 | 6 +(17 rows) + +-- Volatility squeeze: consecutive narrowing of daily price range +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(high - low) OVER w AS start_range, + last_value(high - low) OVER w AS end_range, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (INIT NARROW{5,}) + DEFINE + NARROW AS (high - low) < PREV(high) - PREV(low) + ) +) t WHERE days > 0 ORDER BY start_rn; + start_rn | end_rn | start_range | end_range | days +----------+--------+-------------+-----------+------ + 128 | 133 | 11.000 | 3.000 | 6 + 170 | 175 | 10.250 | 2.750 | 6 + 194 | 201 | 10.000 | 1.625 | 8 + 283 | 288 | 7.000 | 2.375 | 6 + 320 | 325 | 4.750 | 2.500 | 6 + 578 | 583 | 2.750 | 1.125 | 6 + 725 | 731 | 3.370 | 1.125 | 7 + 775 | 780 | 4.500 | 0.875 | 6 + 913 | 918 | 2.870 | 1.250 | 6 + 1130 | 1135 | 3.000 | 1.125 | 6 + 1348 | 1353 | 6.620 | 1.562 | 6 +(11 rows) + +-- Gap up: open significantly higher than previous close (5%+) +SELECT * FROM ( + SELECT first_value(rn) OVER w AS gap_rn, + first_value(price) OVER w AS prev_close, + last_value(open) OVER w AS gap_open, + count(*) OVER w AS cnt + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (PREV_DAY GAP_UP) + DEFINE + GAP_UP AS open > PREV(price) * 1.05 + ) +) t WHERE cnt > 0 ORDER BY gap_rn; + gap_rn | prev_close | gap_open | cnt +--------+------------+----------+----- + 986 | 48.875 | 52.875 | 2 + 994 | 42.375 | 44.880 | 2 + 1029 | 52.250 | 55.000 | 2 + 1039 | 55.880 | 59.000 | 2 + 1177 | 142.380 | 150.500 | 2 + 1238 | 122.000 | 128.250 | 2 + 1318 | 171.875 | 197.250 | 2 + 1383 | 103.500 | 112.000 | 2 + 1392 | 96.687 | 104.375 | 2 + 1401 | 106.500 | 112.000 | 2 + 1464 | 66.400 | 70.370 | 2 + 1477 | 57.580 | 62.000 | 2 + 1479 | 63.420 | 67.750 | 2 + 1481 | 64.900 | 72.800 | 2 + 1517 | 90.310 | 95.070 | 2 +(15 rows) + +-- Price-volume divergence: price rising while volume declining (bearish signal) +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (INIT DIVERGE{3,}) + DEFINE + DIVERGE AS price > PREV(price) AND volume < PREV(volume) + ) +) t WHERE days > 0 ORDER BY start_rn; + start_rn | end_rn | start_price | end_price | days +----------+--------+-------------+-----------+------ + 10 | 13 | 338.500 | 349.750 | 4 + 47 | 50 | 352.000 | 358.000 | 4 + 53 | 56 | 314.000 | 319.000 | 4 + 63 | 66 | 294.500 | 301.750 | 4 + 99 | 102 | 401.000 | 404.000 | 4 + 177 | 180 | 248.500 | 261.750 | 4 + 209 | 212 | 152.000 | 166.500 | 4 + 237 | 240 | 207.500 | 215.500 | 4 + 274 | 277 | 259.500 | 264.500 | 4 + 347 | 350 | 306.750 | 312.000 | 4 + 531 | 534 | 111.875 | 115.750 | 4 + 545 | 548 | 118.000 | 121.000 | 4 + 561 | 564 | 131.130 | 134.250 | 4 + 586 | 589 | 110.250 | 112.000 | 4 + 645 | 648 | 129.630 | 133.250 | 4 + 656 | 660 | 124.500 | 128.125 | 5 + 679 | 682 | 128.000 | 129.750 | 4 + 734 | 737 | 121.380 | 123.750 | 4 + 763 | 766 | 150.750 | 156.500 | 4 + 844 | 847 | 105.500 | 107.880 | 4 + 921 | 924 | 97.250 | 101.250 | 4 + 956 | 959 | 84.875 | 87.500 | 4 + 960 | 963 | 88.250 | 89.500 | 4 + 987 | 990 | 49.000 | 51.500 | 4 + 1023 | 1026 | 53.250 | 54.625 | 4 + 1033 | 1036 | 57.125 | 58.380 | 4 + 1060 | 1063 | 89.000 | 93.625 | 4 + 1066 | 1069 | 89.130 | 93.000 | 4 + 1087 | 1090 | 89.375 | 91.750 | 4 + 1165 | 1168 | 137.625 | 146.875 | 4 + 1202 | 1205 | 96.500 | 98.750 | 4 + 1209 | 1212 | 96.125 | 100.250 | 4 + 1220 | 1223 | 106.130 | 112.000 | 4 + 1231 | 1234 | 117.062 | 119.625 | 4 + 1244 | 1247 | 122.500 | 130.000 | 4 + 1304 | 1307 | 169.500 | 178.562 | 4 + 1342 | 1345 | 122.000 | 124.000 | 4 + 1417 | 1420 | 101.960 | 108.530 | 4 + 1433 | 1436 | 90.000 | 93.770 | 4 + 1437 | 1440 | 96.950 | 98.500 | 4 + 1443 | 1446 | 120.250 | 122.200 | 4 + 1449 | 1452 | 74.650 | 77.140 | 4 + 1455 | 1458 | 67.600 | 73.500 | 4 + 1497 | 1500 | 76.250 | 81.650 | 4 + 1505 | 1508 | 75.860 | 79.510 | 4 + 1511 | 1514 | 79.750 | 81.020 | 4 + 1518 | 1521 | 94.020 | 97.700 | 4 + 1541 | 1544 | 83.650 | 85.250 | 4 + 1547 | 1550 | 82.210 | 84.040 | 4 + 1585 | 1588 | 92.700 | 93.570 | 4 + 1594 | 1597 | 91.600 | 92.410 | 4 + 1613 | 1616 | 72.010 | 74.610 | 4 +(52 rows) + +-- Consolidation then breakout: sideways movement followed by sharp rise +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (FLAT{5,} BREAKOUT) + DEFINE + FLAT AS price BETWEEN PREV(price) * 0.98 AND PREV(price) * 1.02, + BREAKOUT AS price > PREV(price) * 1.05 + ) +) t WHERE days > 0 ORDER BY start_rn; + start_rn | end_rn | start_price | end_price | days +----------+--------+-------------+-----------+------ + 69 | 81 | 296.750 | 314.000 | 13 + 214 | 225 | 171.500 | 164.125 | 12 + 371 | 395 | 71.630 | 71.250 | 25 + 416 | 424 | 54.250 | 53.875 | 9 + 484 | 494 | 75.500 | 79.000 | 11 + 865 | 892 | 112.500 | 115.750 | 28 + 1007 | 1020 | 59.500 | 58.630 | 14 + 1113 | 1118 | 110.750 | 117.625 | 6 + 1146 | 1152 | 133.750 | 145.000 | 7 + 1171 | 1178 | 137.880 | 153.625 | 8 + 1350 | 1357 | 106.125 | 112.000 | 8 + 1360 | 1370 | 109.125 | 115.625 | 11 +(12 rows) + +-- Dead cat bounce: decline followed by weak recovery (<1% per day) +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (DECLINE{4,} BOUNCE{3,}) + DEFINE + DECLINE AS price < PREV(price), + BOUNCE AS price > PREV(price) AND price < PREV(price) * 1.01 + ) +) t WHERE days > 0 ORDER BY start_rn; + start_rn | end_rn | start_price | end_price | days +----------+--------+-------------+-----------+------ + 59 | 66 | 315.000 | 301.750 | 8 + 262 | 271 | 277.625 | 275.500 | 10 + 280 | 287 | 272.000 | 261.250 | 8 + 361 | 368 | 75.375 | 74.130 | 8 + 427 | 433 | 65.130 | 65.500 | 7 + 447 | 453 | 55.500 | 54.625 | 7 + 653 | 659 | 129.375 | 126.625 | 7 + 674 | 682 | 132.375 | 129.750 | 9 + 833 | 839 | 106.500 | 107.125 | 7 + 1423 | 1430 | 108.180 | 106.200 | 8 + 1591 | 1597 | 92.920 | 92.410 | 7 +(11 rows) + +-- Uptrend: 7+ consecutive days of higher highs AND higher lows +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (UPTREND{7,}) + DEFINE + UPTREND AS high > PREV(high) AND low > PREV(low) + ) +) t WHERE days > 0 ORDER BY start_rn; + start_rn | end_rn | start_price | end_price | days +----------+--------+-------------+-----------+------ + 156 | 162 | 317.000 | 329.500 | 7 + 299 | 305 | 266.000 | 273.500 | 7 + 696 | 702 | 144.630 | 152.500 | 7 + 741 | 747 | 155.250 | 166.750 | 7 + 895 | 901 | 119.250 | 126.750 | 7 + 1121 | 1127 | 103.630 | 108.875 | 7 + 1211 | 1217 | 99.130 | 102.875 | 7 + 1271 | 1278 | 164.375 | 189.250 | 8 + 1621 | 1628 | 84.000 | 89.820 | 8 +(9 rows) + +-- Panic and snap-back: 3%+ daily drops followed by 2%+ rebound +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (PANIC{2,} SNAP) + DEFINE + PANIC AS price < PREV(price) * 0.97, + SNAP AS price > PREV(price) * 1.02 + ) +) t WHERE days > 0 ORDER BY start_rn; + start_rn | end_rn | start_price | end_price | days +----------+--------+-------------+-----------+------ + 24 | 26 | 230.625 | 229.875 | 3 + 165 | 167 | 272.000 | 270.750 | 3 + 169 | 171 | 260.000 | 259.500 | 3 + 769 | 772 | 140.125 | 115.000 | 4 + 977 | 979 | 56.130 | 53.000 | 3 + 980 | 982 | 51.375 | 51.750 | 3 + 1387 | 1389 | 86.000 | 89.000 | 3 + 1396 | 1398 | 99.290 | 98.390 | 3 +(8 rows) + +-- Volume climax reversal: uptrend, volume spike (1.5x), then decline +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (RALLY{3,} CLIMAX SELLOFF{2,}) + DEFINE + RALLY AS price > PREV(price), + CLIMAX AS volume > PREV(volume) * 1.5, + SELLOFF AS price < PREV(price) + ) +) t WHERE days > 0 ORDER BY start_rn; + start_rn | end_rn | start_price | end_price | days +----------+--------+-------------+-----------+------ + 2 | 7 | 368.250 | 367.750 | 6 + 16 | 21 | 349.250 | 344.500 | 6 + 105 | 111 | 388.500 | 394.500 | 7 + 228 | 234 | 164.250 | 163.250 | 7 + 243 | 248 | 223.250 | 228.500 | 6 + 251 | 259 | 251.000 | 253.500 | 9 + 352 | 358 | 310.500 | 309.000 | 7 + 398 | 405 | 65.380 | 65.000 | 8 + 466 | 472 | 64.250 | 63.875 | 7 + 586 | 595 | 110.250 | 112.250 | 10 + 626 | 632 | 125.750 | 123.250 | 7 + 700 | 707 | 150.250 | 152.750 | 8 + 714 | 722 | 136.125 | 137.000 | 9 + 740 | 750 | 154.250 | 163.000 | 11 + 805 | 811 | 116.875 | 116.000 | 7 + 814 | 821 | 99.500 | 97.130 | 8 + 850 | 857 | 107.880 | 99.250 | 8 + 922 | 927 | 99.750 | 99.875 | 6 + 934 | 939 | 92.375 | 92.880 | 6 + 998 | 1004 | 52.000 | 53.130 | 7 + 1043 | 1049 | 73.125 | 73.000 | 7 + 1052 | 1057 | 72.130 | 74.250 | 6 + 1138 | 1143 | 127.750 | 125.625 | 6 + 1155 | 1162 | 158.000 | 155.625 | 8 + 1181 | 1191 | 99.130 | 100.062 | 11 + 1192 | 1202 | 100.130 | 96.500 | 11 + 1236 | 1241 | 118.380 | 123.880 | 6 + 1272 | 1282 | 166.062 | 183.000 | 11 + 1325 | 1331 | 217.250 | 237.500 | 7 + 1409 | 1414 | 112.890 | 116.100 | 6 + 1456 | 1461 | 68.580 | 68.760 | 6 +(31 rows) + diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out new file mode 100644 index 00000000000..85a1ad8727d --- /dev/null +++ b/src/test/regress/expected/rpr_base.out @@ -0,0 +1,7940 @@ +-- ============================================================ +-- RPR Base Tests +-- Tests for Row Pattern Recognition (ISO/IEC 19075-5) +-- ============================================================ +-- +-- Parser Layer: +-- Keyword Usage Tests +-- DEFINE Clause Tests +-- FRAME Options Tests +-- PARTITION BY + FRAME Tests +-- PATTERN Syntax Tests +-- Quantifiers Tests +-- Navigation Functions Tests +-- SKIP TO / INITIAL Tests +-- Serialization/Deserialization Tests +-- Glued Quantifier / Alternation Tests +-- Error Cases Tests +-- Window Deduplication Tests +-- +-- Planner Layer: +-- Pattern Optimization Tests +-- Absorption Flag Display Tests +-- Absorption Analysis Tests +-- Edge Case Tests +-- Optimization Fallback Tests +-- Planner Integration Tests +-- Subquery and CTE Tests +-- JOIN Tests +-- Complex Expression Tests +-- Set Operations Tests +-- Sorting and Grouping Tests +-- SQL Function Inlining Tests +-- Stress Tests +-- Error Limit Tests +-- +-- Contributed Tests: +-- Basic Pattern Matching +-- Pathological Patterns +-- ============================================================ +SET client_min_messages = WARNING; +-- ============================================================ +-- Keyword Usage Tests +-- ============================================================ +-- RPR keywords as column names +-- Keywords: define, initial, past, pattern, seek +CREATE TABLE rpr_keywords ( + id INT, + define INT, -- DEFINE keyword + initial INT, -- INITIAL keyword + past INT, -- PAST keyword + pattern INT, -- PATTERN keyword + seek INT, -- SEEK keyword +-- ERROR: SEEK is not supported + skip INT -- SKIP keyword (pre-existing) +); +INSERT INTO rpr_keywords VALUES (1, 10, 20, 30, 40, 50, 60); +SELECT id, define, initial, past, pattern, seek, skip +FROM rpr_keywords +ORDER BY id; + id | define | initial | past | pattern | seek | skip +----+--------+---------+------+---------+------+------ + 1 | 10 | 20 | 30 | 40 | 50 | 60 +(1 row) + +DROP TABLE rpr_keywords; +-- ============================================================ +-- DEFINE Clause Tests +-- ============================================================ +-- Simple column references +CREATE TABLE stock_price ( + dt DATE, + symbol TEXT, + price NUMERIC, + volume INT +); +INSERT INTO stock_price VALUES + ('2024-01-01', 'AAPL', 150, 1000), + ('2024-01-02', 'AAPL', 155, 1200), + ('2024-01-03', 'AAPL', 152, 900), + ('2024-01-04', 'AAPL', 160, 1500), + ('2024-01-05', 'AAPL', 158, 1100); +-- Simple column reference +SELECT dt, price, COUNT(*) OVER w as cnt +FROM stock_price +WINDOW w AS ( + PARTITION BY symbol + ORDER BY dt + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (UP+) + DEFINE UP AS price > 150 +) +ORDER BY dt; + dt | price | cnt +------------+-------+----- + 01-01-2024 | 150 | 0 + 01-02-2024 | 155 | 4 + 01-03-2024 | 152 | 0 + 01-04-2024 | 160 | 0 + 01-05-2024 | 158 | 0 +(5 rows) + +-- Multiple column references +SELECT dt, price, volume, COUNT(*) OVER w as cnt +FROM stock_price +WINDOW w AS ( + PARTITION BY symbol + ORDER BY dt + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (GOOD+) + DEFINE GOOD AS price > 150 AND volume > 1000 +) +ORDER BY dt; + dt | price | volume | cnt +------------+-------+--------+----- + 01-01-2024 | 150 | 1000 | 0 + 01-02-2024 | 155 | 1200 | 1 + 01-03-2024 | 152 | 900 | 0 + 01-04-2024 | 160 | 1500 | 2 + 01-05-2024 | 158 | 1100 | 0 +(5 rows) + +-- Expression in DEFINE +SELECT dt, price, COUNT(*) OVER w as cnt +FROM stock_price +WINDOW w AS ( + PARTITION BY symbol + ORDER BY dt + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (HIGH+) + DEFINE HIGH AS price * 1.1 > 165 +) +ORDER BY dt; + dt | price | cnt +------------+-------+----- + 01-01-2024 | 150 | 0 + 01-02-2024 | 155 | 4 + 01-03-2024 | 152 | 0 + 01-04-2024 | 160 | 0 + 01-05-2024 | 158 | 0 +(5 rows) + +-- Arithmetic and functions +SELECT dt, price, volume, COUNT(*) OVER w as cnt +FROM stock_price +WINDOW w AS ( + PARTITION BY symbol + ORDER BY dt + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (CALC+) + DEFINE CALC AS (price + volume / 100) > 160 +) +ORDER BY dt; + dt | price | volume | cnt +------------+-------+--------+----- + 01-01-2024 | 150 | 1000 | 0 + 01-02-2024 | 155 | 1200 | 4 + 01-03-2024 | 152 | 900 | 0 + 01-04-2024 | 160 | 1500 | 0 + 01-05-2024 | 158 | 1100 | 0 +(5 rows) + +DROP TABLE stock_price; +-- Auto-generated DEFINE +CREATE TABLE rpr_auto (id INT, val INT); +INSERT INTO rpr_auto VALUES (1, 10), (2, 20), (3, 30), (4, 15); +-- One variable undefined (B auto-generated as "B IS TRUE") +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_auto +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ B*) + DEFINE A AS val > 15 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 3 + 3 | 30 | 0 + 4 | 15 | 0 +(4 rows) + +-- Multiple undefined variables +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_auto +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B C) + DEFINE A AS val > 0 + -- B and C auto-generated as "B IS TRUE", "C IS TRUE" +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 15 | 0 +(4 rows) + +-- All variables defined explicitly +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_auto +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (X Y Z) + DEFINE + X AS val > 10, + Y AS val > 20, + Z AS val < 20 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 3 + 3 | 30 | 0 + 4 | 15 | 0 +(4 rows) + +DROP TABLE rpr_auto; +-- Duplicate variable names +CREATE TABLE rpr_dup (id INT); +INSERT INTO rpr_dup VALUES (1), (2); +-- Duplicate DEFINE variable name is not allowed +SELECT COUNT(*) OVER w +FROM rpr_dup +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id > 0, A AS id < 10 +); +ERROR: DEFINE variable "a" appears more than once +LINE 7: DEFINE A AS id > 0, A AS id < 10 + ^ +DROP TABLE rpr_dup; +-- Boolean coercion +CREATE TABLE rpr_bool (id INT, flag BOOLEAN); +INSERT INTO rpr_bool VALUES (1, true), (2, false); +-- DEFINE clause must be a boolean expression +SELECT COUNT(*) OVER w +FROM rpr_bool +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id +); +ERROR: argument of DEFINE must be type boolean, not type integer +LINE 7: DEFINE A AS id + ^ +-- Boolean column reference +SELECT id, flag, COUNT(*) OVER w as cnt +FROM rpr_bool +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (T+) + DEFINE T AS flag +) +ORDER BY id; + id | flag | cnt +----+------+----- + 1 | t | 1 + 2 | f | 0 +(2 rows) + +-- NULL::boolean +SELECT id, COUNT(*) OVER w as cnt +FROM rpr_bool +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (N+) + DEFINE N AS NULL::boolean +) +ORDER BY id; + id | cnt +----+----- + 1 | 0 + 2 | 0 +(2 rows) + +-- Implicit cast to boolean via custom type +CREATE TYPE truthyint AS (v int); +CREATE FUNCTION truthyint_to_bool(truthyint) RETURNS boolean AS $$ + SELECT ($1).v <> 0; +$$ LANGUAGE SQL IMMUTABLE STRICT; +CREATE CAST (truthyint AS boolean) + WITH FUNCTION truthyint_to_bool(truthyint) + AS ASSIGNMENT; +CREATE TABLE rpr_coerce (id int, val truthyint); +INSERT INTO rpr_coerce VALUES (1, ROW(1)), (2, ROW(0)), (3, ROW(5)), (4, ROW(0)); +SELECT id, val, cnt +FROM (SELECT id, val, + COUNT(*) OVER w AS cnt + FROM rpr_coerce + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val + ) +) s ORDER BY id; + id | val | cnt +----+-----+----- + 1 | (1) | 1 + 2 | (0) | 0 + 3 | (5) | 1 + 4 | (0) | 0 +(4 rows) + +DROP TABLE rpr_coerce; +DROP CAST (truthyint AS boolean); +DROP FUNCTION truthyint_to_bool(truthyint); +DROP TYPE truthyint; +DROP TABLE rpr_bool; +-- Coercion over a boolean domain is not a no-op; the wrapped Var must still +-- propagate when referenced only in DEFINE (flag is not in the select list) +CREATE DOMAIN boolish AS boolean; +CREATE TABLE rpr_domain (id int, flag boolish); +INSERT INTO rpr_domain VALUES (1, true), (2, false), (3, true); +SELECT id, COUNT(*) OVER w AS cnt +FROM rpr_domain +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS flag +) +ORDER BY id; + id | cnt +----+----- + 1 | 1 + 2 | 0 + 3 | 1 +(3 rows) + +DROP TABLE rpr_domain; +DROP DOMAIN boolish; +-- A Var referenced only inside a navigation operation must still propagate +-- (val appears only inside PREV(), not as a bare operand or in the select list) +CREATE TABLE rpr_nav (id int, val int); +INSERT INTO rpr_nav VALUES (1, 0), (2, 1), (3, 0), (4, 2); +SELECT id, COUNT(*) OVER w AS cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (UP+) + DEFINE UP AS id > PREV(val) +) +ORDER BY id; + id | cnt +----+----- + 1 | 0 + 2 | 3 + 3 | 0 + 4 | 0 +(4 rows) + +DROP TABLE rpr_nav; +-- A non-boolean DEFINE expression is rejected +CREATE TABLE rpr_noncoerce (id int, n int); +INSERT INTO rpr_noncoerce VALUES (1, 1); +SELECT id, COUNT(*) OVER w AS cnt +FROM rpr_noncoerce +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS n +); +ERROR: argument of DEFINE must be type boolean, not type integer +LINE 7: DEFINE A AS n + ^ +DROP TABLE rpr_noncoerce; +-- A non-boolean later DEFINE is rejected at its own definition even when an +-- earlier DEFINE variable is valid +CREATE TABLE rpr_noncoerce2 (id int, n int); +INSERT INTO rpr_noncoerce2 VALUES (1, 1); +SELECT id, COUNT(*) OVER w AS cnt +FROM rpr_noncoerce2 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS id > 0, B AS n +); +ERROR: argument of DEFINE must be type boolean, not type integer +LINE 7: DEFINE A AS id > 0, B AS n + ^ +DROP TABLE rpr_noncoerce2; +-- Complex expressions +CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT); +INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30); +-- CASE expression +SELECT id, val1, val2, COUNT(*) OVER w as cnt +FROM rpr_complex +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C+) + DEFINE C AS CASE WHEN val1 > 10 THEN val2 > 20 ELSE false END +) +ORDER BY id; + id | val1 | val2 | cnt +----+------+------+----- + 1 | 10 | 20 | 0 + 2 | 15 | 25 | 2 + 3 | 20 | 30 | 0 +(3 rows) + +DROP TABLE rpr_complex; +-- Pattern variable not in PATTERN (should be ignored) +CREATE TABLE rpr_unused (id INT); +INSERT INTO rpr_unused VALUES (1), (2); +-- Extra DEFINE variable +SELECT id, COUNT(*) OVER w as cnt +FROM rpr_unused +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id > 0, B AS id > 5 -- B not in pattern +) +ORDER BY id; +ERROR: DEFINE variable "b" is not used in PATTERN +LINE 7: DEFINE A AS id > 0, B AS id > 5 -- B not in pattern + ^ +DROP TABLE rpr_unused; +-- ============================================================ +-- FRAME Options Tests +-- ============================================================ +CREATE TABLE rpr_frame (id INT, val INT); +INSERT INTO rpr_frame VALUES + (1, 10), (2, 10), (3, 10), -- Same val: 10 + (4, 20), (5, 20), -- Same val: 20 + (6, 30); +-- Valid frame options +-- ROWS: counts physical rows (1 FOLLOWING = next 1 physical row) +-- Expected result: Each row can see 1 physical row ahead +-- id=1,2,3 (val=10): can see next row -> cnt=2 +-- id=4,5 (val=20): can see next row -> cnt=2 +-- id=6 (val=30): no next row -> cnt=1 +-- Result: [2,2,2,2,2,1] +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY val + ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B?) + DEFINE A AS val >= 0, B AS val >= 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 10 | 2 + 3 | 10 | 2 + 4 | 20 | 2 + 5 | 20 | 2 + 6 | 30 | 1 +(6 rows) + +-- ERROR: frame must start at current row when row pattern recognition is used +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: FRAME must start at CURRENT ROW when using row pattern recognition +LINE 5: ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + ^ +DETAIL: Current frame starts with UNBOUNDED PRECEDING. +HINT: Use: ROWS BETWEEN CURRENT ROW AND ... +-- EXCLUDE options +-- EXCLUDE not permitted +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + EXCLUDE CURRENT ROW + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: cannot use EXCLUDE options with row pattern recognition +LINE 6: EXCLUDE CURRENT ROW + ^ +DETAIL: Frame definition includes EXCLUDE CURRENT ROW. +HINT: Remove the EXCLUDE clause from the window definition. +-- EXCLUDE GROUP not permitted +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + EXCLUDE GROUP + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: cannot use EXCLUDE options with row pattern recognition +LINE 6: EXCLUDE GROUP + ^ +DETAIL: Frame definition includes EXCLUDE GROUP. +HINT: Remove the EXCLUDE clause from the window definition. +-- EXCLUDE TIES not permitted +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + EXCLUDE TIES + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: cannot use EXCLUDE options with row pattern recognition +LINE 6: EXCLUDE TIES + ^ +DETAIL: Frame definition includes EXCLUDE TIES. +HINT: Remove the EXCLUDE clause from the window definition. +-- range frame is not allowed with RPR +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: cannot use FRAME option RANGE with row pattern recognition +LINE 5: RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWIN... + ^ +HINT: Use ROWS instead. +-- GROUPS frame is not allowed with RPR +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: cannot use FRAME option GROUPS with row pattern recognition +LINE 5: GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI... + ^ +HINT: Use ROWS instead. +-- ERROR: frame must start at current row when row pattern recognition is used +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: FRAME must start at CURRENT ROW when using row pattern recognition +LINE 5: ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING + ^ +DETAIL: Current frame starts with offset PRECEDING. +HINT: Use: ROWS BETWEEN CURRENT ROW AND ... +-- ERROR: frame must start at current row with RPR +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: FRAME must start at CURRENT ROW when using row pattern recognition +LINE 5: ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING + ^ +DETAIL: Current frame starts with offset FOLLOWING. +HINT: Use: ROWS BETWEEN CURRENT ROW AND ... +-- ERROR: end before start: CURRENT ROW AND 1 PRECEDING +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 1 PRECEDING + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: frame starting from current row cannot have preceding rows +LINE 5: ROWS BETWEEN CURRENT ROW AND 1 PRECEDING + ^ +-- ERROR: end before start: CURRENT ROW AND UNBOUNDED PRECEDING +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: frame end cannot be UNBOUNDED PRECEDING +LINE 5: ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING + ^ +-- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard +-- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING). +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND CURRENT ROW + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A) + DEFINE A AS val > 0 +) +ORDER BY id; +ERROR: cannot use CURRENT ROW as frame end with row pattern recognition +LINE 5: ROWS BETWEEN CURRENT ROW AND CURRENT ROW + ^ +HINT: Use UNBOUNDED FOLLOWING or a positive offset FOLLOWING. +-- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame +-- and is likewise rejected (caught at execution time). +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 0 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A) + DEFINE A AS val > 0 +) +ORDER BY id; +ERROR: frame ending offset must be positive with row pattern recognition +-- A non-constant frame end offset is allowed; a zero value is still rejected, +-- this time at execution time (a literal cannot exercise that path). +PREPARE rpr_end_offset(int8) AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND $1 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A) + DEFINE A AS val > 0 +) +ORDER BY id; +EXECUTE rpr_end_offset(2); + id | val | cnt +----+-----+----- + 1 | 10 | 1 + 2 | 10 | 1 + 3 | 10 | 1 + 4 | 20 | 1 + 5 | 20 | 1 + 6 | 30 | 1 +(6 rows) + +EXECUTE rpr_end_offset(0); +ERROR: frame ending offset must be positive with row pattern recognition +DEALLOCATE rpr_end_offset; +-- Large offset: CURRENT ROW AND 1000 FOLLOWING +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 1000 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 6 + 2 | 10 | 5 + 3 | 10 | 4 + 4 | 20 | 3 + 5 | 20 | 2 + 6 | 30 | 1 +(6 rows) + +-- Maximum offset: CURRENT ROW AND 2147483646 FOLLOWING (INT_MAX - 1) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 2147483646 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 6 + 2 | 10 | 5 + 3 | 10 | 4 + 4 | 20 | 3 + 5 | 20 | 2 + 6 | 30 | 1 +(6 rows) + +-- int64 frame-end overflow: a huge FOLLOWING offset must clamp to the +-- partition end (matchStartRow + offset + 1 overflows int64; the clamp makes +-- it behave like UNBOUNDED FOLLOWING). Guards against signed-integer overflow +-- in the "frameOffset + 1" subexpression (undefined behavior). The cnt values +-- must match the UNBOUNDED FOLLOWING result for the same data. +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 9223372036854775806 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 6 + 2 | 10 | 5 + 3 | 10 | 4 + 4 | 20 | 3 + 5 | 20 | 2 + 6 | 30 | 1 +(6 rows) + +-- range frame is not allowed with RPR +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY val + RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B?) + DEFINE A AS val >= 0, B AS val >= 0 +) +ORDER BY id; +ERROR: cannot use FRAME option RANGE with row pattern recognition +LINE 5: RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING + ^ +HINT: Use ROWS instead. +-- GROUPS frame with RPR (not permitted) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY val + GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B?) + DEFINE A AS val >= 0, B AS val >= 0 +) +ORDER BY id; +ERROR: cannot use FRAME option GROUPS with row pattern recognition +LINE 5: GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING + ^ +HINT: Use ROWS instead. +DROP TABLE rpr_frame; +-- ============================================================ +-- PARTITION BY + FRAME Tests +-- ============================================================ +-- Test PARTITION BY with RPR to ensure proper partitioning behavior +CREATE TABLE rpr_partition (id INT, grp INT, val INT); +INSERT INTO rpr_partition VALUES + (1, 1, 10), (2, 1, 20), (3, 1, 30), + (4, 2, 15), (5, 2, 25), (6, 2, 35); +-- PARTITION BY with ROWS frame +SELECT id, grp, val, COUNT(*) OVER w as cnt +FROM rpr_partition +WINDOW w AS ( + PARTITION BY grp + ORDER BY val + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B+) + DEFINE A AS val >= 10, B AS val > 15 +) +ORDER BY id; + id | grp | val | cnt +----+-----+-----+----- + 1 | 1 | 10 | 3 + 2 | 1 | 20 | 2 + 3 | 1 | 30 | 0 + 4 | 2 | 15 | 3 + 5 | 2 | 25 | 2 + 6 | 2 | 35 | 0 +(6 rows) + +-- Expected: Pattern matching should reset for each partition +-- PARTITION BY with RANGE frame +SELECT id, grp, val, COUNT(*) OVER w as cnt +FROM rpr_partition +WINDOW w AS ( + PARTITION BY grp + ORDER BY val + RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B?) + DEFINE A AS val >= 10, B AS val >= 20 +) +ORDER BY id; +ERROR: cannot use FRAME option RANGE with row pattern recognition +LINE 6: RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING + ^ +HINT: Use ROWS instead. +DROP TABLE rpr_partition; +-- ============================================================ +-- PATTERN Syntax Tests +-- ============================================================ +CREATE TABLE rpr_pattern (id INT, val INT); +INSERT INTO rpr_pattern VALUES + (1, 5), (2, 10), (3, 15), (4, 20), (5, 25), + (6, 30), (7, 35), (8, 40), (9, 45), (10, 50); +-- Alternation (|) +-- Multiple alternatives +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ | B+ | C+) + DEFINE A AS val > 35, B AS val BETWEEN 15 AND 35, C AS val < 15 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 5 | 2 + 2 | 10 | 0 + 3 | 15 | 5 + 4 | 20 | 0 + 5 | 25 | 0 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 40 | 3 + 9 | 45 | 0 + 10 | 50 | 0 +(10 rows) + +-- Grouping +-- Nested grouping with quantifier +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B) C)+) + DEFINE A AS val > 10, B AS val > 20, C AS val > 30 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 5 | 0 + 2 | 10 | 0 + 3 | 15 | 0 + 4 | 20 | 0 + 5 | 25 | 6 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 40 | 0 + 9 | 45 | 0 + 10 | 50 | 0 +(10 rows) + +-- Sequence +-- Multi-element sequence +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B C D E) + DEFINE + A AS val < 15, + B AS val BETWEEN 15 AND 25, + C AS val BETWEEN 25 AND 35, + D AS val BETWEEN 35 AND 45, + E AS val >= 45 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 5 | 0 + 2 | 10 | 0 + 3 | 15 | 0 + 4 | 20 | 0 + 5 | 25 | 0 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 40 | 0 + 9 | 45 | 0 + 10 | 50 | 0 +(10 rows) + +-- Complex combinations +-- Alternation with grouping +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B) | (C D)) + DEFINE A AS val < 20, B AS val >= 20, C AS val < 30, D AS val >= 30 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 5 | 0 + 2 | 10 | 0 + 3 | 15 | 2 + 4 | 20 | 0 + 5 | 25 | 2 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 40 | 0 + 9 | 45 | 0 + 10 | 50 | 0 +(10 rows) + +-- Alternation + sequence + grouping +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (START (UP{2,} DOWN? | FLAT+) FINISH) + DEFINE + START AS val >= 0, + UP AS val > 20, + DOWN AS val <= 30, + FLAT AS val BETWEEN 25 AND 35, + FINISH AS val > 40 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 5 | 0 + 2 | 10 | 0 + 3 | 15 | 0 + 4 | 20 | 7 + 5 | 25 | 0 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 40 | 0 + 9 | 45 | 0 + 10 | 50 | 0 +(10 rows) + +-- Nested alternation in groups +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B) (C | D)) + DEFINE A AS val < 15, B AS val BETWEEN 15 AND 25, C AS val BETWEEN 25 AND 35, D AS val > 35 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 5 | 0 + 2 | 10 | 0 + 3 | 15 | 0 + 4 | 20 | 2 + 5 | 25 | 0 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 40 | 0 + 9 | 45 | 0 + 10 | 50 | 0 +(10 rows) + +DROP TABLE rpr_pattern; +-- ============================================================ +-- Quantifiers Tests +-- ============================================================ +CREATE TABLE rpr_quant (id INT, val INT); +INSERT INTO rpr_quant VALUES + (1, 10), (2, 20), (3, 30), (4, 40), (5, 50), + (6, 60), (7, 70), (8, 80), (9, 90), (10, 100); +-- Basic greedy quantifiers +-- * (zero or more) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A*) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 10 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- + (one or more) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 50 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 5 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- ? (zero or one) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A?) + DEFINE A AS val = 50 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 1 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Edge case quantifiers +-- {0} is not allowed (min must be >= 1) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{0} B) + DEFINE A AS val > 1000, B AS val > 0 +) +ORDER BY id; +ERROR: quantifier bound must be between 1 and 2147483646 +LINE 6: PATTERN (A{0} B) + ^ +-- {0,0} is not allowed (max must be >= 1) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{0,0} B) + DEFINE A AS val > 1000, B AS val > 0 +) +ORDER BY id; +ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1 +LINE 6: PATTERN (A{0,0} B) + ^ +-- {0,1} (equivalent to ?) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{0,1}) + DEFINE A AS val = 50 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 1 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Exact quantifiers {n} +-- {3} (representative exact quantifier) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{3}) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 3 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 3 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Range quantifiers {n,} +-- {2,} (representative n or more) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2,}) + DEFINE A AS val > 40 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 6 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Upper bound quantifiers {,m} +-- {,3} (representative up to m) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,3}) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 3 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 3 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 1 +(10 rows) + +-- Range quantifiers {n,m} +-- {3,7} (representative range) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{3,7}) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 7 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 3 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +DROP TABLE rpr_quant; +-- Reluctant quantifiers +CREATE TABLE rpr_reluctant (id INT, val INT); +INSERT INTO rpr_reluctant VALUES (1, 10), (2, 20), (3, 30); +-- *? (zero or more, reluctant) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A*?) + DEFINE A AS val > 0 +); + count +------- + 0 + 0 + 0 +(3 rows) + +-- +? (one or more, reluctant) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+?) + DEFINE A AS val > 0 +); + count +------- + 1 + 1 + 1 +(3 rows) + +-- ?? (zero or one, reluctant) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A??) + DEFINE A AS val > 0 +); + count +------- + 0 + 0 + 0 +(3 rows) + +-- {n,}? (n or more, reluctant) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2,}?) + DEFINE A AS val > 0 +); + count +------- + 2 + 0 + 0 +(3 rows) + +-- {n,m}? (n to m, reluctant) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1,3}?) + DEFINE A AS val > 0 +); + count +------- + 1 + 1 + 1 +(3 rows) + +-- {n}? (exactly n, reluctant) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2}?) + DEFINE A AS val > 0 +); + count +------- + 2 + 0 + 0 +(3 rows) + +-- {,m}? (up to m, reluctant) - COMPLETELY UNTESTED RULE! +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,3}?) + DEFINE A AS val > 0 +); + count +------- + 0 + 0 + 0 +(3 rows) + +-- {2}+ (should be {2}? not {2}+) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2}+) + DEFINE A AS val > 0 +); +ERROR: syntax error at or near "+" +LINE 6: PATTERN (A{2}+) + ^ +-- {2,}* (should be {2,}? not {2,}*) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2,}*) + DEFINE A AS val > 0 +); +ERROR: syntax error at or near "*" +LINE 6: PATTERN (A{2,}*) + ^ +-- {,3}* (should be {,3}? not {,3}*) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,3}*) + DEFINE A AS val > 0 +); +ERROR: syntax error at or near "*" +LINE 6: PATTERN (A{,3}*) + ^ +-- {1,3}+ (should be {1,3}? not {1,3}+) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1,3}+) + DEFINE A AS val > 0 +); +ERROR: syntax error at or near "+" +LINE 6: PATTERN (A{1,3}+) + ^ +-- Boundary errors in reluctant quantifiers +-- negative bound is not allowed +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{-1}?) + DEFINE A AS val > 0 +); +ERROR: syntax error at or near "-" +LINE 6: PATTERN (A{-1}?) + ^ +-- ERROR: quantifier bound exceeds limits +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2147483647}?) + DEFINE A AS val > 0 +); +ERROR: quantifier bound must be between 1 and 2147483646 +LINE 6: PATTERN (A{2147483647}?) + ^ +-- negative lower bound is not allowed +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{-1,}?) + DEFINE A AS val > 0 +); +ERROR: syntax error at or near "-" +LINE 6: PATTERN (A{-1,}?) + ^ +-- ERROR: quantifier lower bound exceeds limits +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2147483647,}?) + DEFINE A AS val > 0 +); +ERROR: quantifier bound must be between 0 and 2147483646 +LINE 6: PATTERN (A{2147483647,}?) + ^ +-- zero upper bound is not allowed +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,0}?) + DEFINE A AS val > 0 +); +ERROR: quantifier bound must be between 1 and 2147483646 +LINE 6: PATTERN (A{,0}?) + ^ +-- ERROR: {,2147483647}? (upper bound in range exceeds limits) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,2147483647}?) + DEFINE A AS val > 0 +); +ERROR: quantifier bound must be between 1 and 2147483646 +LINE 6: PATTERN (A{,2147483647}?) + ^ +-- ERROR: {-1,3}? (negative lower bound in range is not allowed) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{-1,3}?) + DEFINE A AS val > 0 +); +ERROR: syntax error at or near "-" +LINE 6: PATTERN (A{-1,3}?) + ^ +-- ERROR: {1,2147483647}? (upper bound in range exceeds limits) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1,2147483647}?) + DEFINE A AS val > 0 +); +ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1 +LINE 6: PATTERN (A{1,2147483647}?) + ^ +-- ERROR: {5,3}? (min > max is not allowed) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{5,3}?) + DEFINE A AS val > 0 +); +ERROR: quantifier minimum bound must not exceed maximum +LINE 6: PATTERN (A{5,3}?) + ^ +-- Token-separated reluctant quantifiers (space between quantifier and ?) +-- These may be tokenized differently by the lexer +-- * ? (token separated) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A* ?) + DEFINE A AS val > 0 +); + count +------- + 0 + 0 + 0 +(3 rows) + +-- + ? (token separated) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ ?) + DEFINE A AS val > 0 +); + count +------- + 1 + 1 + 1 +(3 rows) + +-- {2,} ? (token separated) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2,} ?) + DEFINE A AS val > 0 +); + count +------- + 2 + 0 + 0 +(3 rows) + +-- * + (invalid combination) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A* +) + DEFINE A AS val > 0 +); +ERROR: syntax error at or near "+" +LINE 6: PATTERN (A* +) + ^ +-- + * (invalid combination) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ *) + DEFINE A AS val > 0 +); +ERROR: syntax error at or near "*" +LINE 6: PATTERN (A+ *) + ^ +-- ? ? (parsed as ?? reluctant quantifier) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A? ?) + DEFINE A AS val > 0 +); + count +------- + 0 + 0 + 0 +(3 rows) + +DROP TABLE rpr_reluctant; +-- Quantifier boundary conditions +CREATE TABLE rpr_bounds (id INT); +INSERT INTO rpr_bounds VALUES (1), (2); +-- ERROR: quantifier lower bound must not exceed upper bound +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{5,3}) + DEFINE A AS id > 0 +); +ERROR: quantifier minimum bound must not exceed maximum +LINE 6: PATTERN (A{5,3}) + ^ +-- Large bounds +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1000,2000}) + DEFINE A AS id > 0 +); + count +------- + 0 + 0 +(2 rows) + +-- Very large bound +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{100000}) + DEFINE A AS id > 0 +); + count +------- + 0 + 0 +(2 rows) + +-- INT_MAX - 1 = 2147483646 (at limit) +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2147483646}) + DEFINE A AS id > 0 +); + count +------- + 0 + 0 +(2 rows) + +-- ERROR: quantifier bound exceeds limits +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2147483647}) + DEFINE A AS id > 0 +); +ERROR: quantifier bound must be between 1 and 2147483646 +LINE 6: PATTERN (A{2147483647}) + ^ +-- {n,} boundary errors +-- ERROR: negative lower bound in {n,} is not allowed +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{-1,}) + DEFINE A AS id > 0 +); +ERROR: syntax error at or near "-" +LINE 6: PATTERN (A{-1,}) + ^ +-- ERROR: quantifier lower bound exceeds limits +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2147483647,}) + DEFINE A AS id > 0 +); +ERROR: quantifier bound must be between 0 and 2147483646 +LINE 6: PATTERN (A{2147483647,}) + ^ +-- {,m} boundary errors +-- Zero upper bound in {,m} +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,0}) + DEFINE A AS id > 0 +); +ERROR: quantifier bound must be between 1 and 2147483646 +LINE 6: PATTERN (A{,0}) + ^ +-- ERROR: quantifier upper bound exceeds limits +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,2147483647}) + DEFINE A AS id > 0 +); +ERROR: quantifier bound must be between 1 and 2147483646 +LINE 6: PATTERN (A{,2147483647}) + ^ +DROP TABLE rpr_bounds; +-- Pattern element-count boundary at RPR_ELEMIDX_MAX (32767). Alternating +-- distinct variables stop the optimizer from merging consecutive elements, so +-- each "A B" pair contributes two elements; scanRPRPattern adds one FIN marker. +-- ECHO is silenced so the generated multi-thousand-token patterns do not flood +-- the expected output. +-- 16383 pairs -> 32766 + 1 FIN = 32767 = maximum, accepted. +-- 16383 pairs + one A -> 32767 + 1 FIN = 32768 > maximum, rejected. +\set ECHO none + count +------- + 0 +(1 row) + +ERROR: pattern too complex +DETAIL: Pattern has 32768 elements, maximum is 32767. +-- ============================================================ +-- Navigation Functions Tests (PREV / NEXT / FIRST / LAST) +-- ============================================================ +CREATE TABLE rpr_nav (id INT, val INT); +INSERT INTO rpr_nav VALUES + (1, 10), (2, 20), (3, 15), (4, 25), (5, 30); +-- PREV function - reference previous row in pattern +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS val > 0, + B AS val > PREV(val) +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 3 + 4 | 25 | 0 + 5 | 30 | 0 +(5 rows) + +-- NEXT function - reference next row in pattern +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ B) + DEFINE + A AS val < NEXT(val), + B AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 3 + 4 | 25 | 0 + 5 | 30 | 0 +(5 rows) + +-- Combined PREV and NEXT +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B C) + DEFINE + A AS val > 0, + B AS val > PREV(val) AND val < NEXT(val), + C AS val > PREV(val) +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 15 | 3 + 4 | 25 | 0 + 5 | 30 | 0 +(5 rows) + +-- PREV function cannot be used other than in DEFINE +SELECT PREV(id), id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS val > 0, + B AS val > PREV(val) +) +ORDER BY id; +ERROR: function prev(integer) does not exist +LINE 1: SELECT PREV(id), id, val, COUNT(*) OVER w as cnt + ^ +DETAIL: There is no function of that name. +-- NEXT function cannot be used other than in DEFINE +SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS val > 0, + B AS val > PREV(val) +) +ORDER BY id; +ERROR: function next(integer) does not exist +LINE 1: SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt + ^ +DETAIL: There is no function of that name. +-- FIRST function - reference match_start row +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS val > 0, + B AS val > FIRST(val) +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 5 + 2 | 20 | 0 + 3 | 15 | 0 + 4 | 25 | 0 + 5 | 30 | 0 +(5 rows) + +-- LAST function without offset - equivalent to current row's value +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS val > 0, + B AS LAST(val) > PREV(val) +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 3 + 4 | 25 | 0 + 5 | 30 | 0 +(5 rows) + +-- FIRST and LAST combined +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS val > 0, + B AS val > FIRST(val) AND LAST(val) > PREV(val) +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 3 + 4 | 25 | 0 + 5 | 30 | 0 +(5 rows) + +-- FIRST function cannot be used other than in DEFINE +SELECT FIRST(id), id, val FROM rpr_nav; +ERROR: function first(integer) does not exist +LINE 1: SELECT FIRST(id), id, val FROM rpr_nav; + ^ +DETAIL: There is no function of that name. +-- LAST function cannot be used other than in DEFINE +SELECT LAST(id), id, val FROM rpr_nav; +ERROR: function last(integer) does not exist +LINE 1: SELECT LAST(id), id, val FROM rpr_nav; + ^ +DETAIL: There is no function of that name. +DROP TABLE rpr_nav; +-- Name-space: prev/next/first/last are navigation functions, not ordinary functions +CREATE SCHEMA rpr_navns; +SET search_path TO rpr_navns, public; +CREATE TABLE nt (g text, id int, val int); +INSERT INTO nt VALUES ('x', 1, 100), ('x', 2, 200), ('x', 3, 150), + ('x', 4, 140), ('x', 5, 150); +-- Outside DEFINE these are ordinary identifiers and resolve to nothing +SELECT prev(val) FROM nt; +ERROR: function prev(integer) does not exist +LINE 1: SELECT prev(val) FROM nt; + ^ +DETAIL: There is no function of that name. +SELECT next(val) FROM nt; +ERROR: function next(integer) does not exist +LINE 1: SELECT next(val) FROM nt; + ^ +DETAIL: There is no function of that name. +SELECT prev(val, 2) FROM nt; +ERROR: function prev(integer, integer) does not exist +LINE 1: SELECT prev(val, 2) FROM nt; + ^ +DETAIL: There is no function of that name. +SELECT next(val, 2) FROM nt; +ERROR: function next(integer, integer) does not exist +LINE 1: SELECT next(val, 2) FROM nt; + ^ +DETAIL: There is no function of that name. +SELECT first(val) FROM nt; +ERROR: function first(integer) does not exist +LINE 1: SELECT first(val) FROM nt; + ^ +DETAIL: There is no function of that name. +SELECT last(val) FROM nt; +ERROR: function last(integer) does not exist +LINE 1: SELECT last(val) FROM nt; + ^ +DETAIL: There is no function of that name. +SELECT first(val, 1) FROM nt; +ERROR: function first(integer, integer) does not exist +LINE 1: SELECT first(val, 1) FROM nt; + ^ +DETAIL: There is no function of that name. +-- A schema-qualified call is also a plain (failing) function lookup +SELECT pg_catalog.prev(val) FROM nt; +ERROR: function pg_catalog.prev(integer) does not exist +LINE 1: SELECT pg_catalog.prev(val) FROM nt; + ^ +-- Outside DEFINE, a user-defined function of that name is callable +CREATE FUNCTION next(numeric) RETURNS numeric AS 'SELECT -999::numeric' + LANGUAGE sql IMMUTABLE; +SELECT next(10); + next +------ + -999 +(1 row) + +-- Inside DEFINE, unqualified PREV is nav whether or not a user prev() exists +SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (START UP+) + DEFINE START AS TRUE, UP AS val > PREV(val)) + ORDER BY id; + id | val | cnt | last_id +----+-----+-----+--------- + 1 | 100 | 2 | 2 + 2 | 200 | 0 | + 3 | 150 | 0 | + 4 | 140 | 2 | 5 + 5 | 150 | 0 | +(5 rows) + +-- A qualified call invokes the function, so its volatility still matters +-- VOLATILE: unqualified is nav; qualified is rejected as a volatile function +CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999' + LANGUAGE sql VOLATILE; +SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (START UP+) + DEFINE START AS TRUE, UP AS val > PREV(val)) + ORDER BY id; + id | val | cnt | last_id +----+-----+-----+--------- + 1 | 100 | 2 | 2 + 2 | 200 | 0 | + 3 | 150 | 0 | + 4 | 140 | 2 | 5 + 5 | 150 | 0 | +(5 rows) + +SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) + DEFINE A AS rpr_navns.prev(val) = -999) + ORDER BY id; +ERROR: volatile functions are not allowed in DEFINE clause +LINE 6: DEFINE A AS rpr_navns.prev(val) = -999) + ^ +DROP FUNCTION prev(integer); +-- IMMUTABLE: unqualified is nav; qualified is the escape hatch and succeeds +CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999' + LANGUAGE sql IMMUTABLE; +SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (START UP+) + DEFINE START AS TRUE, UP AS val > PREV(val)) + ORDER BY id; + id | val | cnt | last_id +----+-----+-----+--------- + 1 | 100 | 2 | 2 + 2 | 200 | 0 | + 3 | 150 | 0 | + 4 | 140 | 2 | 5 + 5 | 150 | 0 | +(5 rows) + +-- (val).prev is attribute notation, so it calls the ordinary function prev(val) +-- (the IMMUTABLE user prev here), the same as the schema-qualified call below +SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) + DEFINE A AS (val).prev = -999) + ORDER BY id; + id | val | cnt | last_id +----+-----+-----+--------- + 1 | 100 | 5 | 5 + 2 | 200 | 0 | + 3 | 150 | 0 | + 4 | 140 | 0 | + 5 | 150 | 0 | +(5 rows) + +SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) + DEFINE A AS rpr_navns.prev(val) = -999) + ORDER BY id; + id | val | cnt | last_id +----+-----+-----+--------- + 1 | 100 | 5 | 5 + 2 | 200 | 0 | + 3 | 150 | 0 | + 4 | 140 | 0 | + 5 | 150 | 0 | +(5 rows) + +-- Zero or more than two arguments is an error, with no function fallback +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV() IS NULL); +ERROR: too few arguments for row pattern navigation function PREV +LINE 4: PATTERN (A+) DEFINE A AS PREV() IS NULL); + ^ +DETAIL: PREV takes a value expression and an optional offset argument. +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL); +ERROR: too many arguments for row pattern navigation function PREV +LINE 4: PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL); + ^ +DETAIL: PREV takes a value expression and an optional offset argument. +-- the error stands even when a user function of that exact arity exists +CREATE FUNCTION prev(integer, integer, integer) RETURNS integer + AS 'SELECT -999' LANGUAGE sql IMMUTABLE; +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL); +ERROR: too many arguments for row pattern navigation function PREV +LINE 4: PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL); + ^ +DETAIL: PREV takes a value expression and an optional offset argument. +DROP FUNCTION prev(integer, integer, integer); +-- Syntactic decoration is rejected +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(*) IS NULL); +ERROR: prev(*) specified, but prev is not an aggregate function +LINE 4: PATTERN (A+) DEFINE A AS PREV(*) IS NULL); + ^ +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(DISTINCT val) IS NULL); +ERROR: DISTINCT specified, but prev is not an aggregate function +LINE 4: PATTERN (A+) DEFINE A AS PREV(DISTINCT val) IS NULL); + ^ +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val ORDER BY val) IS NULL); +ERROR: ORDER BY specified, but prev is not an aggregate function +LINE 4: PATTERN (A+) DEFINE A AS PREV(val ORDER BY val) IS NULL)... + ^ +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val) FILTER (WHERE true) IS NULL); +ERROR: FILTER specified, but prev is not an aggregate function +LINE 4: PATTERN (A+) DEFINE A AS PREV(val) FILTER (WHERE true) I... + ^ +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val) WITHIN GROUP (ORDER BY val) IS NULL); +ERROR: WITHIN GROUP specified, but prev is not an aggregate function +LINE 4: PATTERN (A+) DEFINE A AS PREV(val) WITHIN GROUP (ORDER B... + ^ +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val) OVER () IS NULL); +ERROR: OVER specified, but prev is not a window function nor an aggregate function +LINE 4: PATTERN (A+) DEFINE A AS PREV(val) OVER () IS NULL); + ^ +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(VARIADIC ARRAY[val]) IS NULL); +ERROR: cannot use VARIADIC with row pattern navigation function PREV +LINE 4: PATTERN (A+) DEFINE A AS PREV(VARIADIC ARRAY[val]) IS NU... + ^ +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS prev(x => val) IS NULL); +ERROR: cannot use named arguments with row pattern navigation function PREV +LINE 4: PATTERN (A+) DEFINE A AS prev(x => val) IS NULL); + ^ +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val) IGNORE NULLS IS NULL); +ERROR: RESPECT/IGNORE NULLS specified, but prev is not a window function +LINE 4: PATTERN (A+) DEFINE A AS PREV(val) IGNORE NULLS IS NULL)... + ^ +-- Quoting does not escape: "prev" is nav, "PREV" is an ordinary name +SELECT id, val, count(*) OVER w AS cnt + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (START UP+) + DEFINE START AS TRUE, UP AS val > "prev"(val)) + ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 100 | 2 + 2 | 200 | 0 + 3 | 150 | 0 + 4 | 140 | 2 + 5 | 150 | 0 +(5 rows) + +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS "PREV"(val) IS NULL); +ERROR: function PREV(integer) does not exist +LINE 4: PATTERN (A+) DEFINE A AS "PREV"(val) IS NULL); + ^ +DETAIL: There is no function of that name. +-- A view round-trips: bare PREV stays a navigation function, and a qualified +-- user prev() stays schema-qualified so it does not reparse as navigation +CREATE VIEW navns_nav AS + SELECT id, count(*) OVER w AS cnt FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (START UP+) DEFINE START AS TRUE, UP AS val > PREV(val)); +CREATE VIEW navns_fn AS + SELECT id, count(*) OVER w AS cnt FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS rpr_navns.prev(val) = -999); +SELECT pg_get_viewdef('navns_nav'); + pg_get_viewdef +--------------------------------------------------------------------------------------------- + SELECT id, + + count(*) OVER w AS cnt + + FROM nt + + WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (start up+) + + DEFINE + + start AS true, + + up AS (val > PREV(val)) ); +(1 row) + +SELECT pg_get_viewdef('navns_fn'); + pg_get_viewdef +--------------------------------------------------------------------------------------------- + SELECT id, + + count(*) OVER w AS cnt + + FROM nt + + WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS (rpr_navns.prev(val) = '-999'::integer) ); +(1 row) + +DROP VIEW navns_nav, navns_fn; +-- A qualified last() in DEFINE must stay schema-qualified on deparse so that +-- it does not reparse as the LAST navigation function (force-qualify path) +CREATE FUNCTION rpr_navns.last(integer) RETURNS integer AS 'SELECT -999' LANGUAGE sql IMMUTABLE; +CREATE VIEW navns_fn_last AS + SELECT id, count(*) OVER w AS cnt FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS rpr_navns.last(val) = -999); +SELECT pg_get_viewdef('navns_fn_last'); + pg_get_viewdef +--------------------------------------------------------------------------------------------- + SELECT id, + + count(*) OVER w AS cnt + + FROM nt + + WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS (rpr_navns.last(val) = '-999'::integer) ); +(1 row) + +DROP VIEW navns_fn_last; +DROP FUNCTION rpr_navns.last(integer); +-- Attribute notation is field selection only, never a function fallback +CREATE TYPE rpr_navns_pair AS (first int, last int); +CREATE TABLE ct (id int, p rpr_navns_pair); +INSERT INTO ct VALUES (1, (10, 20)), (2, (30, 40)); +SELECT (p).last FROM ct ORDER BY id; + last +------ + 20 + 40 +(2 rows) + +SELECT count(*) OVER w FROM ct + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS (p).last > 0); + count +------- + 2 + 0 +(2 rows) + +SELECT count(*) OVER w FROM ct + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS (p).prev > 0); +ERROR: column "prev" not found in data type rpr_navns_pair +LINE 4: PATTERN (A+) DEFINE A AS (p).prev > 0); + ^ +-- Navigation offset must not contain a navigation operation +SELECT id, val + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) + DEFINE A AS PREV(val, FIRST(1)) > 0) + ORDER BY id; +ERROR: row pattern navigation offset cannot contain a row pattern navigation operation +LINE 6: DEFINE A AS PREV(val, FIRST(1)) > 0) + ^ +DETAIL: A navigation offset must be a run-time constant. +DROP SCHEMA rpr_navns CASCADE; +RESET search_path; +-- ============================================================ +-- SKIP TO / INITIAL Tests +-- ============================================================ +CREATE TABLE rpr_skip (id INT, val INT); +INSERT INTO rpr_skip VALUES + (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), + (6, 6), (7, 7), (8, 8); +-- SKIP TO NEXT ROW +-- SKIP TO NEXT ROW +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_skip +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C) + DEFINE A AS val > 0, B AS val > 2, C AS val > 4 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 1 | 0 + 2 | 2 | 0 + 3 | 3 | 3 + 4 | 4 | 3 + 5 | 5 | 3 + 6 | 6 | 3 + 7 | 7 | 0 + 8 | 8 | 0 +(8 rows) + +-- SKIP PAST LAST ROW +-- SKIP PAST LAST ROW +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_skip +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS val > 0, B AS val > 2, C AS val > 4 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 1 | 0 + 2 | 2 | 0 + 3 | 3 | 3 + 4 | 4 | 0 + 5 | 5 | 0 + 6 | 6 | 3 + 7 | 7 | 0 + 8 | 8 | 0 +(8 rows) + +-- Default behavior (should be SKIP PAST LAST ROW) +-- No SKIP TO clause (default) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_skip +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B) + DEFINE A AS val > 0, B AS val > 1 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 1 | 2 + 2 | 2 | 0 + 3 | 3 | 2 + 4 | 4 | 0 + 5 | 5 | 2 + 6 | 6 | 0 + 7 | 7 | 2 + 8 | 8 | 0 +(8 rows) + +-- Compare default with explicit PAST LAST ROW +-- Results should be identical +WITH default_skip AS ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_skip + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B C) + DEFINE A AS val > 0, B AS val > 2, C AS val > 4 + ) +), +explicit_skip AS ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_skip + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS val > 0, B AS val > 2, C AS val > 4 + ) +) +SELECT 'default' as type, * FROM default_skip +UNION ALL +SELECT 'explicit' as type, * FROM explicit_skip +ORDER BY type, id; + type | id | val | cnt +----------+----+-----+----- + default | 1 | 1 | 0 + default | 2 | 2 | 0 + default | 3 | 3 | 3 + default | 4 | 4 | 0 + default | 5 | 5 | 0 + default | 6 | 6 | 3 + default | 7 | 7 | 0 + default | 8 | 8 | 0 + explicit | 1 | 1 | 0 + explicit | 2 | 2 | 0 + explicit | 3 | 3 | 3 + explicit | 4 | 4 | 0 + explicit | 5 | 5 | 0 + explicit | 6 | 6 | 3 + explicit | 7 | 7 | 0 + explicit | 8 | 8 | 0 +(16 rows) + +DROP TABLE rpr_skip; +CREATE TABLE rpr_init (id INT, val INT); +INSERT INTO rpr_init VALUES (1, 10), (2, 20), (3, 30), (4, 40); +-- Explicit INITIAL +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_init +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 4 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 +(4 rows) + +-- Implicit INITIAL (default) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_init +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 4 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 +(4 rows) + +DROP TABLE rpr_init; +-- SEEK +CREATE TABLE rpr_seek (id INT, val INT); +INSERT INTO rpr_seek VALUES (1, 10); +-- SEEK keyword, SEEK mode is not supported +SELECT COUNT(*) OVER w +FROM rpr_seek +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + SEEK + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: SEEK is not supported +LINE 6: SEEK + ^ +HINT: Use INITIAL instead. +DROP TABLE rpr_seek; +-- ============================================================ +-- Serialization/Deserialization Tests +-- ============================================================ +-- RPR-defining views and tables here are intentionally left in place (not +-- dropped) so that pg_dump/pg_upgrade exercise the deparse-then-re-parse +-- round-trip of the RPR window clause. +-- View creation and deparsing +CREATE TABLE rpr_serial (id INT, val INT); +INSERT INTO rpr_serial VALUES + (1, 10), (2, 20), (3, 15), (4, 25), (5, 30); +-- Simple pattern +CREATE VIEW rpr_serial_v1 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); +-- Verify view works (tests deserialization) +SELECT * FROM rpr_serial_v1 ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 5 + 2 | 20 | 0 + 3 | 15 | 0 + 4 | 25 | 0 + 5 | 30 | 0 +(5 rows) + +-- Verify deparsing +SELECT pg_get_viewdef('rpr_serial_v1'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS cnt + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS (val > 0) ); +(1 row) + +-- Complex pattern with alternation +CREATE VIEW rpr_serial_v2 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ | B*) + DEFINE A AS val > 20, B AS val <= 20 +); +SELECT * FROM rpr_serial_v2 ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 + 3 | 15 | 0 + 4 | 25 | 2 + 5 | 30 | 0 +(5 rows) + +SELECT pg_get_viewdef('rpr_serial_v2'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS cnt + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+ | b*) + + DEFINE + + a AS (val > 20), + + b AS (val <= 20) ); +(1 row) + +-- Pattern with grouping and quantifiers +CREATE VIEW rpr_serial_v3 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){2,5} | C*) + DEFINE + A AS val > 10, + B AS val > 20, + C AS val <= 10 +); +SELECT * FROM rpr_serial_v3 ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 1 + 2 | 20 | 0 + 3 | 15 | 0 + 4 | 25 | 0 + 5 | 30 | 0 +(5 rows) + +SELECT pg_get_viewdef('rpr_serial_v3'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS cnt + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN ((a b){2,5} | c*) + + DEFINE + + a AS (val > 10), + + b AS (val > 20), + + c AS (val <= 10) ); +(1 row) + +-- All features combined +CREATE VIEW rpr_serial_v4 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + INITIAL + PATTERN (START (MID{1,3} | ALT+) FINISH) + DEFINE + START AS val > 5, + MID AS val BETWEEN 10 AND 25, + ALT AS val > 25, + FINISH AS val > 15 +); +SELECT * FROM rpr_serial_v4 ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 5 + 2 | 20 | 4 + 3 | 15 | 3 + 4 | 25 | 0 + 5 | 30 | 0 +(5 rows) + +SELECT pg_get_viewdef('rpr_serial_v4'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS cnt + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP TO NEXT ROW + + INITIAL + + PATTERN (start (mid{1,3} | alt+) finish) + + DEFINE + + start AS (val > 5), + + mid AS ((val >= 10) AND (val <= 25)), + + alt AS (val > 25), + + finish AS (val > 15) ); +(1 row) + +-- Additional quantifiers for deparsing coverage +-- ? quantifier (zero or one) +CREATE VIEW rpr_serial_v5 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B?) + DEFINE A AS val > 10, B AS val > 20 +); +SELECT * FROM rpr_serial_v5 ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 1 + 3 | 15 | 2 + 4 | 25 | 0 + 5 | 30 | 1 +(5 rows) + +SELECT pg_get_viewdef('rpr_serial_v5'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS cnt + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a b?) + + DEFINE + + a AS (val > 10), + + b AS (val > 20) ); +(1 row) + +-- {n,} quantifier (n or more) +CREATE VIEW rpr_serial_v6 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2,}) + DEFINE A AS val > 15 +); +SELECT * FROM rpr_serial_v6 ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 15 | 0 + 4 | 25 | 2 + 5 | 30 | 0 +(5 rows) + +SELECT pg_get_viewdef('rpr_serial_v6'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS cnt + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a{2,}) + + DEFINE + + a AS (val > 15) ); +(1 row) + +-- {n} quantifier (exactly n) +CREATE VIEW rpr_serial_v7 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{3}) + DEFINE A AS val > 0 +); +SELECT * FROM rpr_serial_v7 ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 + 3 | 15 | 0 + 4 | 25 | 0 + 5 | 30 | 0 +(5 rows) + +SELECT pg_get_viewdef('rpr_serial_v7'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS cnt + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a{3}) + + DEFINE + + a AS (val > 0) ); +(1 row) + +-- Nested ALT pattern (tests deparse of complex nested structure) +CREATE VIEW rpr_serial_v8 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A+ B) | C) D | A B C) + DEFINE A AS val <= 15, B AS val <= 25, C AS val <= 30, D AS val > 30 +); +SELECT * FROM rpr_serial_v8 ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 + 3 | 15 | 0 + 4 | 25 | 0 + 5 | 30 | 0 +(5 rows) + +SELECT pg_get_viewdef('rpr_serial_v8'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS cnt + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (((a+ b) | c) d | a b c) + + DEFINE + + a AS (val <= 15), + + b AS (val <= 25), + + c AS (val <= 30), + + d AS (val > 30) ); +(1 row) + +-- Navigation function serialization: PREV with offset +CREATE VIEW rpr_serial_nav1 AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS val > PREV(val, 2)); +SELECT pg_get_viewdef('rpr_serial_nav1'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS count + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a b+) + + DEFINE + + a AS true, + + b AS (val > PREV(val, (2)::bigint)) ); +(1 row) + +-- Navigation function serialization: FIRST and LAST +CREATE VIEW rpr_serial_nav2 AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS FIRST(val) < LAST(val, 1)); +SELECT pg_get_viewdef('rpr_serial_nav2'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS count + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a b+) + + DEFINE + + a AS true, + + b AS (FIRST(val) < LAST(val, (1)::bigint)) ); +(1 row) + +-- Navigation function serialization: compound PREV(FIRST()) +CREATE VIEW rpr_serial_nav3 AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(val, 1), 2) > 0); +SELECT pg_get_viewdef('rpr_serial_nav3'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS count + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a b+) + + DEFINE + + a AS true, + + b AS (PREV(FIRST(val, (1)::bigint), (2)::bigint) > 0) ); +(1 row) + +-- Navigation function serialization: compound NEXT(LAST()) +CREATE VIEW rpr_serial_nav4 AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(LAST(val), 2) IS NOT NULL); +SELECT pg_get_viewdef('rpr_serial_nav4'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS count + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a b+) + + DEFINE + + a AS true, + + b AS (NEXT(LAST(val), (2)::bigint) IS NOT NULL) ); +(1 row) + +-- Navigation function serialization: compound PREV(LAST()) +CREATE VIEW rpr_serial_nav5 AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(LAST(val, 1), 2) > 0); +SELECT pg_get_viewdef('rpr_serial_nav5'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS count + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a b+) + + DEFINE + + a AS true, + + b AS (PREV(LAST(val, (1)::bigint), (2)::bigint) > 0) ); +(1 row) + +-- Navigation function serialization: compound NEXT(FIRST()) +CREATE VIEW rpr_serial_nav6 AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(FIRST(val), 3) > 0); +SELECT pg_get_viewdef('rpr_serial_nav6'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS count + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a b+) + + DEFINE + + a AS true, + + b AS (NEXT(FIRST(val), (3)::bigint) > 0) ); +(1 row) + +-- Pretty deparse: navigation calls are function-like and take no extra parens +CREATE VIEW rpr_nav_pretty_v AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, + B AS val > PREV(val) AND PREV(val) IS NOT NULL + AND NEXT(val) > FIRST(val) + AND PREV(FIRST(val)) > 0); +SELECT pg_get_viewdef('rpr_nav_pretty_v'::regclass, true); + pg_get_viewdef +--------------------------------------------------------------------------------------------------------- + SELECT id, + + val, + + count(*) OVER w AS count + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a b+) + + DEFINE + + a AS true, + + b AS val > PREV(val) AND PREV(val) IS NOT NULL AND NEXT(val) > FIRST(val) AND PREV(FIRST(val)) > 0 ); +(1 row) + +-- Reluctant {1}? quantifier deparse through ruleutils +CREATE VIEW rpr_quant_reluctant_v AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A{1}? B) + DEFINE A AS val > 0, B AS val > 0); +SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS count + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a{1}? b) + + DEFINE + + a AS (val > 0), + + b AS (val > 0) ); +(1 row) + +-- Quoted identifier round-trip: mixed case and reserved words need quoting +CREATE VIEW rpr_serial_quoted AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ("Start" "Up"+) + DEFINE "Start" AS TRUE, "Up" AS val > PREV(val)); +SELECT pg_get_viewdef('rpr_serial_quoted'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS count + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN ("Start" "Up"+) + + DEFINE + + "Start" AS true, + + "Up" AS (val > PREV(val)) ); +(1 row) + +-- Inline OVER round-trip: inline window spec (no WINDOW alias) deparses inside OVER (...) +CREATE VIEW rpr_serial_inline_over AS +SELECT id, val, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS val > 10, B AS val > PREV(val)) AS cnt +FROM rpr_serial; +SELECT pg_get_viewdef('rpr_serial_inline_over'::regclass); + pg_get_viewdef +---------------------------------------------------------------------------------- + SELECT id, + + val, + + count(*) OVER (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a b+) + + DEFINE + + a AS (val > 10), + + b AS (val > PREV(val)) ) AS cnt + + FROM rpr_serial; +(1 row) + +-- Materialized view (if supported) +CREATE TABLE rpr_mview (id INT, val INT); +INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30); +CREATE MATERIALIZED VIEW rpr_mview_v1 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_mview +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); +SELECT * FROM rpr_mview_v1 ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 + 3 | 30 | 0 +(3 rows) + +SELECT pg_get_viewdef('rpr_mview_v1'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS cnt + + FROM rpr_mview + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS (val > 0) ); +(1 row) + +-- Refresh test +REFRESH MATERIALIZED VIEW rpr_mview_v1; +SELECT * FROM rpr_mview_v1 ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 + 3 | 30 | 0 +(3 rows) + +-- CREATE TABLE AS SELECT with RPR +CREATE TABLE rpr_ctas (id INT, val INT); +INSERT INTO rpr_ctas VALUES (1, 10), (2, 20), (3, 15), (4, 25); +CREATE TABLE rpr_ctas_result AS +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_ctas +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 TRUE, B AS val > PREV(val) +); +SELECT * FROM rpr_ctas_result ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 2 + 4 | 25 | 0 +(4 rows) + +-- INSERT INTO ... SELECT with RPR +CREATE TABLE rpr_insert_target (id INT, val INT, cnt BIGINT); +INSERT INTO rpr_insert_target +SELECT id, val, count(*) OVER w +FROM rpr_ctas +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 TRUE, B AS val > PREV(val) +); +SELECT * FROM rpr_insert_target ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 2 + 4 | 25 | 0 +(4 rows) + +DROP TABLE rpr_ctas_result; +DROP TABLE rpr_insert_target; +DROP TABLE rpr_ctas; +-- Prepared statements (tests outfuncs.c / readfuncs.c) +CREATE TABLE rpr_prep (id INT, val INT); +INSERT INTO rpr_prep VALUES (1, 10), (2, 20), (3, 30); +-- Simple prepared statement +PREPARE rpr_prep_simple AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_prep +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; +EXECUTE rpr_prep_simple; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 + 3 | 30 | 0 +(3 rows) + +EXECUTE rpr_prep_simple; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 + 3 | 30 | 0 +(3 rows) + +DEALLOCATE rpr_prep_simple; +-- Prepared statement with parameters +PREPARE rpr_prep_param(int) AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_prep +WHERE id <= $1 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 10 +) +ORDER BY id; +EXECUTE rpr_prep_param(2); + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 1 +(2 rows) + +EXECUTE rpr_prep_param(3); + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 2 + 3 | 30 | 0 +(3 rows) + +DEALLOCATE rpr_prep_param; +-- Complex prepared statement +PREPARE rpr_prep_complex AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_prep +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A B){1,2} | C+) + DEFINE + A AS val > 5, + B AS val > 15, + C AS val <= 15 +) +ORDER BY id; +EXECUTE rpr_prep_complex; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 2 + 3 | 30 | 0 +(3 rows) + +EXECUTE rpr_prep_complex; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 2 + 3 | 30 | 0 +(3 rows) + +DEALLOCATE rpr_prep_complex; +DROP TABLE rpr_prep; +-- CTE and Subquery (tests copyfuncs.c) +CREATE TABLE rpr_copy (id INT, val INT); +INSERT INTO rpr_copy VALUES (1, 10), (2, 20), (3, 30), (4, 40); +-- Simple CTE +WITH rpr_cte AS ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_copy + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) +SELECT * FROM rpr_cte ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 4 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 +(4 rows) + +-- CTE with multiple references (forces node copy) +WITH rpr_cte AS ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_copy + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 15 + ) +) +SELECT c1.id, c1.cnt as cnt1, c2.cnt as cnt2 +FROM rpr_cte c1 +JOIN rpr_cte c2 ON c1.id = c2.id +ORDER BY c1.id; + id | cnt1 | cnt2 +----+------+------ + 1 | 0 | 0 + 2 | 3 | 3 + 3 | 0 | 0 + 4 | 0 | 0 +(4 rows) + +-- Subquery in FROM clause +SELECT * +FROM ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_copy + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B?) + DEFINE A AS val > 10, B AS val > 20 + ) +) sub +WHERE cnt > 0 +ORDER BY id; + id | val | cnt +----+-----+----- + 2 | 20 | 2 + 4 | 40 | 1 +(2 rows) + +-- Nested subqueries +SELECT * +FROM ( + SELECT * + FROM ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_copy + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val >= 10 + ) + ) inner_sub + WHERE cnt > 0 +) outer_sub +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 4 +(1 row) + +DROP TABLE rpr_copy; +-- DISTINCT and set operations (tests equalfuncs.c) +CREATE TABLE rpr_equal (id INT, val INT); +INSERT INTO rpr_equal VALUES (1, 10), (2, 20), (3, 10), (4, 20); +-- DISTINCT with RPR +SELECT DISTINCT cnt +FROM ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_equal + WINDOW w AS ( + ORDER BY val + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE A AS val > 0 + ) +) sub +ORDER BY cnt; + cnt +----- + 1 + 2 + 3 + 4 +(4 rows) + +-- UNION with RPR in both sides +SELECT id, val, cnt FROM ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_equal + WHERE val = 10 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) sub1 +UNION +SELECT id, val, cnt FROM ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_equal + WHERE val = 20 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) sub2 +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 2 + 3 | 10 | 0 + 4 | 20 | 0 +(4 rows) + +-- UNION ALL +SELECT id, cnt FROM ( + SELECT id, COUNT(*) OVER w as cnt + FROM rpr_equal + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 10 + ) +) sub +UNION ALL +SELECT id, cnt FROM ( + SELECT id, COUNT(*) OVER w as cnt + FROM rpr_equal + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (B+) + DEFINE B AS val <= 10 + ) +) sub +ORDER BY id, cnt; + id | cnt +----+----- + 1 | 0 + 1 | 1 + 2 | 0 + 2 | 1 + 3 | 0 + 3 | 1 + 4 | 0 + 4 | 1 +(8 rows) + +-- INTERSECT +SELECT id, cnt FROM ( + SELECT id, COUNT(*) OVER w as cnt + FROM rpr_equal + WHERE id <= 3 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) sub1 +INTERSECT +SELECT id, cnt FROM ( + SELECT id, COUNT(*) OVER w as cnt + FROM rpr_equal + WHERE id >= 2 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) sub2 +ORDER BY id; + id | cnt +----+----- + 3 | 0 +(1 row) + +DROP TABLE rpr_equal; +-- View with multiple window definitions +CREATE TABLE rpr_multiwin (id INT, val INT); +INSERT INTO rpr_multiwin VALUES (1, 10), (2, 20), (3, 30); +CREATE VIEW rpr_multiwin_v AS +SELECT + id, + val, + COUNT(*) OVER w1 as cnt1, + COUNT(*) OVER w2 as cnt2 +FROM rpr_multiwin +WINDOW + w1 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 15 + ), + w2 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (B*) + DEFINE B AS val <= 15 + ); +SELECT * FROM rpr_multiwin_v ORDER BY id; + id | val | cnt1 | cnt2 +----+-----+------+------ + 1 | 10 | 0 | 1 + 2 | 20 | 2 | 0 + 3 | 30 | 0 | 0 +(3 rows) + +SELECT pg_get_viewdef('rpr_multiwin_v'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------------------- + SELECT id, + + val, + + count(*) OVER w1 AS cnt1, + + count(*) OVER w2 AS cnt2 + + FROM rpr_multiwin + + WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS (val > 15) ), w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (b*) + + DEFINE + + b AS (val <= 15) ); +(1 row) + +-- {n} quantifier display in view +CREATE VIEW rpr_quant_n_v AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A{3}) + DEFINE A AS val > 0); +SELECT pg_get_viewdef('rpr_quant_n_v'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS count + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a{3}) + + DEFINE + + a AS (val > 0) ); +(1 row) + +-- {n,} quantifier display in view +CREATE VIEW rpr_quant_n_plus_v AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A{2,}) + DEFINE A AS val > 0); +SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT id, + + val, + + count(*) OVER w AS count + + FROM rpr_serial + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a{2,}) + + DEFINE + + a AS (val > 0) ); +(1 row) + +-- ============================================================ +-- Glued Quantifier / Alternation Tests +-- ============================================================ +CREATE TABLE rpr_glue (id INT, val INT); +INSERT INTO rpr_glue VALUES (1, 5), (2, 8), (3, 9), (4, -1), (5, 6), (6, -2); +-- Quantifier glued to the alternation operator '|' without a space (0059). +-- The lexer glues the trailing '|' into one Op token; the grammar reattaches it +-- as the lowest-precedence alternation once the surrounding sequence is built. +-- Deparse is canonical, so the glued, spaced, and mixed-spacing forms all +-- reduce to the same PATTERN -- one deparse per shape proves the parse tree. +-- Op-char quantifiers (*, +, ?, *?, +?, ??) glued to '|'. +CREATE VIEW rpr_dp_op AS SELECT + count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, + count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6 +FROM rpr_glue +WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0), + w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0), + w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A?|B) DEFINE A AS val > 0, B AS val <= 0), + w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0), + w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0), + w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??|B) DEFINE A AS val > 0, B AS val <= 0); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_op'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------- + PATTERN (a* | b) + PATTERN (a+ | b) + PATTERN (a? | b) + PATTERN (a*? | b) + PATTERN (a+? | b) + PATTERN (a?? | b) +(6 rows) + +DROP VIEW rpr_dp_op; +-- Spaced reference: the fully-spaced canonical forms. Identical deparse to the +-- glued rpr_dp_op w1/w4 above completes the glued = spaced = mixed equivalence. +CREATE VIEW rpr_dp_spc AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2 +FROM rpr_glue +WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* | B) DEFINE A AS val > 0, B AS val <= 0), + w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*? | B) DEFINE A AS val > 0, B AS val <= 0); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_spc'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------- + PATTERN (a* | b) + PATTERN (a*? | b) +(2 rows) + +DROP VIEW rpr_dp_spc; +-- Range quantifiers glued to '|': non-reluctant {n}| (} + char '|') and +-- reluctant {n}?| (} + Op "?|"). +CREATE VIEW rpr_dp_rng AS SELECT + count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, count(*) OVER w4 AS w4, + count(*) OVER w5 AS w5, count(*) OVER w6 AS w6, count(*) OVER w7 AS w7, count(*) OVER w8 AS w8 +FROM rpr_glue +WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}|B) DEFINE A AS val > 0, B AS val <= 0), + w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}|B) DEFINE A AS val > 0, B AS val <= 0), + w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}|B) DEFINE A AS val > 0, B AS val <= 0), + w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}|B) DEFINE A AS val > 0, B AS val <= 0), + w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}?|B) DEFINE A AS val > 0, B AS val <= 0), + w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}?|B) DEFINE A AS val > 0, B AS val <= 0), + w7 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}?|B) DEFINE A AS val > 0, B AS val <= 0), + w8 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}?|B) DEFINE A AS val > 0, B AS val <= 0); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_rng'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------- + PATTERN (a{2} | b) + PATTERN (a{2,} | b) + PATTERN (a{0,3} | b) + PATTERN (a{2,3} | b) + PATTERN (a{2}? | b) + PATTERN (a{2,}? | b) + PATTERN (a{0,3}? | b) + PATTERN (a{2,3}? | b) +(8 rows) + +DROP VIEW rpr_dp_rng; +-- Mixed spacing: a space inside the quantifier with '|' still glued. +-- "A* ?|B" = '*' + Op"?|" = reluctant "A*?" plus alternation. +CREATE VIEW rpr_dp_mix AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3 +FROM rpr_glue +WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* ?|B) DEFINE A AS val > 0, B AS val <= 0), + w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ ?|B) DEFINE A AS val > 0, B AS val <= 0), + w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? ?|B) DEFINE A AS val > 0, B AS val <= 0); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_mix'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------- + PATTERN (a*? | b) + PATTERN (a+? | b) + PATTERN (a?? | b) +(3 rows) + +DROP VIEW rpr_dp_mix; +-- Structure: precedence (| is lowest, so its right operand is the whole +-- following sequence), chaining, concatenation, and grouping. +CREATE VIEW rpr_dp_struct AS SELECT + count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, + count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6 +FROM rpr_glue +WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100), + w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B*|C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100), + w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B*|C D) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100, D AS val > 5), + w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)) DEFINE A AS val > 0, B AS val <= 0), + w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|(B|C)) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100), + w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)+) DEFINE A AS val > 0, B AS val <= 0); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_struct'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------------- + PATTERN (a* | b c) + PATTERN (a* | b* | c) + PATTERN (a b* | c d) + PATTERN ((a* | b)) + PATTERN (a* | (b | c)) + PATTERN ((a* | b)+) +(6 rows) + +DROP VIEW rpr_dp_struct; +-- Execution semantics (deparse cannot show reluctant shortest-match). The +-- rpr_glue rows -- an A-run followed by B rows -- make the '|B' alternative +-- reachable: with "*" the greedy form matches the whole run while the +-- reluctant form matches empty; with "+" the greedy form matches the run and +-- the reluctant form matches one row, and on a B row (where "A+" fails) the B +-- alternative fires. +SELECT id, val, + count(*) OVER gs AS gstar, count(*) OVER rs AS rstar, + count(*) OVER gp AS gplus, count(*) OVER rp AS rplus +FROM rpr_glue +WINDOW gs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0), + rs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0), + gp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0), + rp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0) +ORDER BY id; + id | val | gstar | rstar | gplus | rplus +----+-----+-------+-------+-------+------- + 1 | 5 | 3 | 0 | 3 | 1 + 2 | 8 | 0 | 0 | 0 | 1 + 3 | 9 | 0 | 0 | 0 | 1 + 4 | -1 | 1 | 1 | 1 | 1 + 5 | 6 | 1 | 0 | 1 | 1 + 6 | -2 | 1 | 1 | 1 | 1 +(6 rows) + +-- Patterns that must stay rejected. "&" is an invalid op; a '|' with an empty +-- side (leading, trailing, doubled, or alone in a group) has no operand; "||" +-- and "*||" are doubled pipes; "A* *|B"/"A* *?|B"/"A{2}*?|B" are doubled +-- quantifiers. +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE A AS val > 0); +ERROR: unsupported quantifier "&" +LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE... + ^ +HINT: Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions. +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFINE A AS val > 0); +ERROR: alternation operator "|" requires a pattern on both sides +LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFIN... + ^ +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DEFINE A AS val > 0, B AS val <= 0); +ERROR: alternation operator "|" requires a pattern on both sides +LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DE... + ^ +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFINE A AS val > 0, B AS val <= 0); +ERROR: unsupported quantifier "*||" +LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFI... + ^ +HINT: Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions. +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFINE A AS val > 0, B AS val <= 0); +ERROR: unsupported quantifier "||" +LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFIN... + ^ +HINT: Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions. +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A AS val > 0, B AS val <= 0); +ERROR: syntax error at or near ")" +LINE 1: ...CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A... + ^ +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE A AS val > 0); +ERROR: syntax error at or near "|" +LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE... + ^ +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFINE A AS val > 0); +ERROR: alternation operator "|" requires a pattern on both sides +LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFI... + ^ +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFINE A AS val > 0, B AS val <= 0); +ERROR: invalid token "*" after "*" quantifier +LINE 1: ...N CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFIN... + ^ +HINT: Did you mean "*?" for reluctant quantifier? +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFINE A AS val > 0, B AS val <= 0); +ERROR: invalid token "*?" after "*" quantifier +LINE 1: ...N CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFI... + ^ +HINT: Did you mean "*?" for reluctant quantifier? +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DEFINE A AS val > 0, B AS val <= 0); +ERROR: invalid quantifier combination +LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DE... + ^ +HINT: Did you mean "??" for reluctant quantifier? +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFINE A AS val > 0, B AS val <= 0); +ERROR: invalid token "*?" after range quantifier +LINE 1: ... CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFI... + ^ +HINT: Only "?" is allowed after {n} to make it reluctant. +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFINE A AS val > 0, B AS val <= 0); +ERROR: invalid token "*?" after range quantifier +LINE 1: ...CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFI... + ^ +HINT: Only "?" is allowed after {n} to make it reluctant. +-- Doubled op-char quantifiers lex as one Op token and are unsupported, whether +-- glued to '|' ("**|", "*+|", "???|") or on their own ("**"). +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFINE A AS val > 0, B AS val <= 0); +ERROR: unsupported quantifier "**|" +LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFI... + ^ +HINT: Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions. +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFINE A AS val > 0, B AS val <= 0); +ERROR: unsupported quantifier "*+|" +LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFI... + ^ +HINT: Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions. +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEFINE A AS val > 0, B AS val <= 0); +ERROR: unsupported quantifier "???|" +LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEF... + ^ +HINT: Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions. +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFINE A AS val > 0); +ERROR: unsupported quantifier "**" +LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFIN... + ^ +HINT: Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions. +DROP TABLE rpr_glue; +-- ============================================================ +-- Error Cases Tests +-- ============================================================ +DROP TABLE IF EXISTS rpr_err; +CREATE TABLE rpr_err (id INT, val INT); +INSERT INTO rpr_err VALUES (1, 10), (2, 20); +-- Invalid quantifier syntax +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+!) + DEFINE A AS val > 0 +); +ERROR: unsupported quantifier "+!" +LINE 6: PATTERN (A+!) + ^ +HINT: Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions. +-- none of the following queries should be accepted +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ !) DEFINE A AS TRUE); +ERROR: invalid token "!" after "+" quantifier +LINE 1: ...S BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ !) DEFINE ... + ^ +HINT: Did you mean "+?" for reluctant quantifier? +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ ?+) DEFINE A AS TRUE); +ERROR: invalid token "?+" after "+" quantifier +LINE 1: ...S BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ ?+) DEFINE... + ^ +HINT: Did you mean "+?" for reluctant quantifier? +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A* ?+) DEFINE A AS TRUE); +ERROR: invalid token "?+" after "*" quantifier +LINE 1: ...S BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A* ?+) DEFINE... + ^ +HINT: Did you mean "*?" for reluctant quantifier? +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A? ??) DEFINE A AS TRUE); +ERROR: invalid quantifier combination +LINE 1: ...OWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A? ??) DEFI... + ^ +HINT: Did you mean "??" for reluctant quantifier? +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A {1,2}??) DEFINE A AS TRUE); +ERROR: invalid token "??" after range quantifier +LINE 1: ...TWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A {1,2}??) DEFINE... + ^ +HINT: Only "?" is allowed after {n,m} to make it reluctant. +-- none of the following 4 range-quantifier queries should be accepted +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2} !) DEFINE A AS TRUE); +ERROR: invalid token "!" after range quantifier +LINE 1: ...BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2} !) DEFINE ... + ^ +HINT: Only "?" is allowed after {n} to make it reluctant. +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,} !) DEFINE A AS TRUE); +ERROR: invalid token "!" after range quantifier +LINE 1: ...ETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,} !) DEFINE ... + ^ +HINT: Only "?" is allowed after {n,} or {,m} to make it reluctant. +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{,3} !) DEFINE A AS TRUE); +ERROR: invalid token "!" after range quantifier +LINE 1: ...ETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{,3} !) DEFINE ... + ^ +HINT: Only "?" is allowed after {n,} or {,m} to make it reluctant. +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,3} !) DEFINE A AS TRUE); +ERROR: invalid token "!" after range quantifier +LINE 1: ...TWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,3} !) DEFINE ... + ^ +HINT: Only "?" is allowed after {n,m} to make it reluctant. +-- Unmatched parentheses +SET client_min_messages = NOTICE; +DO $$ +BEGIN + EXECUTE 'SELECT COUNT(*) OVER w FROM rpr_err WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A B) DEFINE A AS val > 0, B AS val > 10)'; + RAISE NOTICE 'Unmatched parentheses: UNEXPECTED SUCCESS'; +EXCEPTION + WHEN syntax_error THEN + RAISE NOTICE 'Unmatched parentheses: EXPECTED ERROR - %', SQLERRM; + WHEN OTHERS THEN + RAISE NOTICE 'Unmatched parentheses: UNEXPECTED ERROR - %', SQLERRM; +END $$; +NOTICE: Unmatched parentheses: EXPECTED ERROR - syntax error at or near "AS" +SET client_min_messages = WARNING; +-- ERROR: empty DEFINE not allowed +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE +); +ERROR: syntax error at or near ")" +LINE 8: ); + ^ +-- ERROR: empty PATTERN not allowed +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN () + DEFINE A AS val > 0 +); +ERROR: syntax error at or near ")" +LINE 6: PATTERN () + ^ +-- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together) +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + DEFINE A AS val > 0 +); +ERROR: syntax error at or near "DEFINE" +LINE 6: DEFINE A AS val > 0 + ^ +-- Qualified column references (NOT SUPPORTED) +-- Pattern variable qualified name: not supported (valid per ISO/IEC 19075-5 6.15 / 4.16, not yet implemented) +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS A.val > 0 +); +ERROR: pattern variable qualified expression "a.val" is not supported in DEFINE clause +LINE 7: DEFINE A AS A.val > 0 + ^ +-- PATTERN-only variable qualified name: not supported even without DEFINE entry +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ B+) + DEFINE A AS B.val > 0 +); +ERROR: pattern variable qualified expression "b.val" is not supported in DEFINE clause +LINE 7: DEFINE A AS B.val > 0 + ^ +-- DEFINE-only variable qualified name: still a pattern variable, not a range variable +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0, B AS B.val > 0 +); +ERROR: DEFINE variable "b" is not used in PATTERN +LINE 7: DEFINE A AS val > 0, B AS B.val > 0 + ^ +-- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5) +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS rpr_err.val > 0 +); +ERROR: range variable qualified expression "rpr_err.val" is not allowed in DEFINE clause +LINE 7: DEFINE A AS rpr_err.val > 0 + ^ +-- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check +-- must fall through so that normal column resolution produces a sensible error. +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS nosuch.val > 0 +); +ERROR: missing FROM-clause entry for table "nosuch" +LINE 7: DEFINE A AS nosuch.val > 0 + ^ +-- Unqualified composite field access in DEFINE works: no qualifier means no +-- pattern/range-var navigation, so the pre-check skips and normal resolution +-- handles "(items).amount" via A_Indirection on the current row. +CREATE TYPE rpr_item AS (name TEXT, amount INT); +CREATE TEMP TABLE rpr_composite (id int, items rpr_item); +INSERT INTO rpr_composite VALUES (1, ROW('a',5)), (2, ROW('b',15)), (3, ROW('c',25)); +SELECT id, (items).amount, COUNT(*) OVER w AS cnt +FROM rpr_composite +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE A AS (items).amount > 10 +); + id | amount | cnt +----+--------+----- + 1 | 5 | 0 + 2 | 15 | 2 + 3 | 25 | 1 +(3 rows) + +-- Expected: rows where (items).amount > 10 form matches; counts reflect frame size +-- Composite type field selection (qualified forms): the ColumnRef portion ("A.items" or +-- "rpr_composite.items") is what gets quoted; the trailing ".amount" lives in +-- the surrounding A_Indirection node and is not visible to the pre-check. +SELECT COUNT(*) OVER w +FROM rpr_composite +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS (A.items).amount > 10 +); +ERROR: pattern variable qualified expression "a.items" is not supported in DEFINE clause +LINE 7: DEFINE A AS (A.items).amount > 10 + ^ +SELECT COUNT(*) OVER w +FROM rpr_composite +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS (rpr_composite.items).amount > 10 +); +ERROR: range variable qualified expression "rpr_composite.items" is not allowed in DEFINE clause +LINE 7: DEFINE A AS (rpr_composite.items).amount > 10 + ^ +DROP TABLE rpr_composite; +DROP TYPE rpr_item; +-- ERROR: undefined column in DEFINE +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS nonexistent_column > 0 +); +ERROR: column "nonexistent_column" does not exist +LINE 7: DEFINE A AS nonexistent_column > 0 + ^ +-- ERROR: type mismatch +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 'string' +); +ERROR: invalid input syntax for type integer: "string" +LINE 7: DEFINE A AS val > 'string' + ^ +-- ERROR: aggregate function in DEFINE is not supported +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS COUNT(*) > 0 +); +ERROR: aggregate functions are not allowed in DEFINE +LINE 7: DEFINE A AS COUNT(*) > 0 + ^ +-- ERROR: set-returning function in DEFINE is not supported +SELECT FROM rpr_err +WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > generate_series(1 ,2)); +ERROR: set-returning functions are not allowed in DEFINE +LINE 2: ... ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > generate_s... + ^ +-- ERROR: window function in DEFINE is not supported +SELECT FROM rpr_err +WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > row_number() OVER ()); +ERROR: window functions are not allowed in DEFINE +LINE 2: ... ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > row_number... + ^ +-- Subquery in DEFINE is not supported +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > (SELECT max(val) FROM rpr_err) +); +ERROR: cannot use subquery in DEFINE expression +LINE 7: DEFINE A AS val > (SELECT max(val) FROM rpr_err) + ^ +-- Pattern variable not used (should work, extra vars ignored) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0, B AS val > 5, C AS val > 10 +) +ORDER BY id; +ERROR: DEFINE variable "b" is not used in PATTERN +LINE 7: DEFINE A AS val > 0, B AS val > 5, C AS val > 10 + ^ +DROP TABLE rpr_err; +-- NULL handling +CREATE TABLE rpr_null (id INT, val INT); +INSERT INTO rpr_null VALUES (1, 10), (2, NULL), (3, 30); +-- NULL in DEFINE expression +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_null +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 15 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | | 0 + 3 | 30 | 1 +(3 rows) + +-- IS NULL in DEFINE +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_null +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (N+) + DEFINE N AS val IS NULL +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | | 1 + 3 | 30 | 0 +(3 rows) + +-- IS NOT NULL in DEFINE +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_null +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (NN+) + DEFINE NN AS val IS NOT NULL +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 1 + 2 | | 0 + 3 | 30 | 1 +(3 rows) + +DROP TABLE rpr_null; +-- Compound navigation: inner nav must be direct arg (not nested in expression) +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(v + FIRST(v)) > 0 +); +ERROR: row pattern navigation operation must be a direct argument of the outer navigation +LINE 6: DEFINE A AS PREV(v + FIRST(v)) > 0 + ^ +HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. +-- FIRST/LAST wrapping FIRST/LAST: prohibited +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS FIRST(FIRST(v)) > 0 +); +ERROR: FIRST and LAST cannot contain FIRST or LAST +LINE 6: DEFINE A AS FIRST(FIRST(v)) > 0 + ^ +HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. +-- Triple nesting: prohibited (3-level deep navigation) +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(FIRST(PREV(v))) > 0 +); +ERROR: cannot nest row pattern navigation more than two levels deep +LINE 6: DEFINE A AS PREV(FIRST(PREV(v))) > 0 + ^ +HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. +-- A navigation offset must be a run-time constant, not a navigation operation +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, FIRST(1)) > 0); +ERROR: row pattern navigation offset cannot contain a row pattern navigation operation +LINE 4: PATTERN (A+) DEFINE A AS PREV(v, FIRST(1)) > 0); + ^ +DETAIL: A navigation offset must be a run-time constant. +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, FIRST(1) + 1) > 0); +ERROR: row pattern navigation offset cannot contain a row pattern navigation operation +LINE 4: PATTERN (A+) DEFINE A AS PREV(v, FIRST(1) + 1) > 0); + ^ +DETAIL: A navigation offset must be a run-time constant. +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, NEXT(1, 0)) > 0); +ERROR: row pattern navigation offset cannot contain a row pattern navigation operation +LINE 4: PATTERN (A+) DEFINE A AS PREV(v, NEXT(1, 0)) > 0); + ^ +DETAIL: A navigation offset must be a run-time constant. +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(FIRST(v), LAST(1)) > 0); +ERROR: row pattern navigation offset cannot contain a row pattern navigation operation +LINE 4: PATTERN (A+) DEFINE A AS PREV(FIRST(v), LAST(1)) > 0); + ^ +DETAIL: A navigation offset must be a run-time constant. +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, FIRST(v)) > 0); +ERROR: row pattern navigation offset cannot contain a row pattern navigation operation +LINE 4: PATTERN (A+) DEFINE A AS PREV(v, FIRST(v)) > 0); + ^ +DETAIL: A navigation offset must be a run-time constant. +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS NEXT(v, PREV(v, 1)) > 0); +ERROR: row pattern navigation offset cannot contain a row pattern navigation operation +LINE 4: PATTERN (A+) DEFINE A AS NEXT(v, PREV(v, 1)) > 0); + ^ +DETAIL: A navigation offset must be a run-time constant. +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(FIRST(v, LAST(1)), 2) > 0); +ERROR: cannot nest row pattern navigation more than two levels deep +LINE 4: PATTERN (A+) DEFINE A AS PREV(FIRST(v, LAST(1)), 2) > 0)... + ^ +HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, FIRST(1::bigint)) > 0); +ERROR: row pattern navigation offset cannot contain a row pattern navigation operation +LINE 4: PATTERN (A+) DEFINE A AS PREV(v, FIRST(1::bigint)) > 0); + ^ +DETAIL: A navigation offset must be a run-time constant. +-- An unknown literal argument resolves to text; it must still reference a column +SELECT count(*) OVER w +FROM generate_series(1,5) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV('foo') = 'bar'); +ERROR: argument of row pattern navigation operation must include at least one column reference +LINE 4: PATTERN (A+) DEFINE A AS PREV('foo') = 'bar'); + ^ +SELECT count(*) OVER w +FROM generate_series(1,5) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV('foo')); +ERROR: argument of DEFINE must be type boolean, not type text +LINE 4: PATTERN (A+) DEFINE A AS PREV('foo')); + ^ +SELECT count(*) OVER w +FROM generate_series(1,5) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(NULL) IS NULL); +ERROR: argument of row pattern navigation operation must include at least one column reference +LINE 4: PATTERN (A+) DEFINE A AS PREV(NULL) IS NULL); + ^ +PREPARE rpr_navarg AS SELECT count(*) OVER w +FROM generate_series(1,5) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV($1) IS NULL); +ERROR: argument of row pattern navigation operation must include at least one column reference +LINE 4: PATTERN (A+) DEFINE A AS PREV($1) IS NULL); + ^ +-- An int2 offset is coerced to int8 like any implicit cast (same as plain 0) +SELECT count(*) OVER w +FROM generate_series(1,5) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, 0::smallint) = v); + count +------- + 5 + 0 + 0 + 0 + 0 +(5 rows) + +SELECT count(*) OVER w +FROM generate_series(1,5) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, 0) = v); + count +------- + 5 + 0 + 0 + 0 + 0 +(5 rows) + +-- ============================================================ +-- Window Deduplication Tests +-- ============================================================ +-- non-RPR and RPR windows with identical base frame are kept separate. +SELECT id, val, + first_value(id) OVER ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + ) AS fv_normal, + first_value(id) OVER w1 AS fv_rpr +FROM (VALUES (1, 10), (2, 20), (3, 30), (4, 40)) AS t(id, val) +WINDOW w1 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 10 +); + id | val | fv_normal | fv_rpr +----+-----+-----------+-------- + 1 | 10 | 1 | + 2 | 20 | 2 | 2 + 3 | 30 | 3 | + 4 | 40 | 4 | +(4 rows) + +-- ============================================================ +-- Pattern Optimization Tests +-- ============================================================ +-- Tests for pattern optimization +-- Use EXPLAIN to verify optimized pattern (shown as "Pattern: ...") +CREATE TABLE rpr_plan (id INT, val INT); +INSERT INTO rpr_plan VALUES + (1, 10), (2, 20), (3, 30), (4, 40), (5, 50), + (6, 60), (7, 70), (8, 80), (9, 90), (10, 100); +-- Consecutive VAR merge: A A A -> a{3} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A A A) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{3} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Consecutive VAR merge: A{2} A{3} -> a{5} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2} A{3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{5} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Consecutive VAR merge: A+ A* -> a+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ A*) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Consecutive VAR merge: A A+ -> a{2,} +-- where a finite prev (A{1,1}) meets an infinite child (A+). +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A A+) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Consecutive VAR merge at the boundary: A{1073741823,} A{1073741823,} -> +-- a{2147483646,}. The min sum 2147483646 = INT32_MAX - 1 is the largest +-- still-finite bound, so the merge proceeds; a sum of exactly INF instead +-- falls back (see the Optimization Fallback Tests). +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1073741823,} A{1073741823,}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2147483646,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B){5}) ((A B){10})) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b){15} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Consecutive GROUP merge with unbounded: (A B)+ (A B)+ -> (a b){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ (A B)+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b'){2,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,} +-- Where a finite prev ((A B){2,2}) meets an infinite child ((A B)+). +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){2} (A B)+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b'){3,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Consecutive GROUP merge at the boundary: (A B){1073741823,} (A B){1073741823,} +-- -> (a b){2147483646,}. The min sum INT32_MAX - 1 is still finite, so the +-- merge proceeds; a sum of exactly INF instead falls back (see the +-- Optimization Fallback Tests). +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){1073741823,} (A B){1073741823,}) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b'){2147483646,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- PREFIX merge: A B (A B)+ -> (a b){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (A B)+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b'){2,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- PREFIX and SUFFIX merge: A B (A B)+ A B -> (a b){3,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (A B)+ A B) DEFINE A AS val <= 40, B AS val > 40); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b'){3,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Flatten nested: A ((B) (C)) -> a b c +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A ((B) (C))) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Data execution: SEQ flatten produces correct results +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A ((B) (C))) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- ALT flatten: (A | (B | C))+ -> (a | b | c)+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | (B | C))+) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b | c)+ + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- ALT deduplicate: (A | B | A) -> (a | b) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B | A)+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b)+ + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Data execution: ALT dedup produces correct results +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | A)+) DEFINE A AS val <= 50, B AS val > 50); + id | val | cnt +----+-----+----- + 1 | 10 | 10 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Quantifier multiply: (A{2}){3} -> a{6} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}){3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{6} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Quantifier NO multiply: reluctant GROUP child (((A B){2}?){3}) stays nested +-- a reluctant quantifier on a GROUP is not subject to multiplication +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B){2}?){3}) DEFINE A AS val > 0, B AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a b){2}?){3} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Quantifier multiply control: greedy GROUP (((A B){2}){3}) -> (a b){6} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B){2}){3}) DEFINE A AS val > 0, B AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b){6} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9} +-- outer exact, child range - optimization applies +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,3}){3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{6,9} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Quantifier NO multiply: (A{2}){2,3} stays as (a{2}){2,3} +-- outer range - gaps would occur (4,6 not 4,5,6), no optimization +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}){2,3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2}){2,3} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Quantifier NO multiply: (A{2}){2,} stays as (a{2}){2,} +-- outer unbounded - gaps would occur (4,6,8,... not 4,5,6,...), no optimization +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}){2,}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2}'){2,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Quantifier multiply: (A){2,} -> a{2,} +-- child exact 1 - no gaps, optimization applies +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A){2,}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Quantifier multiply: (A)+ -> a+ +-- child exact 1 - no gaps, optimization applies +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A)+) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Quantifier NO multiply: (A{2}){3,5} stays as (a{2}){3,5} +-- outer range, child exact > 1 - gaps would occur (6,8,10 not 6,7,8,9,10) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}){3,5}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2}){3,5} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Quantifier multiply: (A{2,3}){2,3} -> a{4,9} +-- outer range, child range: counts [4,6] U [6,9] = [4,9] are contiguous, so it folds +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,3}){2,3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{4,9} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Quantifier NO multiply: (A{4,5}){2,3} stays as (a{4,5}){2,3} +-- outer range, child range with a gap: [8,10] U [12,15] misses 11 +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{4,5}){2,3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{4,5}){2,3} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Nested unbounded: (A*)* -> a* +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A*)*) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a*" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Nested unbounded: (A+)* -> a* +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+)*) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a*" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Nested unbounded: (A+)+ -> a+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+)+) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Quantifier multiply with an unbounded child: an exact outer count (m == n) +-- always folds regardless of the child's max - (A+){3} -> a{3,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+){3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{3,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- (A{2,}){3} -> a{6,} (m == n, unbounded child with min 2) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,}){3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{6,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- (A+){2,4} -> a{2,} (outer range, unbounded child: every interval reaches INF, +-- so they always touch) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+){2,4}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- (A{2,3}){2,4} -> a{4,12} (outer range x child range, contiguous: +-- [4,6] U [6,9] U [8,12] = [4,12]) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,3}){2,4}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{4,12} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Skippable outer (min 0) folds only when the zero case connects to the child +-- range: (A{1,3})? -> a{0,3} (child min <= 1, so {0} U [1,3] = [0,3] is contiguous) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{1,3})?) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{0,3} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Quantifier NO multiply: (A{2,3})? stays as (a{2,3})? +-- min 0 with child min >= 2: {0} U [2,3] leaves 1 unreachable (intervals touch but +-- the zero case does not connect) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,3})?) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2,3})? + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Quantifier NO multiply: (A{3,4})? stays as (a{3,4})? +-- min 0 with child min >= 2: {0} U [3,4] leaves 1,2 unreachable +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{3,4})?) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{3,4})? + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Unwrap GROUP{1,1}: (A) -> a +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A)) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Unwrap GROUP{1,1}: (A B) -> a b +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Combined optimization: A A (B B)+ B B C C C -> a{2} (b{2}){2,} c{3} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A A (B B)+ B B C C C) + DEFINE A AS val <= 20, B AS val > 20 AND val <= 70, C AS val > 70); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2} (b{2}){2,} c{3} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Consecutive GROUP merge with unbounded: (A+) (A+) -> a{2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+) (A+)) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Consecutive GROUP merge finite: (A{10}){20} -> a{200} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{10}){20}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{200} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Different GROUP prevents merge: (A B){2} (C D){3} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){2} (C D){3}) + DEFINE A AS val <= 25, B AS val > 25 AND val <= 50, + C AS val > 50 AND val <= 75, D AS val > 75); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b){2} (c d){3} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Different children count prevents merge: (A B)+ (A B C)+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ (A B C)+) + DEFINE A AS val <= 33, B AS val > 33 AND val <= 66, C AS val > 66); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b')+" (a b c)+ + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- PREFIX only merge: A B (A B)+ -> (a b){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (A B)+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b'){2,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- SUFFIX only merge: (A B)+ A B -> (a b){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ A B) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b'){2,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Multiple SUFFIX absorption with skipUntil: (A B)+ A B A B C +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ A B A B C) + DEFINE A AS val <= 50, B AS val > 50 AND val <= 75, C AS val > 75); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b'){3,}" c + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- PREFIX merge with remaining prefix: A B C D (C D)+ -> A B (C D) {2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B C D (C D)+) + DEFINE A AS val <= 25, B AS val > 25 AND val <= 50, + C AS val > 50 AND val <= 75, D AS val > 75); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b (c d){2,} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- cannot merge, prefix is different +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w +FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +PATTERN (A B C D C (C D) +) +DEFINE A AS val <= 25, B AS val > 25, + C AS val > 50, D AS val > 75); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c d c (c d)+ + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- PREFIX merge with quantifiers: A B* (A B*)+ -> (a b*){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B* (A B*)+) + DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b*){2,} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- PREFIX merge with multiple quantifiers: A+ B* C? (A+ B* C?)+ -> (a+ b* c?){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ B* C? (A+ B* C?)+) + DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+" b* c?){2,} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- SUFFIX merge with quantifiers: (A B*)+ A B* -> (a b*){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B*)+ A B*) + DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b*){2,} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Unwrap GROUP{1,1}: ((A | B | C)) -> (a | b | c) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B | C)) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b | c) + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Data execution: GROUP unwrap produces correct results +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A | B | C)) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + id | val | cnt +----+-----+----- + 1 | 10 | 1 + 2 | 20 | 1 + 3 | 30 | 1 + 4 | 40 | 1 + 5 | 50 | 1 + 6 | 60 | 1 + 7 | 70 | 1 + 8 | 80 | 1 + 9 | 90 | 1 + 10 | 100 | 1 +(10 rows) + +-- Reluctant optimization bypass: VAR merge +-- A+? A stays as a+? a (greedy A+ A merges to a{2,}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+? A) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+? a + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Reluctant optimization bypass: GROUP merge +-- (A B)+? (A B) stays separate (greedy merges to (a b){2,}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+? (A B)) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b)+? a b + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Reluctant optimization bypass: quantifier multiply (outer reluctant) +-- (A{2}){3}? stays as (a{2}){3}? (greedy merges to a{6}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}){3}?) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2}){3}? + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Reluctant optimization bypass: quantifier multiply (inner reluctant) +-- (A{2}?){3} stays as (a{2}?){3} (greedy merges to a{6}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}?){3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2}?){3} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Reluctant optimization bypass: PREFIX merge +-- A B (A B)+? stays separate (greedy merges to (a b){2,}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (A B)+?) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b (a b)+? + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Reluctant optimization bypass: SUFFIX merge +-- (A B)+? A B stays separate (greedy merges to (a b){2,}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+? A B) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b)+? a b + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- GROUP unwrap with quantifier propagation: (A)?? B -> a?? b +-- Single VAR child {1,1} receives GROUP's quantifier and reluctant +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A)?? B) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a?? b + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Reluctant preserved through ALT flatten +-- (A | (B | C))+? flattens to (a | b | c)+? - inner ALT flattened, reluctant kept +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | (B | C))+?) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b | c)+? + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Reluctant optimization bypass: absorption flags +-- A+? with SKIP PAST LAST ROW - no absorption markers (greedy A+ gets a+") +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+?) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+? + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Duplicate GROUP removal: ((A | B)+ | (A | B)+) -> (a | b)+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B)+ | (A | B)+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b)+ + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Consecutive VAR merge with zero-min: A* A+ -> a+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A* A+) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Consecutive VAR merge (4-element): A A{2} A+ A{3} -> a{7,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A A{2} A+ A{3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{7,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- PREFIX+SUFFIX merge (5-way): A B A B (A B)+ A B A B -> (a b){5,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B A B (A B)+ A B A B) + DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b'){5,}" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- PREFIX+SUFFIX merge (5-way): B A B (A B)+ A B A B -> b (a b){4,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (B A B (A B)+ A B A B) + DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: b (a b){4,} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Unwrap single-item ALT after dedup: (A | A)+ -> a+ +-- ALT dedup reduces to single-item, then GROUP unwrap +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | A)+) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- GROUP{1,1} to SEQ with flatten: ((A B)(C D)) -> a b c d +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B)(C D))) + DEFINE A AS val <= 25, B AS val > 25 AND val <= 50, + C AS val > 50 AND val <= 75, D AS val > 75); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c d + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Nested ALT pattern: ((A B) | C) D | A B C +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B) | C) D | A B C) + DEFINE A AS val <= 25, B AS val > 25 AND val <= 50, + C AS val > 50 AND val <= 75, D AS val > 75); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a b | c) d | a b c) + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Nested ALT with unbounded: ((A+ B) | C) D | A B C +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A+ B) | C) D | A B C) + DEFINE A AS val <= 25, B AS val > 25 AND val <= 50, + C AS val > 50 AND val <= 75, D AS val > 75); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a+" b | c) d | a b c) + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- ============================================================ +-- Absorption Flag Display Tests +-- ============================================================ +-- Tests absorption marker display in EXPLAIN output +-- Markers: ' = branch element, " = comparison point +-- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern) +-- Simple VAR: A+ -> a+" (comparison point) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + comparison) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B)+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b')+" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- ALT both absorbable: A+ | B+ -> (a+" | b+") +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ | B+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+" | b+") + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- ALT one absorbable: A+ | B -> (a+" | b) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ | B) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+" | b) + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Sequence with absorbable start: A+ B -> a+" b +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Complex nested: ((A+ B) | C) D | A B C - deeply nested ALT +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (((A+ B) | C) D | A B C) + DEFINE A AS val <= 30, B AS val <= 60, C AS val <= 80, D AS val > 80); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a+" b | c) d | a b c) + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Nested unbounded: (A+ | B)+ -> (a+" | b)+ (first iteration absorbable) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ | B)+) + DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+" | b)+ + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- ALT inside unbounded GROUP: (A+ B | A B)* -> (a+" b | a b)* (first iteration absorbable) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ B | A B)*) + DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+" b | a b)* + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Fixed-length group absorbable: (A{2} B{3})+ -> (a{2}' b{3}'){2,}" +-- All children have min == max, equivalent to unrolling to {1,1} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A{2} B{3})+) + DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2}' b{3}')+" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Nested fixed-length group: (A (B C){2} D)+ -> absorbable +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A (B C){2} D)+) + DEFINE A AS val <= 20, B AS val <= 40, C AS val <= 60, D AS val > 60); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' (b' c'){2}' d')+" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Nested fixed-length with inner quantifier: ((A{2} B{3}){2})+ -> absorbable +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (((A{2} B{3}){2})+) + DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a{2}' b{3}'){2}')+" + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Non-absorbable fixed-length: (A B{2,5})+ -> no markers (min != max) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B{2,5})+) + DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b{2,5})+ + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Non-absorbable fixed-length: (A B?)+ -> no markers (min != max) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B?)+) + DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b?)+ + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A B+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Non-absorbable (no unbounded branch): (A | B){2,} -> (a | b){2,} (no markers) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A | B){2,}) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b){2,} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Non-absorbable (SKIP TO NEXT ROW): A+ -> a+ (no markers) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW PATTERN (A+) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Non-absorbable (limited frame): A+ -> a+ (no markers) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0); + QUERY PLAN +---------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND '10'::bigint FOLLOWING) + Pattern: a+ + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(7 rows) + +-- Reluctant {1}? quantifier deparse +-- A{1}? is a reluctant {1,1} quantifier. The deparse code must +-- output "{1}" explicitly to disambiguate from a bare "?" quantifier +-- (which would mean {0,1}). +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM rpr_plan +WINDOW w AS ( + ORDER BY val + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1}? B) + DEFINE A AS val > 0, B AS val > 0 +); + QUERY PLAN +-------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY val ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{1}? b + Nav Mark Lookback: 0 + -> Sort + Sort Key: val + -> Seq Scan on rpr_plan +(7 rows) + +-- ============================================================ +-- Absorption Analysis Tests +-- ============================================================ +-- Tests context absorption optimization (O(n^2) -> O(n)) +-- Files: rpr.c (computeAbsorbability) +-- Simple Absorbable Pattern: A+ B +-- Pattern starts with unbounded VAR +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS val <= 50, B AS val > 50 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 6 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Absorbable GROUP Pattern: (A B)+ C +-- Pattern starts with unbounded GROUP +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+ C) + DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Non-Absorbable: Unbounded Not at Start +-- Pattern: A B+ (unbounded not at start) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS val <= 50, B AS val > 50 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 6 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- ALT with Absorbable Branches +-- Pattern: (A+ | B+) C - both branches absorbable +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ | B+) C) + DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 4 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- ALT with Mixed Branches +-- Pattern: (A+ | B C) - only first branch absorbable +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ | B C)+) + DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 2 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Non-Absorbable: ALT Inside GROUP +-- Pattern: (A | B){2,} - ALT inside unbounded GROUP +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B){2,}) + DEFINE A AS val <= 50, B AS val > 50 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 10 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Non-Absorbable: Nested Unbounded +-- Pattern: ((A B)+ C)+ - nested GROUP structure +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A B)+ C)+) + DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Non-Absorbable: Unbounded Element Inside GROUP +-- Pattern: (A B+){2,} - unbounded inside GROUP +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B+){2,}) + DEFINE A AS val <= 50, B AS val > 50 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Runtime Conditions: SKIP TO NEXT ROW +-- Absorption disabled with SKIP TO NEXT ROW +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE A AS val <= 50, B AS val > 50 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 6 + 2 | 20 | 5 + 3 | 30 | 4 + 4 | 40 | 3 + 5 | 50 | 2 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Runtime Conditions: Limited Frame +-- Absorption disabled with limited frame end +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 5 FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS val <= 50, B AS val > 50 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 6 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- ALT Non-Absorbable Branch Match: A+ B | C +-- C match on the non-absorbable branch (id=2, id=5) must survive absorption of +-- the dominating A+ run, which never completes a match (B is never present) +WITH test_nonabsorbable_branch AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A', 'C']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A', 'C']), + (6, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nonabsorbable_branch +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} | | + 2 | {A,C} | 2 | 2 + 3 | {A} | | + 4 | {A} | | + 5 | {A,C} | 5 | 5 + 6 | {A} | | +(6 rows) + +-- ALT Both Branches Absorbable: A+ C | B+ +-- A+ C never completes (C absent) so its A+ run keeps expanding and dominates; +-- a finalized B+ match on the other branch (id=1, id=6) must survive absorption +WITH test_absorbable_branches AS ( + SELECT * FROM (VALUES + (1, ARRAY['A', 'B']), + (2, ARRAY['A', 'B']), + (3, ARRAY['A', 'B']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['A', 'B']), + (7, ARRAY['A', 'B']), + (8, ARRAY['A', 'B']), + (9, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_absorbable_branches +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ C | B+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 3 + 2 | {A,B} | | + 3 | {A,B} | | + 4 | {A} | | + 5 | {A} | | + 6 | {A,B} | 6 | 8 + 7 | {A,B} | | + 8 | {A,B} | | + 9 | {A} | | +(9 rows) + +-- ============================================================ +-- Edge Case Tests +-- ============================================================ +-- Tests boundary conditions and complex scenarios +-- Empty Match Prevention +-- Pattern that could match empty: A* +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A*) + DEFINE A AS val > 1000 -- Never matches +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- All Rows Match +-- Pattern where every row matches +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val >= 0 -- Always true +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 10 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Large Quantifiers +-- Pattern: A{100} (large exact quantifier) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{100}) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Pattern: A{10,20} (large range quantifier) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{10,20}) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 10 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Complex Multi-Level Nesting +-- Pattern: (((A B) | C)+ D)+ +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((((A B) | C)+ D)+) + DEFINE A AS val <= 20, B AS val > 20 AND val <= 40, + C AS val > 40 AND val <= 60, D AS val > 60 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 3 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Long Alternation Chain +-- Pattern: A | B | C | D | E (5-way ALT) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A | B | C | D | E) + DEFINE A AS val = 10, B AS val = 30, C AS val = 50, + D AS val = 70, E AS val = 90 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 1 + 2 | 20 | 0 + 3 | 30 | 1 + 4 | 40 | 0 + 5 | 50 | 1 + 6 | 60 | 0 + 7 | 70 | 1 + 8 | 80 | 0 + 9 | 90 | 1 + 10 | 100 | 0 +(10 rows) + +-- Long Sequence +-- Pattern: A B C D E F G H (8-element SEQ) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B C D E F G H) + DEFINE A AS val >= 10, B AS val >= 20, C AS val >= 30, + D AS val >= 40, E AS val >= 50, F AS val >= 60, + G AS val >= 70, H AS val >= 80 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 8 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Interleaved Quantifiers +-- Pattern: A{2} B+ C{3,5} D* E{1,} +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2} B+ C{3,5} D* E{1,}) + DEFINE A AS val > 0, B AS val > 0, C AS val > 0, + D AS val > 0, E AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 10 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- ============================================================ +-- Optimization Fallback Tests +-- ============================================================ +-- Tests for optimization edge cases and fallback behavior +CREATE TABLE rpr_fallback (id INT, val INT); +INSERT INTO rpr_fallback VALUES (1, 10), (2, 20); +-- Test: min quantifier overflow causes optimization fallback (min == max case) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2000000000}){2}) + DEFINE A AS val > 0 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2000000000}){2} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(7 rows) + +-- Expected: Fallback - pattern not merged due to min overflow (4000000000 > INT32_MAX) +-- Test: max-only quantifier overflow causes optimization fallback +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{1,2000000000}){2}) + DEFINE A AS val > 0 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{1,2000000000}){2} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(7 rows) + +-- Expected: Fallback - min OK (2*1=2), but max overflow (2*2000000000 > INT32_MAX) +-- Test: max quantifier exceeds valid range (2147483647 = INT_MAX, limit is 2147483646) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2000000000,2147483647}){2}) + DEFINE A AS val > 0 +); +ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1 +LINE 6: PATTERN ((A{2000000000,2147483647}){2}) + ^ +-- Test: nested unbounded with large min causes overflow fallback +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2000000000,}){2000000000,}) + DEFINE A AS val > 0 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2000000000,}"){2000000000,} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(7 rows) + +-- Expected: Fallback - min overflow (2000000000 * 2000000000 > INT32_MAX) +-- Test: prefix mismatch causes optimization fallback +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (C D)+) + DEFINE A AS val > 0, B AS val > 5, C AS val > 10, D AS val > 15 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b (c d)+ + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(7 rows) + +-- Expected: Fallback - prefix elements don't match GROUP content +-- Test: consecutive VAR merge whose min sum is exactly INF causes fallback. +-- 1073741824 + 1073741823 = 2147483647 = INT32_MAX = RPR_QUANTITY_INF. +-- Merging would yield a VAR with min == INF, so the merge must fall back and +-- leave the two VARs unmerged (mirrors the multiply path's >= INF guard). +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1073741824,} A{1073741823,}) + DEFINE A AS val > 0 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{1073741824,}" a{1073741823,} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(7 rows) + +-- Expected: Fallback - VARs not merged (min sum 2147483647 == INF) +-- Test: consecutive GROUP merge whose min sum is exactly INF causes fallback. +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){1073741824,} (A B){1073741823,}) + DEFINE A AS val > 0, B AS val > 5 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b'){1073741824,}" (a b){1073741823,} + Nav Mark Lookback: 0 + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(7 rows) + +-- Expected: Fallback - GROUPs not merged (min sum 2147483647 == INF) +DROP TABLE rpr_fallback; +-- ============================================================ +-- Planner Integration Tests +-- ============================================================ +-- Tests full planning pipeline and WindowAgg plan node creation +-- Files: planner.c, createplan.c +CREATE TABLE rpr_planner (id INT, category VARCHAR(10), val INT); +INSERT INTO rpr_planner VALUES + (1, 'A', 10), (2, 'A', 20), (3, 'A', 30), + (4, 'B', 40), (5, 'B', 50), (6, 'B', 60), + (7, 'C', 70), (8, 'C', 80), (9, 'C', 90); +-- Multiple Window Functions in Same Query +SELECT id, category, val, + COUNT(*) OVER w1 as cnt1, + COUNT(*) OVER w2 as cnt2 +FROM rpr_planner +WINDOW w1 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +), +w2 AS ( + PARTITION BY category + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (B+) + DEFINE B AS val >= 40 +) +ORDER BY id; + id | category | val | cnt1 | cnt2 +----+----------+-----+------+------ + 1 | A | 10 | 9 | 0 + 2 | A | 20 | 0 | 0 + 3 | A | 30 | 0 | 0 + 4 | B | 40 | 0 | 3 + 5 | B | 50 | 0 | 0 + 6 | B | 60 | 0 | 0 + 7 | C | 70 | 0 | 3 + 8 | C | 80 | 0 | 0 + 9 | C | 90 | 0 | 0 +(9 rows) + +-- Window Function with PARTITION BY +SELECT id, category, val, + COUNT(*) OVER w as cnt +FROM rpr_planner +WINDOW w AS ( + PARTITION BY category + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY category, id; + id | category | val | cnt +----+----------+-----+----- + 1 | A | 10 | 3 + 2 | A | 20 | 0 + 3 | A | 30 | 0 + 4 | B | 40 | 3 + 5 | B | 50 | 0 + 6 | B | 60 | 0 + 7 | C | 70 | 3 + 8 | C | 80 | 0 + 9 | C | 90 | 0 +(9 rows) + +-- Window Function with Complex ORDER BY +SELECT id, category, val, + COUNT(*) OVER w as cnt +FROM rpr_planner +WINDOW w AS ( + ORDER BY category DESC, val ASC + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY category DESC, val ASC; + id | category | val | cnt +----+----------+-----+----- + 7 | C | 70 | 9 + 8 | C | 80 | 0 + 9 | C | 90 | 0 + 4 | B | 40 | 0 + 5 | B | 50 | 0 + 6 | B | 60 | 0 + 1 | A | 10 | 0 + 2 | A | 20 | 0 + 3 | A | 30 | 0 +(9 rows) + +-- Named Window Reference +SELECT id, category, val, + COUNT(*) OVER w as cnt +FROM rpr_planner +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + id | category | val | cnt +----+----------+-----+----- + 1 | A | 10 | 9 + 2 | A | 20 | 0 + 3 | A | 30 | 0 + 4 | B | 40 | 0 + 5 | B | 50 | 0 + 6 | B | 60 | 0 + 7 | C | 70 | 0 + 8 | C | 80 | 0 + 9 | C | 90 | 0 +(9 rows) + +-- Inline Window Definition +SELECT id, category, val, + COUNT(*) OVER ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) as cnt +FROM rpr_planner +ORDER BY id; + id | category | val | cnt +----+----------+-----+----- + 1 | A | 10 | 9 + 2 | A | 20 | 0 + 3 | A | 30 | 0 + 4 | B | 40 | 0 + 5 | B | 50 | 0 + 6 | B | 60 | 0 + 7 | C | 70 | 0 + 8 | C | 80 | 0 + 9 | C | 90 | 0 +(9 rows) + +-- Window with Aggregate Functions +SELECT category, + COUNT(*) OVER w as window_cnt, + COUNT(*) as agg_cnt +FROM rpr_planner +WINDOW w AS ( + PARTITION BY category + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +GROUP BY category +ORDER BY category; +ERROR: syntax error at or near "GROUP" +LINE 12: GROUP BY category + ^ +-- (GROUP BY after WINDOW clause is not valid SQL syntax) +-- ============================================================ +-- Subquery and CTE Tests +-- Files: planner.c, prepjointree.c +-- ============================================================ +-- Tests RPR with subqueries and CTEs +-- RPR in Subquery (FROM clause) +SELECT * FROM ( + SELECT id, category, val, + COUNT(*) OVER w as cnt + FROM rpr_planner + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) sub +WHERE cnt > 5 +ORDER BY id; + id | category | val | cnt +----+----------+-----+----- + 1 | A | 10 | 9 +(1 row) + +-- RPR with Subquery in WHERE +SELECT id, category, val, + COUNT(*) OVER w as cnt +FROM rpr_planner +WHERE val > (SELECT AVG(val) FROM rpr_planner) +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 50 +) +ORDER BY id; + id | category | val | cnt +----+----------+-----+----- + 6 | B | 60 | 4 + 7 | C | 70 | 0 + 8 | C | 80 | 0 + 9 | C | 90 | 0 +(4 rows) + +-- CTE with RPR +WITH rpr_cte AS ( + SELECT id, category, val, + COUNT(*) OVER w as cnt + FROM rpr_planner + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) +SELECT * FROM rpr_cte WHERE cnt > 5 ORDER BY id; + id | category | val | cnt +----+----------+-----+----- + 1 | A | 10 | 9 +(1 row) + +-- Multiple CTE References +WITH rpr_cte AS ( + SELECT id, category, val, + COUNT(*) OVER w as cnt + FROM rpr_planner + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) +SELECT c1.id, c1.cnt, c2.cnt as cnt2 +FROM rpr_cte c1 +JOIN rpr_cte c2 ON c1.id = c2.id +ORDER BY c1.id; + id | cnt | cnt2 +----+-----+------ + 1 | 9 | 9 + 2 | 0 | 0 + 3 | 0 | 0 + 4 | 0 | 0 + 5 | 0 | 0 + 6 | 0 | 0 + 7 | 0 | 0 + 8 | 0 | 0 + 9 | 0 | 0 +(9 rows) + +-- Nested CTEs +WITH cte1 AS ( + SELECT id, category, val FROM rpr_planner WHERE val > 30 +), +cte2 AS ( + SELECT id, category, val, + COUNT(*) OVER w as cnt + FROM cte1 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) +SELECT * FROM cte2 ORDER BY id; + id | category | val | cnt +----+----------+-----+----- + 4 | B | 40 | 6 + 5 | B | 50 | 0 + 6 | B | 60 | 0 + 7 | C | 70 | 0 + 8 | C | 80 | 0 + 9 | C | 90 | 0 +(6 rows) + +-- ============================================================ +-- JOIN Tests +-- Files: prepjointree.c, setrefs.c +-- ============================================================ +-- Tests RPR with JOINs and multiple table references +CREATE TABLE rpr_join1 (id INT, val1 INT); +CREATE TABLE rpr_join2 (id INT, val2 INT); +INSERT INTO rpr_join1 VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50); +INSERT INTO rpr_join2 VALUES (1, 100), (2, 200), (3, 300), (4, 400), (5, 500); +-- RPR After INNER JOIN +SELECT t1.id, t1.val1, t2.val2, + COUNT(*) OVER w as cnt +FROM rpr_join1 t1 +INNER JOIN rpr_join2 t2 ON t1.id = t2.id +WINDOW w AS ( + ORDER BY t1.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val1 + val2 > 100 +) +ORDER BY t1.id; + id | val1 | val2 | cnt +----+------+------+----- + 1 | 10 | 100 | 5 + 2 | 20 | 200 | 0 + 3 | 30 | 300 | 0 + 4 | 40 | 400 | 0 + 5 | 50 | 500 | 0 +(5 rows) + +-- RPR After LEFT JOIN +SELECT t1.id, t1.val1, t2.val2, + COUNT(*) OVER w as cnt +FROM rpr_join1 t1 +LEFT JOIN rpr_join2 t2 ON t1.id = t2.id +WINDOW w AS ( + ORDER BY t1.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val1 > 0 +) +ORDER BY t1.id; + id | val1 | val2 | cnt +----+------+------+----- + 1 | 10 | 100 | 5 + 2 | 20 | 200 | 0 + 3 | 30 | 300 | 0 + 4 | 40 | 400 | 0 + 5 | 50 | 500 | 0 +(5 rows) + +-- RPR with Multiple Tables in DEFINE +SELECT t1.id, t1.val1, t2.val2, + COUNT(*) OVER w as cnt +FROM rpr_join1 t1 +INNER JOIN rpr_join2 t2 ON t1.id = t2.id +WINDOW w AS ( + ORDER BY t1.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ B) + DEFINE A AS val1 > 20, + B AS val2 > 200 +) +ORDER BY t1.id; + id | val1 | val2 | cnt +----+------+------+----- + 1 | 10 | 100 | 0 + 2 | 20 | 200 | 0 + 3 | 30 | 300 | 3 + 4 | 40 | 400 | 0 + 5 | 50 | 500 | 0 +(5 rows) + +-- RPR After Cross Join +SELECT t1.id as id1, t2.id as id2, t1.val1, t2.val2, + COUNT(*) OVER w as cnt +FROM rpr_join1 t1 +CROSS JOIN rpr_join2 t2 +WHERE t1.id <= 2 AND t2.id <= 2 +WINDOW w AS ( + ORDER BY t1.id, t2.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val1 + val2 > 0 +) +ORDER BY t1.id, t2.id; + id1 | id2 | val1 | val2 | cnt +-----+-----+------+------+----- + 1 | 1 | 10 | 100 | 4 + 1 | 2 | 10 | 200 | 0 + 2 | 1 | 20 | 100 | 0 + 2 | 2 | 20 | 200 | 0 +(4 rows) + +-- Self-Join with RPR +SELECT id, val1, val1_next, + COUNT(*) OVER w as cnt +FROM (SELECT a.id, a.val1, b.val1 as val1_next + FROM rpr_join1 a + INNER JOIN rpr_join1 b ON a.id + 1 = b.id) sub +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (X+) + DEFINE X AS val1 < val1_next +) +ORDER BY id; + id | val1 | val1_next | cnt +----+------+-----------+----- + 1 | 10 | 20 | 4 + 2 | 20 | 30 | 0 + 3 | 30 | 40 | 0 + 4 | 40 | 50 | 0 +(4 rows) + +DROP TABLE rpr_join1, rpr_join2; +-- ============================================================ +-- Complex Expression Tests +-- Files: createplan.c, setrefs.c +-- ============================================================ +-- Tests complex target list expressions +CREATE TABLE rpr_target (id INT, val INT); +INSERT INTO rpr_target VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50); +-- Expressions in Target List +SELECT id, + val * 2 as doubled, + val + 10 as added, + COUNT(*) OVER w as cnt +FROM rpr_target +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + id | doubled | added | cnt +----+---------+-------+----- + 1 | 20 | 20 | 5 + 2 | 40 | 30 | 0 + 3 | 60 | 40 | 0 + 4 | 80 | 50 | 0 + 5 | 100 | 60 | 0 +(5 rows) + +-- CASE Expression in Target List +SELECT id, val, + CASE + WHEN val < 30 THEN 'low' + WHEN val < 50 THEN 'medium' + ELSE 'high' + END as category, + COUNT(*) OVER w as cnt +FROM rpr_target +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | category | cnt +----+-----+----------+----- + 1 | 10 | low | 5 + 2 | 20 | low | 0 + 3 | 30 | medium | 0 + 4 | 40 | medium | 0 + 5 | 50 | high | 0 +(5 rows) + +-- Subquery in Target List +SELECT id, val, + (SELECT MAX(val) FROM rpr_target) as max_val, + COUNT(*) OVER w as cnt +FROM rpr_target +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | max_val | cnt +----+-----+---------+----- + 1 | 10 | 50 | 5 + 2 | 20 | 50 | 0 + 3 | 30 | 50 | 0 + 4 | 40 | 50 | 0 + 5 | 50 | 50 | 0 +(5 rows) + +-- Function Calls in Target List +SELECT id, val, + COALESCE(val, 0) as coalesced, + ABS(val - 30) as distance, + COUNT(*) OVER w as cnt +FROM rpr_target +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | coalesced | distance | cnt +----+-----+-----------+----------+----- + 1 | 10 | 10 | 20 | 5 + 2 | 20 | 20 | 10 | 0 + 3 | 30 | 30 | 0 | 0 + 4 | 40 | 40 | 10 | 0 + 5 | 50 | 50 | 20 | 0 +(5 rows) + +-- Column Aliases and References +SELECT id as row_id, + val as value, + COUNT(*) OVER w as cnt +FROM rpr_target +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY row_id; + row_id | value | cnt +--------+-------+----- + 1 | 10 | 5 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 +(5 rows) + +DROP TABLE rpr_target; +-- ============================================================ +-- Set Operations Tests +-- Files: planner.c +-- ============================================================ +-- Tests RPR with UNION, INTERSECT, EXCEPT +CREATE TABLE rpr_set1 (id INT, val INT); +CREATE TABLE rpr_set2 (id INT, val INT); +INSERT INTO rpr_set1 VALUES (1, 10), (2, 20), (3, 30); +INSERT INTO rpr_set2 VALUES (2, 20), (3, 30), (4, 40); +-- UNION with RPR +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set1 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +UNION +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set2 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 + 2 | 20 | 3 + 3 | 30 | 0 + 4 | 40 | 0 +(5 rows) + +-- UNION ALL with RPR +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set1 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +UNION ALL +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set2 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +ORDER BY id, val; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 + 2 | 20 | 3 + 3 | 30 | 0 + 3 | 30 | 0 + 4 | 40 | 0 +(6 rows) + +-- INTERSECT with RPR +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set1 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +INTERSECT +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set2 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +ORDER BY id; + id | val | cnt +----+-----+----- + 3 | 30 | 0 +(1 row) + +-- EXCEPT with RPR +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set1 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +EXCEPT +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set2 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 3 + 2 | 20 | 0 +(2 rows) + +DROP TABLE rpr_set1, rpr_set2; +-- ============================================================ +-- Sorting and Grouping Tests +-- Files: planner.c, createplan.c +-- ============================================================ +-- Tests RPR interaction with sorting and grouping +CREATE TABLE rpr_sort (id INT, category VARCHAR(10), val INT); +INSERT INTO rpr_sort VALUES + (1, 'A', 30), (2, 'B', 20), (3, 'A', 10), + (4, 'B', 40), (5, 'A', 50), (6, 'B', 60); +-- RPR with GROUP BY (aggregate in DEFINE -> ERROR before GROUP BY interaction) +SELECT category, + COUNT(*) as group_cnt, + MAX(val) as max_val, + COUNT(*) OVER w as window_cnt +FROM rpr_sort +GROUP BY category +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS COUNT(*) > 0 +) +ORDER BY category; +ERROR: aggregate functions are not allowed in DEFINE +LINE 11: DEFINE A AS COUNT(*) > 0 + ^ +-- RPR with HAVING (same aggregate-in-DEFINE error) +SELECT category, + COUNT(*) as group_cnt, + COUNT(*) OVER w as window_cnt +FROM rpr_sort +GROUP BY category +HAVING COUNT(*) > 2 +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS COUNT(*) > 0 +) +ORDER BY category; +ERROR: aggregate functions are not allowed in DEFINE +LINE 11: DEFINE A AS COUNT(*) > 0 + ^ +-- RPR with DISTINCT +SELECT DISTINCT category, + COUNT(*) OVER w as cnt +FROM rpr_sort +WINDOW w AS ( + PARTITION BY category + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY category; + category | cnt +----------+----- + A | 3 + A | 0 + B | 0 + B | 3 +(4 rows) + +-- RPR with ORDER BY (different from window ORDER BY) +SELECT id, category, val, + COUNT(*) OVER w as cnt +FROM rpr_sort +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY val DESC; + id | category | val | cnt +----+----------+-----+----- + 6 | B | 60 | 0 + 5 | A | 50 | 0 + 4 | B | 40 | 0 + 1 | A | 30 | 6 + 2 | B | 20 | 0 + 3 | A | 10 | 0 +(6 rows) + +-- RPR with LIMIT and OFFSET +SELECT id, category, val, + COUNT(*) OVER w as cnt +FROM rpr_sort +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id +LIMIT 3 OFFSET 1; + id | category | val | cnt +----+----------+-----+----- + 2 | B | 20 | 0 + 3 | A | 10 | 0 + 4 | B | 40 | 0 +(3 rows) + +DROP TABLE rpr_sort; +-- SQL function inlining: $1 in DEFINE must be substituted by +-- substitute_actual_parameters_in_from via query_tree_mutator. +CREATE TABLE rpr_srf_t (v int); +INSERT INTO rpr_srf_t SELECT generate_series(1, 5); +CREATE FUNCTION rpr_srf_f(threshold int) +RETURNS TABLE (v int, cnt bigint) +LANGUAGE sql STABLE AS $$ + SELECT v::int, count(*) OVER w + FROM rpr_srf_t + WINDOW w AS ( + ORDER BY v + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > $1 + ) +$$; +SELECT v, cnt FROM rpr_srf_f(3) ORDER BY v; + v | cnt +---+----- + 1 | 0 + 2 | 0 + 3 | 0 + 4 | 2 + 5 | 0 +(5 rows) + +DROP TABLE rpr_srf_t; +DROP FUNCTION rpr_srf_f(int); +DROP TABLE rpr_planner; +-- ============================================================ +-- Stress Tests +-- ============================================================ +-- Edge cases and stress scenarios +CREATE TABLE rpr_stress (id INT, val INT); +INSERT INTO rpr_stress SELECT i, i * 10 FROM generate_series(1, 20) i; +-- Very Long Query with Many Windows +SELECT id, val, + COUNT(*) OVER w1 as cnt1, + COUNT(*) OVER w2 as cnt2, + COUNT(*) OVER w3 as cnt3 +FROM rpr_stress +WINDOW w1 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +), +w2 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (B+) + DEFINE B AS val > 50 +), +w3 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C+) + DEFINE C AS val > 100 +) +ORDER BY id; + id | val | cnt1 | cnt2 | cnt3 +----+-----+------+------+------ + 1 | 10 | 20 | 0 | 0 + 2 | 20 | 0 | 0 | 0 + 3 | 30 | 0 | 0 | 0 + 4 | 40 | 0 | 0 | 0 + 5 | 50 | 0 | 0 | 0 + 6 | 60 | 0 | 15 | 0 + 7 | 70 | 0 | 0 | 0 + 8 | 80 | 0 | 0 | 0 + 9 | 90 | 0 | 0 | 0 + 10 | 100 | 0 | 0 | 0 + 11 | 110 | 0 | 0 | 10 + 12 | 120 | 0 | 0 | 0 + 13 | 130 | 0 | 0 | 0 + 14 | 140 | 0 | 0 | 0 + 15 | 150 | 0 | 0 | 0 + 16 | 160 | 0 | 0 | 0 + 17 | 170 | 0 | 0 | 0 + 18 | 180 | 0 | 0 | 0 + 19 | 190 | 0 | 0 | 0 + 20 | 200 | 0 | 0 | 0 +(20 rows) + +-- Deeply Nested Subqueries with RPR +SELECT * FROM ( + SELECT * FROM ( + SELECT * FROM ( + SELECT id, val, + COUNT(*) OVER w as cnt + FROM rpr_stress + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) + ) sub1 + ) sub2 +) sub3 +WHERE cnt > 10 +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 20 +(1 row) + +-- Complex Expression in DEFINE Clause +SELECT id, val, + COUNT(*) OVER w as cnt +FROM rpr_stress +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ B) + DEFINE A AS (val % 3 = 0 OR val % 5 = 0), + B AS (val * 2 > 100 AND val / 2 < 100) +) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 19 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 + 11 | 110 | 0 + 12 | 120 | 0 + 13 | 130 | 0 + 14 | 140 | 0 + 15 | 150 | 0 + 16 | 160 | 0 + 17 | 170 | 0 + 18 | 180 | 0 + 19 | 190 | 0 + 20 | 200 | 0 +(20 rows) + +-- Window with No Matching Rows +SELECT id, val, + COUNT(*) OVER w as cnt +FROM rpr_stress +WHERE val > 1000 -- No rows match +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- +(0 rows) + +-- Window on Single Row +SELECT id, val, + COUNT(*) OVER w as cnt +FROM rpr_stress +WHERE id = 10 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + id | val | cnt +----+-----+----- + 10 | 100 | 1 +(1 row) + +DROP TABLE rpr_stress; +-- ============================================================ +-- Error Limit Tests +-- ============================================================ +-- Tests for error conditions in rpr.c +CREATE TABLE rpr_errors (id INT, val INT); +INSERT INTO rpr_errors VALUES (1, 10), (2, 20); +-- Test: DEFINE variable not in PATTERN (error) +SELECT id, val, COUNT(*) OVER w FROM rpr_errors +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE + B AS TRUE +); +ERROR: DEFINE variable "b" is not used in PATTERN +LINE 7: B AS TRUE + ^ +-- Expected: Error - B is not used in PATTERN +-- Test: 240 variables in PATTERN and DEFINE (boundary - should succeed) +SELECT COUNT(*) OVER w FROM rpr_errors +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 + V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 + V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 + V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 + V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 + V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 + V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 + V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 + V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 + V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 + V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 + V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240) + DEFINE + V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0, + V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0, + V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0, + V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0, + V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0, + V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0, + V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0, + V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0, + V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0, + V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0, + V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0, + V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0, + V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0, + V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0, + V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0, + V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0, + V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0, + V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0, + V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0, + V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0, + V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0, + V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0, + V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0, + V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0 +); + count +------- + 0 + 0 +(2 rows) + +-- Expected: Success - exactly at RPR_VARID_MAX boundary +-- ERROR: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE) +SELECT COUNT(*) OVER w FROM rpr_errors +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 + V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 + V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 + V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 + V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 + V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 + V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 + V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 + V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 + V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 + V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 + V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 + V241) + DEFINE + V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0, + V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0, + V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0, + V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0, + V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0, + V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0, + V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0, + V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0, + V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0, + V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0, + V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0, + V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0, + V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0, + V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0, + V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0, + V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0, + V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0, + V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0, + V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0, + V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0, + V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0, + V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0, + V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0, + V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0 +); +ERROR: too many row pattern variables +LINE 17: V241) + ^ +DETAIL: The maximum number of row pattern variables is 240. +-- Test: Pattern nesting at maximum depth (depth 253) +-- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not +-- subject to quantifier multiplication, so the nesting (and depth 253) is +-- preserved after optimization. +SELECT id, val, COUNT(*) OVER w FROM rpr_errors +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?) + DEFINE A AS val > 0 +); + id | val | count +----+-----+------- + 1 | 10 | 0 + 2 | 20 | 0 +(2 rows) + +-- Expected: Should succeed +-- Test: Pattern nesting depth exceeds maximum (depth 254) +-- Note: 254 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not +-- subject to quantifier multiplication, so the nesting reaches depth 254 and +-- exceeds the limit. +SELECT id, val, COUNT(*) OVER w FROM rpr_errors +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?) + DEFINE A AS val > 0 +); +ERROR: pattern nesting too deep +DETAIL: Pattern nesting depth 254 exceeds maximum 253. +DROP TABLE rpr_errors; +-- ============================================================ +-- Basic Pattern Matching +-- ============================================================ +-- Test: A? (optional, greedy) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A?) + DEFINE A AS val > 50 +); + id | val | c +----+-----+--- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 1 + 7 | 70 | 1 + 8 | 80 | 1 + 9 | 90 | 1 + 10 | 100 | 1 +(10 rows) + +-- Test: A{2} (exact count) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2}) + DEFINE A AS val <= 50 +); + id | val | c +----+-----+--- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 30 | 2 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Test: A{1,3} (bounded range, greedy) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{1,3}) + DEFINE A AS val <= 50 +); + id | val | c +----+-----+--- + 1 | 10 | 3 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 2 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Test: A | B (simple alternation) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A | B) + DEFINE A AS val <= 30, B AS val > 70 +); + id | val | c +----+-----+--- + 1 | 10 | 1 + 2 | 20 | 1 + 3 | 30 | 1 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 1 + 9 | 90 | 1 + 10 | 100 | 1 +(10 rows) + +-- Test: A | B | C (three-way alternation) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A | B | C) + DEFINE A AS val <= 20, B AS val BETWEEN 40 AND 60, C AS val > 80 +); + id | val | c +----+-----+--- + 1 | 10 | 1 + 2 | 20 | 1 + 3 | 30 | 0 + 4 | 40 | 1 + 5 | 50 | 1 + 6 | 60 | 1 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 1 + 10 | 100 | 1 +(10 rows) + +-- Test: A B C (concatenation) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 60 +); + id | val | c +----+-----+--- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Test: A B? C (optional middle) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B? C) + DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 60 +); + id | val | c +----+-----+--- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Test: (A B)+ (grouped quantifier) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+) + DEFINE A AS val <= 50, B AS val > 50 +); + id | val | c +----+-----+--- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 2 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Test: (A | B)+ C (alternation with quantifier) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B)+ C) + DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 80 +); + id | val | c +----+-----+--- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- Test: (A+ | (A | B)+)* - nested alternation inside quantified group +-- Previously caused infinite recursion in alternation handling when the inner +-- BEGIN(+)'s skip jump was followed as an ALT branch pointer. +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM (VALUES + (1, ARRAY['A', 'B']), + (2, ARRAY['B']), + (3, ARRAY['C']) +) AS t(id, flags) +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ | (A | B)+)*) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 2 + 2 | {B} | | + 3 | {C} | | +(3 rows) + +-- ============================================================ +-- Pathological Patterns +-- ============================================================ +-- These patterns previously caused issues. Now optimized or handled safely. +-- Test: (A*)* - nested unbounded (optimized to A*) +SELECT v, count(*) OVER w AS c +FROM (SELECT generate_series(1, 5) v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((A*)*) + DEFINE A AS TRUE +); + v | c +---+--- + 1 | 5 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +-- Test: (A*)+ - inner nullable (optimized to A*) +SELECT v, count(*) OVER w AS c +FROM (SELECT generate_series(1, 5) v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((A*)+) + DEFINE A AS TRUE +); + v | c +---+--- + 1 | 5 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +-- Test: (A+)* - outer nullable (optimized to A*) +SELECT v, count(*) OVER w AS c +FROM (SELECT generate_series(1, 5) v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((A+)*) + DEFINE A AS TRUE +); + v | c +---+--- + 1 | 5 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +-- Test: (A+)+ - both require match (optimized to A+) +SELECT v, count(*) OVER w AS c +FROM (SELECT generate_series(1, 5) v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((A+)+) + DEFINE A AS TRUE +); + v | c +---+--- + 1 | 5 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +-- Test: (((A)*)*)* - triple nested (optimized to A*) +SELECT v, count(*) OVER w AS c +FROM (SELECT generate_series(1, 3) v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((((A)*)*)*) + DEFINE A AS TRUE +); + v | c +---+--- + 1 | 3 + 2 | 0 + 3 | 0 +(3 rows) + +-- Optional group with alternation: A ((B | C) (D | E))* F? +-- When only A matches, the * group matches 0 times and F? matches 0 times +SELECT id, val, match_len +FROM (SELECT id, val, + COUNT(*) OVER w AS match_len + FROM (VALUES (1, 1), (2, 99)) AS t(id, val) + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A ((B | C) (D | E))* F?) + DEFINE A AS val = 1, + B AS val = 2, C AS val = 3, + D AS val = 4, E AS val = 5, + F AS val = 6 + ) +) s; + id | val | match_len +----+-----+----------- + 1 | 1 | 1 + 2 | 99 | 0 +(2 rows) + +DROP TABLE rpr_plan; +RESET client_min_messages; diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out new file mode 100644 index 00000000000..11fad2b80ba --- /dev/null +++ b/src/test/regress/expected/rpr_explain.out @@ -0,0 +1,6404 @@ +-- ============================================================ +-- RPR EXPLAIN Tests +-- Tests for Row Pattern Recognition EXPLAIN output +-- ============================================================ +-- +-- Views and tables in this file are intentionally not dropped, +-- so that pg_upgrade/pg_dump can test RPR syntax serialization. +-- +-- This test suite validates EXPLAIN output for RPR queries, +-- including NFA statistics shown in EXPLAIN ANALYZE: +-- - NFA States: peak, total, merged +-- - NFA Contexts: peak, total, pruned +-- - NFA: matched (len min/max/avg), mismatched (len min/max/avg) +-- - NFA: absorbed (len min/max/avg), skipped (len min/max/avg) +-- - Pattern deparse formatting +-- - Multiple output formats (text, JSON, XML) +-- +-- Test Coverage: +-- Basic NFA Statistics Tests +-- State Statistics Tests +-- Context Statistics Tests +-- Match Length Statistics Tests +-- Mismatch Length Statistics Tests +-- JSON Format Tests +-- XML Format Tests +-- Multiple Partitions Tests +-- Edge Cases +-- Complex Pattern Tests +-- Real-world Pattern Examples +-- Performance-oriented Tests +-- INITIAL vs no INITIAL comparison +-- Quantifier Variations +-- Regression Tests for Statistics Accuracy +-- Alternation Pattern Tests +-- Group Pattern Tests +-- Window Function Combinations +-- DEFINE Expression Variations +-- Large Scale Statistics Verification +-- Nav Mark Lookback/Lookahead (tuplestore trim) +-- ============================================================ +-- Filter function to normalize platform-dependent memory values (not NFA statistics). +-- NFA statistics should not change between platforms; if they do, it could +-- indicate issues such as uninitialized memory access. +-- Works for text, JSON, and XML formats. +create function rpr_explain_filter(text) returns setof text +language plpgsql as +$$ +declare + ln text; +begin + for ln in execute $1 + loop + -- Normalize platform-dependent memory values + -- Keep NFA statistics numbers unchanged (they are test assertions) + + -- Text format: "Storage: Memory Maximum Storage: 18kB" + if ln ~ 'Storage:.*Maximum Storage:' then + ln := regexp_replace(ln, '\m\d+kB', 'NkB', 'g'); + end if; + + -- JSON format: "Maximum Storage": 17 (number in kB units) + if ln ~ '"Maximum Storage":' then + ln := regexp_replace(ln, '"Maximum Storage": \d+', '"Maximum Storage": 0', 'g'); + end if; + + -- XML format: 17 (number in kB units) + if ln ~ '' then + ln := regexp_replace(ln, '\d+', '0', 'g'); + end if; + + -- Sort Method memory is platform-dependent (32-bit vs 64-bit) + if ln ~ 'Sort Method:.*Memory:' then + ln := regexp_replace(ln, 'Memory: \d+kB', 'Memory: NkB'); + end if; + + return next ln; + end loop; +end; +$$; +-- Setup: Create test tables +CREATE TABLE rpr_nfa_test ( + id serial, + v int, + cat char(1) +); +-- Insert test data: 100 rows with predictable pattern +INSERT INTO rpr_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 TABLE rpr_nfa_complex ( + id serial, + price int, + trend char(1) -- U=up, D=down, S=stable +); +INSERT INTO rpr_nfa_complex (price, trend) +VALUES + (100, 'S'), (105, 'U'), (110, 'U'), (108, 'D'), (112, 'U'), + (115, 'U'), (113, 'D'), (111, 'D'), (109, 'D'), (110, 'U'), + (120, 'U'), (125, 'U'), (130, 'U'), (128, 'D'), (126, 'D'), + (124, 'D'), (122, 'D'), (120, 'D'), (118, 'D'), (119, 'U'), + (121, 'U'), (123, 'U'), (125, 'U'), (127, 'U'), (129, 'U'), + (131, 'U'), (133, 'U'), (130, 'D'), (127, 'D'), (124, 'D'); +-- ============================================================ +-- Basic NFA Statistics Tests +-- ============================================================ +-- Simple pattern - should show basic statistics +CREATE VIEW rpr_ev_basic_simple AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS cat = 'A', B AS cat = 'B' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_simple'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------ + PATTERN (a b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS cat = ''A'', B AS cat = ''B'' +)'); + rpr_explain_filter +------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 101 total, 0 merged + NFA Contexts: 2 peak, 101 total, 60 pruned + NFA: 20 matched (len 2/2/2.0), 0 mismatched + NFA: 0 absorbed, 20 skipped (len 1/1/1.0) + -> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1) +(10 rows) + +-- Pattern with no matches - 0 matched +CREATE VIEW rpr_ev_basic_nomatch AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (X Y Z) + DEFINE X AS cat = 'X', Y AS cat = 'Y', Z AS cat = 'Z' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_nomatch'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------- + PATTERN (x y z) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (X Y Z) + DEFINE X AS cat = ''X'', Y AS cat = ''Y'', Z AS cat = ''Z'' +);'); + rpr_explain_filter +------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: x y z + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 1 peak, 101 total, 0 merged + NFA Contexts: 2 peak, 101 total, 100 pruned + NFA: 0 matched, 0 mismatched + -> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1) +(9 rows) + +-- Pattern matching every row - high match count +CREATE VIEW rpr_ev_basic_allrows AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (R) + DEFINE R AS TRUE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_allrows'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------- + PATTERN (r) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (R) + DEFINE R AS TRUE +);'); + rpr_explain_filter +------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: r + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 101 total, 0 merged + NFA Contexts: 2 peak, 101 total, 0 pruned + NFA: 100 matched (len 1/1/1.0), 0 mismatched + -> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1) +(9 rows) + +-- Regression test: Space before parenthesis in pattern deparse +-- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space +CREATE VIEW rpr_ev_basic_deparse_space AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B | C)) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_deparse_space'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------ + PATTERN (a (b | c)) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B | C)) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a (b | c) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 35 total, 0 merged + NFA Contexts: 2 peak, 21 total, 6 pruned + NFA: 7 matched (len 2/2/2.0), 0 mismatched + NFA: 0 absorbed, 7 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Regression test: Sequential alternations at same depth +-- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)" +-- Previously failed due to missing parentheses on ALT depth decrease +CREATE VIEW rpr_ev_basic_deparse_seqalt AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A ((B | C) (D | E))*) + DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_deparse_seqalt'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------------------- + PATTERN (a ((b | c) (d | e))*) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A ((B | C) (D | E))*) + DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=30.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a ((b | c) (d | e))* + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 61 total, 0 merged + NFA Contexts: 3 peak, 31 total, 24 pruned + NFA: 6 matched (len 1/1/1.0), 0 mismatched + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(9 rows) + +-- Regression test: Quoted identifiers in EXPLAIN pattern deparse +-- Mixed case names must be quoted to preserve round-trip safety +SELECT rpr_explain_filter(' +EXPLAIN (COSTS OFF) +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ("Start" "Up"+) + DEFINE "Start" AS TRUE, "Up" AS v > PREV(v) +);'); + rpr_explain_filter +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: "Start" "Up"+ + Nav Mark Lookback: 1 + -> Function Scan on generate_series s +(5 rows) + +-- ============================================================ +-- State Statistics Tests (peak, total, merged) +-- ============================================================ +-- Simple quantifier pattern - A+ with short matches (no merging) +CREATE VIEW rpr_ev_state_simple_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v % 2 = 1 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_simple_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------- + PATTERN (a+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v % 2 = 1 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 76 total, 0 merged + NFA Contexts: 3 peak, 51 total, 25 pruned + NFA: 25 matched (len 1/1/1.0), 0 mismatched + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +-- Alternation pattern - multiple state branches +CREATE VIEW rpr_ev_state_alt AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | C) (D | E)) + DEFINE + A AS cat = 'A', B AS cat = 'B', C AS cat = 'C', + D AS cat = 'D', E AS cat = 'E' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------------------- + PATTERN ((a | b | c) (d | e)) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | C) (D | E)) + DEFINE + A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'', + D AS cat = ''D'', E AS cat = ''E'' +);'); + rpr_explain_filter +------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b | c) (d | e) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 524 total, 0 merged + NFA Contexts: 3 peak, 101 total, 20 pruned + NFA: 20 matched (len 2/2/2.0), 40 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 20 skipped (len 1/1/1.0) + -> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1) +(10 rows) + +-- Complex pattern with high state count +CREATE VIEW rpr_ev_state_complex AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B* C+) + DEFINE + A AS v % 3 = 1, + B AS v % 3 = 2, + C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_complex'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------- + PATTERN (a+ b* c+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B* C+) + DEFINE + A AS v % 3 = 1, + B AS v % 3 = 2, + C AS v % 3 = 0 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b* c+ + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 235 total, 0 merged + NFA Contexts: 3 peak, 101 total, 34 pruned + NFA: 33 matched (len 3/3/3.0), 0 mismatched + NFA: 0 absorbed, 33 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(10 rows) + +-- Grouped pattern with quantifier - state count with grouping +CREATE VIEW rpr_ev_state_group_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_group_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------- + PATTERN ((a b)+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=60.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b')+" + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 91 total, 0 merged + NFA Contexts: 3 peak, 61 total, 0 pruned + NFA: 1 matched (len 60/60/60.0), 0 mismatched + NFA: 29 absorbed (len 2/2/2.0), 30 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=60.00 loops=1) +(10 rows) + +-- State explosion pattern - many alternations +-- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states +CREATE VIEW rpr_ev_state_explosion AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B)) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_explosion'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------------------------------------------------------------ + PATTERN ((a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b)) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B)) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b){8} + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 17 peak, 995 total, 0 merged + NFA Contexts: 8 peak, 101 total, 1 pruned + NFA: 12 matched (len 8/8/8.0), 3 mismatched (len 2/4/3.0) + NFA: 0 absorbed, 84 skipped (len 1/7/4.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(10 rows) + +-- Consecutive ALT merge followed by different ALT +-- ((A | B) (A | B) (C | D)) -> (A|B){2} (C|D) +CREATE VIEW rpr_ev_state_alt_merge_alt AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) (C | D)) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_merge_alt'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------------------- + PATTERN ((a | b) (a | b) (c | d)) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) (C | D)) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=40.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b){2} (c | d) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 7 peak, 181 total, 0 merged + NFA Contexts: 3 peak, 41 total, 12 pruned + NFA: 9 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 18 skipped (len 1/2/1.5) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(10 rows) + +-- Consecutive ALT merge followed by non-ALT element +-- ((A | B) (A | B) C) -> (A|B){2} C +CREATE VIEW rpr_ev_state_alt_merge_nonalt AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) C) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_merge_nonalt'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------------- + PATTERN ((a | b) (a | b) c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) C) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=40.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b){2} c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 177 total, 0 merged + NFA Contexts: 3 peak, 41 total, 2 pruned + NFA: 12 matched (len 3/3/3.0), 2 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 24 skipped (len 1/2/1.5) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(10 rows) + +-- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,} +CREATE VIEW rpr_ev_state_alt_absorb_group AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B)+ (A | B)) + DEFINE A AS v % 2 = 0, B AS v % 2 = 1 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_absorb_group'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------------------------- + PATTERN ((a | b) (a | b)+ (a | b)) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B)+ (A | B)) + DEFINE A AS v % 2 = 0, B AS v % 2 = 1 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=40.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b){3,} + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 7 peak, 243 total, 0 merged + NFA Contexts: 3 peak, 41 total, 0 pruned + NFA: 1 matched (len 40/40/40.0), 0 mismatched + NFA: 0 absorbed, 39 skipped (len 1/2/1.0) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(10 rows) + +-- High state count - alternation with plus quantifier +CREATE VIEW rpr_ev_state_alt_plus AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | C)+ D) + DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_plus'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------------- + PATTERN ((a | b | c)+ d) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | C)+ D) + DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b | c)+ d + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 16 peak, 1004 total, 0 merged + NFA Contexts: 4 peak, 101 total, 0 pruned + NFA: 25 matched (len 4/4/4.0), 0 mismatched + NFA: 0 absorbed, 75 skipped (len 1/3/2.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(10 rows) + +-- Early termination: first ALT branch (A) reaches FIN immediately, +-- pruning second branch (A B+) before it can accumulate B repetitions. +CREATE VIEW rpr_ev_state_alt_prune AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | A B)+) + DEFINE A AS v = 1, B AS v > 1 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_prune'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------- + PATTERN ((a | a b)+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | A B)+) + DEFINE A AS v = 1, B AS v > 1 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | a b)+ + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 306 total, 0 merged + NFA Contexts: 3 peak, 101 total, 99 pruned + NFA: 1 matched (len 1/1/1.0), 0 mismatched + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(9 rows) + +-- Nested quantifiers causing state growth +CREATE VIEW rpr_ev_state_nested_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 1000) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A | B)+)+) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_nested_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------- + PATTERN (((a | b)+)+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 1000) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A | B)+)+) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2 +);'); + rpr_explain_filter +------------------------------------------------------------------------ + WindowAgg (actual rows=1000.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b)+ + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 5004 total, 0 merged + NFA Contexts: 3 peak, 1001 total, 333 pruned + NFA: 334 matched (len 1/2/2.0), 0 mismatched + NFA: 0 absorbed, 333 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=1000.00 loops=1) +(10 rows) + +-- (A{2,})* must NOT flatten to a* (H-1): counts {0} UNION [2, INF) leave 1 +-- unreachable. The planner keeps it as (a{2,})*, not a*. +CREATE VIEW rpr_ev_nested_quant_no_flatten AS +SELECT count(*) OVER w +FROM generate_series(1, 6) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A{2,})*) + DEFINE A AS v % 3 <> 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nested_quant_no_flatten'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------- + PATTERN ((a{2,})*) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 6) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A{2,})*) + DEFINE A AS v % 3 <> 0 +);'); + rpr_explain_filter +--------------------------------------------------------------------- + WindowAgg (actual rows=6.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2,}")* + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 18 total, 0 merged + NFA Contexts: 3 peak, 7 total, 2 pruned + NFA: 2 matched (len 2/2/2.0), 0 mismatched + NFA: 0 absorbed, 2 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=6.00 loops=1) +(10 rows) + +-- ============================================================ +-- Context Statistics Tests (peak, total, pruned + absorbed/skipped) +-- ============================================================ +-- Context absorption with unbounded quantifier at start +CREATE VIEW rpr_ev_ctx_absorb_unbounded AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_unbounded'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 91 total, 0 merged + NFA Contexts: 2 peak, 51 total, 0 pruned + NFA: 10 matched (len 5/5/5.0), 0 mismatched + NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- Bare unbounded quantifier: A+ absorbs redundant contexts +-- min=1 commits no match until the run ends, so newer contexts absorb in-progress +CREATE VIEW rpr_ev_ctx_absorb_plus AS +SELECT count(*) OVER w +FROM generate_series(1, 10) 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 > 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_plus'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------- + PATTERN (a+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 10) 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 > 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=10.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 21 total, 0 merged + NFA Contexts: 2 peak, 11 total, 0 pruned + NFA: 1 matched (len 10/10/10.0), 0 mismatched + NFA: 9 absorbed (len 1/1/1.0), 0 skipped + -> Function Scan on generate_series s (actual rows=10.00 loops=1) +(10 rows) + +-- Bare min=0 quantifier: A* is skipped, not absorbed +-- min=0 commits an empty match at creation, so SKIP (not absorption) removes them +CREATE VIEW rpr_ev_ctx_absorb_star AS +SELECT count(*) OVER w +FROM generate_series(1, 10) 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 > 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_star'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------- + PATTERN (a*) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 10) 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 > 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=10.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a*" + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 32 total, 0 merged + NFA Contexts: 2 peak, 11 total, 0 pruned + NFA: 1 matched (len 10/10/10.0), 0 mismatched + NFA: 0 absorbed, 9 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=10.00 loops=1) +(10 rows) + +-- No absorption - bounded quantifier +CREATE VIEW rpr_ev_ctx_no_absorb AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,4} B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_no_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------- + PATTERN (a{2,4} b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,4} B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2,4} b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 7 peak, 101 total, 0 merged + NFA Contexts: 5 peak, 51 total, 0 pruned + NFA: 10 matched (len 5/5/5.0), 0 mismatched + NFA: 0 absorbed, 40 skipped (len 1/4/2.5) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- Contexts skipped by SKIP PAST LAST ROW +CREATE VIEW rpr_ev_ctx_skip AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_skip'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------- + PATTERN (a b c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 101 total, 0 merged + NFA Contexts: 3 peak, 101 total, 80 pruned + NFA: 10 matched (len 3/3/3.0), 0 mismatched + NFA: 0 absorbed, 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(10 rows) + +-- High context absorption - unbounded group +CREATE VIEW rpr_ev_ctx_absorb_group AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+ C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_group'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------- + PATTERN ((a b)+ c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+ C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b')+" c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 134 total, 0 merged + NFA Contexts: 3 peak, 101 total, 34 pruned + NFA: 33 matched (len 3/3/3.0), 0 mismatched + NFA: 0 absorbed, 33 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(10 rows) + +-- Fixed-length group absorption: (A B B)+ C +-- B B merged to B{2}; absorbable with fixed-length check +-- step_size=3 (A + B + B); v % 7 cycle gives 2 iterations per match +CREATE VIEW rpr_ev_ctx_absorb_fixedvar AS +SELECT count(*) OVER w +FROM generate_series(1, 70) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B B)+ C) + DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_fixedvar'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------- + PATTERN ((a b b)+ c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 70) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B B)+ C) + DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=70.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b{2}')+" c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 91 total, 0 merged + NFA Contexts: 4 peak, 71 total, 40 pruned + NFA: 10 matched (len 7/7/7.0), 0 mismatched + NFA: 10 absorbed (len 3/3/3.0), 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=70.00 loops=1) +(10 rows) + +-- Nested fixed-length group absorption: (A (B C){2} D)+ E +-- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E +CREATE VIEW rpr_ev_ctx_absorb_nested AS +SELECT count(*) OVER w +FROM generate_series(1, 65) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A (B C){2} D)+ E) + DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10), + C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12), + E AS v % 13 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_nested'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------------- + PATTERN ((a (b c){2} d)+ e) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 65) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A (B C){2} D)+ E) + DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10), + C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12), + E AS v % 13 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=65.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' (b' c'){2}' d')+" e + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 76 total, 0 merged + NFA Contexts: 4 peak, 66 total, 50 pruned + NFA: 5 matched (len 13/13/13.0), 0 mismatched + NFA: 5 absorbed (len 6/6/6.0), 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=65.00 loops=1) +(10 rows) + +-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F +-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F +CREATE VIEW rpr_ev_ctx_absorb_deep AS +SELECT count(*) OVER w +FROM generate_series(1, 82) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A ((B C C C){2} D){2} E)+ F) + DEFINE A AS v % 41 IN (1, 21), + B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35), + C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18, + 23,24,25, 27,28,29, 32,33,34, 36,37,38), + D AS v % 41 IN (10, 19, 30, 39), + E AS v % 41 IN (20, 40), + F AS v % 41 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_deep'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------------------------- + PATTERN ((a ((b c c c){2} d){2} e)+ f) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 82) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A ((B C C C){2} D){2} E)+ F) + DEFINE A AS v % 41 IN (1, 21), + B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35), + C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18, + 23,24,25, 27,28,29, 32,33,34, 36,37,38), + D AS v % 41 IN (10, 19, 30, 39), + E AS v % 41 IN (20, 40), + F AS v % 41 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=82.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' ((b' c{3}'){2}' d'){2}' e')+" f + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 87 total, 0 merged + NFA Contexts: 4 peak, 83 total, 76 pruned + NFA: 2 matched (len 41/41/41.0), 0 mismatched + NFA: 2 absorbed (len 20/20/20.0), 2 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=82.00 loops=1) +(10 rows) + +-- 3-level END chain absorption: ((A (B C){2}){2})+ +-- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations +-- END chain: END(BC{2}) -> END(A..{2}) -> END(+, ABSORBABLE) +CREATE VIEW rpr_ev_ctx_absorb_endchain AS +SELECT count(*) OVER w +FROM generate_series(1, 42) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A (B C){2}){2})+) + DEFINE A AS v % 21 IN (1, 6, 11, 16), + B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19), + C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_endchain'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------------------- + PATTERN (((a (b c){2}){2})+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 42) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A (B C){2}){2})+) + DEFINE A AS v % 21 IN (1, 6, 11, 16), + B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19), + C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20) +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=42.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a' (b' c'){2}'){2}')+" + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 47 total, 0 merged + NFA Contexts: 5 peak, 43 total, 30 pruned + NFA: 2 matched (len 20/20/20.0), 0 mismatched + NFA: 2 absorbed (len 10/10/10.0), 8 skipped (len 1/5/3.0) + -> Function Scan on generate_series s (actual rows=42.00 loops=1) +(10 rows) + +-- No absorption when DEFINE uses FIRST (match_start-dependent) +-- Same pattern as rpr_ev_ctx_absorb_unbounded but with FIRST in DEFINE. +-- Compare: absorbed count should be 0 here vs >0 above. +CREATE VIEW rpr_ev_ctx_no_absorb_first AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND v > FIRST(v) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_no_absorb_first'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND v > FIRST(v) +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ b + Nav Mark Lookback: 0 + Nav Mark Lookahead: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 9 peak, 151 total, 0 merged + NFA Contexts: 5 peak, 51 total, 0 pruned + NFA: 10 matched (len 5/5/5.0), 0 mismatched + NFA: 0 absorbed, 40 skipped (len 1/4/2.5) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(11 rows) + +-- Absorption preserved when DEFINE uses only LAST without offset +-- LAST(v) is match_start-independent (always currentpos), so absorption +-- remains active. Compare: absorbed count should be >0, like the +-- PREV-only case above. +CREATE VIEW rpr_ev_ctx_absorb_last AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS LAST(v) % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_last'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS LAST(v) % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 91 total, 0 merged + NFA Contexts: 2 peak, 51 total, 0 pruned + NFA: 10 matched (len 5/5/5.0), 0 mismatched + NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- No absorption with compound PREV(FIRST()) (match_start-dependent) +CREATE VIEW rpr_ev_ctx_no_absorb_compound AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND PREV(FIRST(v), 1) IS NOT NULL +); +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND PREV(FIRST(v), 1) IS NOT NULL +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ b + Nav Mark Lookback: 0 + Nav Mark Lookahead: -1 + Storage: Memory Maximum Storage: NkB + NFA States: 9 peak, 151 total, 0 merged + NFA Contexts: 5 peak, 51 total, 0 pruned + NFA: 10 matched (len 4/5/4.9), 1 mismatched (len 5/5/5.0) + NFA: 0 absorbed, 39 skipped (len 1/4/2.5) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(11 rows) + +-- Alternation, non-absorbable branch match survives absorption: A+ B | C +-- The dominating A+ run absorbs redundant contexts, but the recorded C matches +-- are not absorbable, so they survive (2 matched, not 0) +CREATE VIEW rpr_ev_ctx_absorb_alt_nonabsorb AS +WITH d(id, flags) AS ( + VALUES (1, ARRAY['A']), (2, ARRAY['A', 'C']), (3, ARRAY['A']), + (4, ARRAY['A']), (5, ARRAY['A', 'C']), (6, ARRAY['A'])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +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) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_alt_nonabsorb'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------- + PATTERN (a+ b | c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +WITH d(id, flags) AS ( + VALUES (1, ARRAY[''A'']), (2, ARRAY[''A'', ''C'']), (3, ARRAY[''A'']), + (4, ARRAY[''A'']), (5, ARRAY[''A'', ''C'']), (6, ARRAY[''A''])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +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) +);'); + rpr_explain_filter +----------------------------------------------------------------------------------------------- + WindowAgg (actual rows=6.00 loops=1) + Window: w AS (ORDER BY "*VALUES*".column1 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+" b | c) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 11 peak, 34 total, 0 merged + NFA Contexts: 4 peak, 7 total, 0 pruned + NFA: 2 matched (len 1/1/1.0), 1 mismatched (len 6/6/6.0) + NFA: 3 absorbed (len 1/1/1.0), 0 skipped + -> Sort (actual rows=6.00 loops=1) + Sort Key: "*VALUES*".column1 + Sort Method: quicksort Memory: NkB + -> Values Scan on "*VALUES*" (actual rows=6.00 loops=1) +(13 rows) + +-- Alternation, both branches absorbable: A+ C | B+ +-- A+ C never completes (C absent) so its A+ run absorbs redundant contexts; the +-- finalized B+ matches on the other branch survive (2 matched, not 0) +CREATE VIEW rpr_ev_ctx_absorb_alt_both AS +WITH d(id, flags) AS ( + VALUES (1, ARRAY['A', 'B']), (2, ARRAY['A', 'B']), (3, ARRAY['A', 'B']), + (4, ARRAY['A']), (5, ARRAY['A']), (6, ARRAY['A', 'B']), + (7, ARRAY['A', 'B']), (8, ARRAY['A', 'B']), (9, ARRAY['A'])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ C | B+) + DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags), C AS 'C' = ANY(flags) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_alt_both'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------ + PATTERN (a+ c | b+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +WITH d(id, flags) AS ( + VALUES (1, ARRAY[''A'', ''B'']), (2, ARRAY[''A'', ''B'']), (3, ARRAY[''A'', ''B'']), + (4, ARRAY[''A'']), (5, ARRAY[''A'']), (6, ARRAY[''A'', ''B'']), + (7, ARRAY[''A'', ''B'']), (8, ARRAY[''A'', ''B'']), (9, ARRAY[''A''])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ C | B+) + DEFINE A AS ''A'' = ANY(flags), B AS ''B'' = ANY(flags), C AS ''C'' = ANY(flags) +);'); + rpr_explain_filter +----------------------------------------------------------------------------------------------- + WindowAgg (actual rows=9.00 loops=1) + Window: w AS (ORDER BY "*VALUES*".column1 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+" c | b+") + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 10 peak, 49 total, 0 merged + NFA Contexts: 3 peak, 10 total, 0 pruned + NFA: 2 matched (len 3/3/3.0), 0 mismatched + NFA: 7 absorbed (len 1/1/1.0), 0 skipped + -> Sort (actual rows=9.00 loops=1) + Sort Key: "*VALUES*".column1 + Sort Method: quicksort Memory: NkB + -> Values Scan on "*VALUES*" (actual rows=9.00 loops=1) +(13 rows) + +-- ============================================================ +-- Match Length Statistics Tests +-- ============================================================ +-- Fixed length matches - all same length +CREATE VIEW rpr_ev_mlen_fixed AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E) + DEFINE + A AS cat = 'A', B AS cat = 'B', C AS cat = 'C', + D AS cat = 'D', E AS cat = 'E' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_fixed'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------ + PATTERN (a b c d e) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E) + DEFINE + A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'', + D AS cat = ''D'', E AS cat = ''E'' +);'); + rpr_explain_filter +------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c d e + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 101 total, 0 merged + NFA Contexts: 3 peak, 101 total, 60 pruned + NFA: 20 matched (len 5/5/5.0), 0 mismatched + NFA: 0 absorbed, 20 skipped (len 1/1/1.0) + -> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1) +(10 rows) + +-- Variable length matches - min/max/avg differ +CREATE VIEW rpr_ev_mlen_variable AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_variable'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 191 total, 0 merged + NFA Contexts: 2 peak, 101 total, 0 pruned + NFA: 10 matched (len 10/10/10.0), 0 mismatched + NFA: 80 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(10 rows) + +-- Very long matches +CREATE VIEW rpr_ev_mlen_long AS +SELECT count(*) OVER w +FROM generate_series(1, 200) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v <= 195, B AS v > 195 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_long'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 200) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v <= 195, B AS v > 195 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=200.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 396 total, 0 merged + NFA Contexts: 2 peak, 201 total, 4 pruned + NFA: 1 matched (len 196/196/196.0), 0 mismatched + NFA: 194 absorbed (len 1/1/1.0), 1 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=200.00 loops=1) +(10 rows) + +-- Uniform match length with mismatches from gap rows (v%20 = 11..15) +CREATE VIEW rpr_ev_mlen_with_mismatch AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE + A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15), + B AS v % 20 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_with_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE + A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15), + B AS v % 20 = 0 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 171 total, 0 merged + NFA Contexts: 3 peak, 101 total, 25 pruned + NFA: 5 matched (len 5/5/5.0), 5 mismatched (len 11/11/11.0) + NFA: 60 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(10 rows) + +-- ============================================================ +-- Mismatch Length Statistics Tests +-- ============================================================ +-- Pattern with complete match every cycle: 0 mismatched +-- A(1,2,3) B(4,5) C(6) repeats perfectly; X rows are pruned, not mismatched +CREATE VIEW rpr_ev_mlen_no_mismatch AS +SELECT count(*) OVER w +FROM ( + SELECT v, + CASE WHEN v % 10 IN (1,2,3) THEN 'A' + WHEN v % 10 IN (4,5) THEN 'B' + WHEN v % 10 = 6 THEN 'C' + ELSE 'X' END AS cat + FROM generate_series(1, 100) AS s(v) +) t +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B+ C) + DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_no_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------- + PATTERN (a+ b+ c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM ( + SELECT v, + CASE WHEN v % 10 IN (1,2,3) THEN ''A'' + WHEN v % 10 IN (4,5) THEN ''B'' + WHEN v % 10 = 6 THEN ''C'' + ELSE ''X'' END AS cat + FROM generate_series(1, 100) AS s(v) +) t +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B+ C) + DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'' +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b+ c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 151 total, 0 merged + NFA Contexts: 3 peak, 101 total, 60 pruned + NFA: 10 matched (len 6/6/6.0), 0 mismatched + NFA: 20 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(10 rows) + +-- Long partial matches that fail +CREATE VIEW rpr_ev_mlen_long_partial AS +SELECT count(*) OVER w +FROM ( + SELECT i AS v, + CASE + WHEN i <= 20 THEN 'A' + WHEN i <= 25 THEN 'B' + WHEN i = 26 THEN 'X' -- breaks the pattern + WHEN i <= 50 THEN 'A' + WHEN i <= 55 THEN 'B' + WHEN i = 56 THEN 'C' -- completes pattern + ELSE 'Y' + END AS cat + FROM generate_series(1, 60) i +) t +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B+ C) + DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_long_partial'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------- + PATTERN (a+ b+ c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM ( + SELECT i AS v, + CASE + WHEN i <= 20 THEN ''A'' + WHEN i <= 25 THEN ''B'' + WHEN i = 26 THEN ''X'' -- breaks the pattern + WHEN i <= 50 THEN ''A'' + WHEN i <= 55 THEN ''B'' + WHEN i = 56 THEN ''C'' -- completes pattern + ELSE ''Y'' + END AS cat + FROM generate_series(1, 60) i +) t +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B+ C) + DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'' +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=60.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b+ c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 115 total, 0 merged + NFA Contexts: 3 peak, 61 total, 15 pruned + NFA: 1 matched (len 30/30/30.0), 1 mismatched (len 26/26/26.0) + NFA: 42 absorbed (len 1/1/1.0), 1 skipped (len 1/1/1.0) + -> Function Scan on generate_series i (actual rows=60.00 loops=1) +(10 rows) + +-- ============================================================ +-- JSON Format Tests +-- ============================================================ +-- JSON format output with all statistics +CREATE VIEW rpr_ev_json_basic AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B+) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_basic'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------- + PATTERN (a+ b+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B+) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2 +)'); + rpr_explain_filter +---------------------------------------------------------------------------- + [ + + { + + "Plan": { + + "Node Type": "WindowAgg", + + "Parallel Aware": false, + + "Async Capable": false, + + "Actual Rows": 50.00, + + "Actual Loops": 1, + + "Disabled": false, + + "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+ + "Pattern": "a+\" b+", + + "Nav Mark Lookback": 0, + + "Storage": "Memory", + + "Maximum Storage": 0, + + "NFA States Peak": 3, + + "NFA States Total": 85, + + "NFA States Merged": 0, + + "NFA Contexts Peak": 3, + + "NFA Contexts Total": 51, + + "NFA Contexts Absorbed": 0, + + "NFA Contexts Skipped": 17, + + "NFA Contexts Pruned": 16, + + "NFA Matched": 17, + + "NFA Mismatched": 0, + + "NFA Match Length Min": 2, + + "NFA Match Length Max": 2, + + "NFA Match Length Avg": 2.0, + + "NFA Skipped Length Min": 1, + + "NFA Skipped Length Max": 1, + + "NFA Skipped Length Avg": 1.0, + + "Plans": [ + + { + + "Node Type": "Function Scan", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Function Name": "generate_series", + + "Alias": "s", + + "Actual Rows": 50.00, + + "Actual Loops": 1, + + "Disabled": false + + } + + ] + + }, + + "Triggers": [ + + ] + + } + + ] +(1 row) + +-- JSON format with match length statistics +CREATE VIEW rpr_ev_json_matchlen AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_matchlen'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +)'); + rpr_explain_filter +---------------------------------------------------------------------------- + [ + + { + + "Plan": { + + "Node Type": "WindowAgg", + + "Parallel Aware": false, + + "Async Capable": false, + + "Actual Rows": 100.00, + + "Actual Loops": 1, + + "Disabled": false, + + "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+ + "Pattern": "a+\" b", + + "Nav Mark Lookback": 0, + + "Storage": "Memory", + + "Maximum Storage": 0, + + "NFA States Peak": 3, + + "NFA States Total": 191, + + "NFA States Merged": 0, + + "NFA Contexts Peak": 2, + + "NFA Contexts Total": 101, + + "NFA Contexts Absorbed": 80, + + "NFA Contexts Skipped": 10, + + "NFA Contexts Pruned": 0, + + "NFA Matched": 10, + + "NFA Mismatched": 0, + + "NFA Match Length Min": 10, + + "NFA Match Length Max": 10, + + "NFA Match Length Avg": 10.0, + + "NFA Absorbed Length Min": 1, + + "NFA Absorbed Length Max": 1, + + "NFA Absorbed Length Avg": 1.0, + + "NFA Skipped Length Min": 1, + + "NFA Skipped Length Max": 1, + + "NFA Skipped Length Avg": 1.0, + + "Plans": [ + + { + + "Node Type": "Function Scan", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Function Name": "generate_series", + + "Alias": "s", + + "Actual Rows": 100.00, + + "Actual Loops": 1, + + "Disabled": false + + } + + ] + + }, + + "Triggers": [ + + ] + + } + + ] +(1 row) + +-- JSON format with mismatch statistics +-- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches +CREATE VIEW rpr_ev_json_mismatch AS +SELECT count(*) OVER w +FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS v = 1, B AS v = 2, C AS v = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------- + PATTERN (a b c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON) +SELECT count(*) OVER w +FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS v = 1, B AS v = 2, C AS v = 3 +)'); + rpr_explain_filter +---------------------------------------------------------------------------- + [ + + { + + "Plan": { + + "Node Type": "WindowAgg", + + "Parallel Aware": false, + + "Async Capable": false, + + "Actual Rows": 9.00, + + "Actual Loops": 1, + + "Disabled": false, + + "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+ + "Pattern": "a b c", + + "Nav Mark Lookback": 0, + + "Storage": "Memory", + + "Maximum Storage": 0, + + "NFA States Peak": 2, + + "NFA States Total": 10, + + "NFA States Merged": 0, + + "NFA Contexts Peak": 3, + + "NFA Contexts Total": 10, + + "NFA Contexts Absorbed": 0, + + "NFA Contexts Skipped": 1, + + "NFA Contexts Pruned": 5, + + "NFA Matched": 1, + + "NFA Mismatched": 2, + + "NFA Match Length Min": 3, + + "NFA Match Length Max": 3, + + "NFA Match Length Avg": 3.0, + + "NFA Mismatch Length Min": 3, + + "NFA Mismatch Length Max": 3, + + "NFA Mismatch Length Avg": 3.0, + + "NFA Skipped Length Min": 1, + + "NFA Skipped Length Max": 1, + + "NFA Skipped Length Avg": 1.0, + + "Plans": [ + + { + + "Node Type": "Values Scan", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Alias": "*VALUES*", + + "Actual Rows": 9.00, + + "Actual Loops": 1, + + "Disabled": false + + } + + ] + + }, + + "Triggers": [ + + ] + + } + + ] +(1 row) + +-- JSON format with skipped context statistics +-- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped +CREATE VIEW rpr_ev_json_skip AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B)) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_skip'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------------------------------------------------------------ + PATTERN ((a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b)) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B)) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +)'); + rpr_explain_filter +---------------------------------------------------------------------------- + [ + + { + + "Plan": { + + "Node Type": "WindowAgg", + + "Parallel Aware": false, + + "Async Capable": false, + + "Actual Rows": 100.00, + + "Actual Loops": 1, + + "Disabled": false, + + "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+ + "Pattern": "(a | b){8}", + + "Nav Mark Lookback": 0, + + "Storage": "Memory", + + "Maximum Storage": 0, + + "NFA States Peak": 17, + + "NFA States Total": 995, + + "NFA States Merged": 0, + + "NFA Contexts Peak": 8, + + "NFA Contexts Total": 101, + + "NFA Contexts Absorbed": 0, + + "NFA Contexts Skipped": 84, + + "NFA Contexts Pruned": 1, + + "NFA Matched": 12, + + "NFA Mismatched": 3, + + "NFA Match Length Min": 8, + + "NFA Match Length Max": 8, + + "NFA Match Length Avg": 8.0, + + "NFA Mismatch Length Min": 2, + + "NFA Mismatch Length Max": 4, + + "NFA Mismatch Length Avg": 3.0, + + "NFA Skipped Length Min": 1, + + "NFA Skipped Length Max": 7, + + "NFA Skipped Length Avg": 4.0, + + "Plans": [ + + { + + "Node Type": "Function Scan", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Function Name": "generate_series", + + "Alias": "s", + + "Actual Rows": 100.00, + + "Actual Loops": 1, + + "Disabled": false + + } + + ] + + }, + + "Triggers": [ + + ] + + } + + ] +(1 row) + +-- ============================================================ +-- XML Format Tests +-- ============================================================ +-- XML format output +CREATE VIEW rpr_ev_xml_basic AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_xml_basic'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------ + PATTERN (a b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT XML) +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +)'); + rpr_explain_filter +-------------------------------------------------------------------------------- + + + + + + + WindowAgg + + false + + false + + 30.00 + + 1 + + false + + w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)+ + a b + + 0 + + Memory + + 0 + + 2 + + 31 + + 0 + + 2 + + 31 + + 0 + + 15 + + 0 + + 15 + + 0 + + 2 + + 2 + + 2.0 + + 1 + + 1 + + 1.0 + + + + + + Function Scan + + Outer + + false + + false + + generate_series + + s + + 30.00 + + 1 + + false + + + + + + + + + + + + + + +(1 row) + +-- ============================================================ +-- Multiple Partitions Tests +-- ============================================================ +-- Statistics across multiple partitions +CREATE VIEW rpr_ev_part_multi AS +SELECT count(*) OVER w +FROM ( + SELECT p, v + FROM generate_series(1, 3) p, + generate_series(1, 30) v +) t +WINDOW w AS ( + PARTITION BY p + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_part_multi'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM ( + SELECT p, v + FROM generate_series(1, 3) p, + generate_series(1, 30) v +) t +WINDOW w AS ( + PARTITION BY p + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +------------------------------------------------------------------------------------ + WindowAgg (actual rows=90.00 loops=1) + Window: w AS (PARTITION BY p.p ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 165 total, 0 merged + NFA Contexts: 2 peak, 93 total, 0 pruned + NFA: 18 matched (len 5/5/5.0), 0 mismatched + NFA: 54 absorbed (len 1/1/1.0), 18 skipped (len 1/1/1.0) + -> Sort (actual rows=90.00 loops=1) + Sort Key: p.p + Sort Method: quicksort Memory: NkB + -> 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) +(15 rows) + +-- Different pattern behavior per partition +CREATE VIEW rpr_ev_part_diff AS +SELECT count(*) OVER w +FROM ( + SELECT + CASE WHEN v <= 25 THEN 1 ELSE 2 END AS p, + v % 10 AS val + FROM generate_series(1, 50) v +) t +WINDOW w AS ( + PARTITION BY p + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS val < 5, B AS val >= 5 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_part_diff'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM ( + SELECT + CASE WHEN v <= 25 THEN 1 ELSE 2 END AS p, + v % 10 AS val + FROM generate_series(1, 50) v +) t +WINDOW w AS ( + PARTITION BY p + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS val < 5, B AS val >= 5 +);'); + rpr_explain_filter +-------------------------------------------------------------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (PARTITION BY (CASE WHEN (v.v <= 25) THEN 1 ELSE 2 END) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 77 total, 0 merged + NFA Contexts: 2 peak, 52 total, 21 pruned + NFA: 5 matched (len 5/6/5.8), 0 mismatched + NFA: 19 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0) + -> Sort (actual rows=50.00 loops=1) + Sort Key: (CASE WHEN (v.v <= 25) THEN 1 ELSE 2 END) + Sort Method: quicksort Memory: NkB + -> Function Scan on generate_series v (actual rows=50.00 loops=1) +(13 rows) + +-- ============================================================ +-- Edge Cases +-- ============================================================ +-- Empty result set +CREATE VIEW rpr_ev_edge_empty AS +SELECT count(*) OVER w +FROM generate_series(1, 0) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v = 1, B AS v = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------ + PATTERN (a b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 0) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v = 1, B AS v = 2 +);'); + rpr_explain_filter +--------------------------------------------------------------------- + WindowAgg (actual rows=0.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b + Nav Mark Lookback: 0 + -> Function Scan on generate_series s (actual rows=0.00 loops=1) +(5 rows) + +-- Empty matches (length 0): mirror the test_728_* cases in rpr_nfa.sql. +-- Window aggregates over a length-0 frame return 0 / NULL, so the SELECT +-- result alone cannot distinguish "no match" from "empty match"; the +-- "NFA: N matched (len 0/0/0.0)" line in EXPLAIN is the only observable +-- proof that the empty matches were found. +-- (A?){0,3}: min=0, A never matches -> 3 length-0 matches +CREATE VIEW rpr_ev_edge_empty_match_min0 AS +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){0,3}) + DEFINE A AS FALSE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min0'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------ + PATTERN ((a?){0,3}) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){0,3}) + DEFINE A AS FALSE +);'); + rpr_explain_filter +--------------------------------------------------------------------- + WindowAgg (actual rows=3.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{0,3} + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 8 total, 0 merged + NFA Contexts: 2 peak, 4 total, 0 pruned + NFA: 3 matched (len 0/0/0.0), 0 mismatched + -> Function Scan on generate_series s (actual rows=3.00 loops=1) +(9 rows) + +-- (A?){1,3}: min=1, one empty iteration satisfies min -> 3 length-0 matches +CREATE VIEW rpr_ev_edge_empty_match_min1 AS +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){1,3}) + DEFINE A AS FALSE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min1'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------ + PATTERN ((a?){1,3}) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){1,3}) + DEFINE A AS FALSE +);'); + rpr_explain_filter +--------------------------------------------------------------------- + WindowAgg (actual rows=3.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{0,3} + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 8 total, 0 merged + NFA Contexts: 2 peak, 4 total, 0 pruned + NFA: 3 matched (len 0/0/0.0), 0 mismatched + -> Function Scan on generate_series s (actual rows=3.00 loops=1) +(9 rows) + +-- (A?){2,3}: min=2 (ISO/IEC 19075-5 7.2.8 STR06 = STRE STRE) -> 3 length-0 matches +CREATE VIEW rpr_ev_edge_empty_match_min2 AS +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){2,3}) + DEFINE A AS FALSE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min2'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------ + PATTERN ((a?){2,3}) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){2,3}) + DEFINE A AS FALSE +);'); + rpr_explain_filter +--------------------------------------------------------------------- + WindowAgg (actual rows=3.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{0,3} + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 8 total, 0 merged + NFA Contexts: 2 peak, 4 total, 0 pruned + NFA: 3 matched (len 0/0/0.0), 0 mismatched + -> Function Scan on generate_series s (actual rows=3.00 loops=1) +(9 rows) + +-- (A?){2,3} mixed: rows 1-2 match A (real), rows 3-4 fall back to empty +CREATE VIEW rpr_ev_edge_empty_match_mixed AS +SELECT count(*) OVER w +FROM generate_series(1, 4) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){2,3}) + DEFINE A AS v <= 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_mixed'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------ + PATTERN ((a?){2,3}) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 4) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){2,3}) + DEFINE A AS v <= 2 +);'); + rpr_explain_filter +--------------------------------------------------------------------- + WindowAgg (actual rows=4.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{0,3} + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 13 total, 0 merged + NFA Contexts: 4 peak, 5 total, 1 pruned + NFA: 3 matched (len 0/2/1.0), 0 mismatched + -> Function Scan on generate_series s (actual rows=4.00 loops=1) +(9 rows) + +-- (A? B?){2,3}: pure empty multi-element body -> 3 length-0 matches +CREATE VIEW rpr_ev_edge_empty_match_multi AS +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? B?){2,3}) + DEFINE A AS FALSE, B AS FALSE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_multi'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------------- + PATTERN ((a? b?){2,3}) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? B?){2,3}) + DEFINE A AS FALSE, B AS FALSE +);'); + rpr_explain_filter +--------------------------------------------------------------------- + WindowAgg (actual rows=3.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a? b?){2,3} + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 20 total, 0 merged + NFA Contexts: 2 peak, 4 total, 0 pruned + NFA: 3 matched (len 0/0/0.0), 0 mismatched + -> Function Scan on generate_series s (actual rows=3.00 loops=1) +(9 rows) + +-- Single row +CREATE VIEW rpr_ev_edge_single_row AS +SELECT count(*) OVER w +FROM generate_series(1, 1) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A) + DEFINE A AS TRUE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_single_row'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------- + PATTERN (a) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 1) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A) + DEFINE A AS TRUE +);'); + rpr_explain_filter +--------------------------------------------------------------------- + WindowAgg (actual rows=1.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 2 total, 0 merged + NFA Contexts: 2 peak, 2 total, 0 pruned + NFA: 1 matched (len 1/1/1.0), 0 mismatched + -> Function Scan on generate_series s (actual rows=1.00 loops=1) +(9 rows) + +-- Pattern longer than data +CREATE VIEW rpr_ev_edge_pattern_longer AS +SELECT count(*) OVER w +FROM generate_series(1, 5) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E F G H I J) + DEFINE + A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5, + F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_pattern_longer'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------------------- + PATTERN (a b c d e f g h i j) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 5) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E F G H I J) + DEFINE + A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5, + F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10 +);'); + rpr_explain_filter +--------------------------------------------------------------------- + WindowAgg (actual rows=5.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c d e f g h i j + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 6 total, 0 merged + NFA Contexts: 3 peak, 6 total, 4 pruned + NFA: 0 matched, 1 mismatched (len 5/5/5.0) + -> Function Scan on generate_series s (actual rows=5.00 loops=1) +(9 rows) + +-- All rows match as single match +CREATE VIEW rpr_ev_edge_single_match AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS TRUE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_single_match'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------- + PATTERN (a+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS TRUE +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 101 total, 0 merged + NFA Contexts: 2 peak, 51 total, 0 pruned + NFA: 1 matched (len 50/50/50.0), 0 mismatched + NFA: 49 absorbed (len 1/1/1.0), 0 skipped + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- ============================================================ +-- Complex Pattern Tests +-- ============================================================ +-- Nested groups +CREATE VIEW rpr_ev_cpx_nested AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A B) C)+) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_nested'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------- + PATTERN (((a b) c)+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A B) C)+) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=60.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b' c')+" + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 81 total, 0 merged + NFA Contexts: 4 peak, 61 total, 20 pruned + NFA: 1 matched (len 60/60/60.0), 0 mismatched + NFA: 19 absorbed (len 3/3/3.0), 20 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=60.00 loops=1) +(10 rows) + +-- Multiple alternations +CREATE VIEW rpr_ev_cpx_multi_alt AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (C | D | E)) + DEFINE + A AS cat = 'A', B AS cat = 'B', C AS cat = 'C', + D AS cat = 'D', E AS cat = 'E' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_multi_alt'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------------------- + PATTERN ((a | b) (c | d | e)) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (C | D | E)) + DEFINE + A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'', + D AS cat = ''D'', E AS cat = ''E'' +);'); + rpr_explain_filter +------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b) (c | d | e) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 423 total, 0 merged + NFA Contexts: 3 peak, 101 total, 40 pruned + NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 20 skipped (len 1/1/1.0) + -> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1) +(10 rows) + +-- Optional elements +CREATE VIEW rpr_ev_cpx_optional AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B? C) + DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_optional'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------- + PATTERN (a b? c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B? C) + DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b? c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 64 total, 0 merged + NFA Contexts: 3 peak, 51 total, 25 pruned + NFA: 12 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 12 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- Bounded quantifiers +CREATE VIEW rpr_ev_cpx_bounded AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,5} B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------- + PATTERN (a{2,5} b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,5} B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2,5} b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 9 peak, 311 total, 0 merged + NFA Contexts: 7 peak, 101 total, 0 pruned + NFA: 10 matched (len 6/6/6.0), 40 mismatched (len 6/6/6.0) + NFA: 0 absorbed, 50 skipped (len 1/5/3.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(10 rows) + +-- Star quantifier +CREATE VIEW rpr_ev_cpx_star AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B* C) + DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_star'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------- + PATTERN (a b* c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B* C) + DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b* c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 91 total, 0 merged + NFA Contexts: 3 peak, 51 total, 40 pruned + NFA: 5 matched (len 9/9/9.0), 0 mismatched + NFA: 0 absorbed, 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- ============================================================ +-- Real-world Pattern Examples +-- ============================================================ +-- Stock price pattern - V-shape (down then up) +CREATE VIEW rpr_ev_real_vshape AS +SELECT count(*) OVER w +FROM rpr_nfa_complex +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (D+ U+) + DEFINE D AS trend = 'D', U AS trend = 'U' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_vshape'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------- + PATTERN (d+ u+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_complex +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (D+ U+) + DEFINE D AS trend = ''D'', U AS trend = ''U'' +);'); + rpr_explain_filter +------------------------------------------------------------------- + WindowAgg (actual rows=30.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: d+" u+ + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 58 total, 0 merged + NFA Contexts: 3 peak, 31 total, 3 pruned + NFA: 3 matched (len 3/14/8.0), 1 mismatched (len 3/3/3.0) + NFA: 9 absorbed (len 1/1/1.0), 14 skipped (len 1/1/1.0) + -> Seq Scan on rpr_nfa_complex (actual rows=30.00 loops=1) +(10 rows) + +-- Stock price pattern - peak (up, stable, down) +CREATE VIEW rpr_ev_real_peak AS +SELECT count(*) OVER w +FROM rpr_nfa_complex +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (U+ S* D+) + DEFINE U AS trend = 'U', S AS trend = 'S', D AS trend = 'D' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_peak'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------- + PATTERN (u+ s* d+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_complex +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (U+ S* D+) + DEFINE U AS trend = ''U'', S AS trend = ''S'', D AS trend = ''D'' +);'); + rpr_explain_filter +------------------------------------------------------------------- + WindowAgg (actual rows=30.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: u+" s* d+ + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 76 total, 0 merged + NFA Contexts: 3 peak, 31 total, 1 pruned + NFA: 4 matched (len 3/11/7.2), 0 mismatched + NFA: 12 absorbed (len 1/1/1.0), 13 skipped (len 1/1/1.0) + -> Seq Scan on rpr_nfa_complex (actual rows=30.00 loops=1) +(10 rows) + +-- Consecutive increasing values (using PREV) +CREATE VIEW rpr_ev_real_increasing AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3,}) + DEFINE A AS v > PREV(v) OR PREV(v) IS NULL +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_increasing'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------- + PATTERN (a{3,}) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3,}) + DEFINE A AS v > PREV(v) OR PREV(v) IS NULL +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{3,}" + Nav Mark Lookback: 1 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 99 total, 0 merged + NFA Contexts: 2 peak, 51 total, 0 pruned + NFA: 1 matched (len 50/50/50.0), 0 mismatched + NFA: 49 absorbed (len 1/1/1.0), 0 skipped + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- ============================================================ +-- Performance-oriented Tests +-- ============================================================ +-- Large dataset with simple pattern +CREATE VIEW rpr_ev_perf_large_simple AS +SELECT count(*) OVER w +FROM generate_series(1, 1000) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_large_simple'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------ + PATTERN (a b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 1000) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + rpr_explain_filter +------------------------------------------------------------------------ + WindowAgg (actual rows=1000.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 1001 total, 0 merged + NFA Contexts: 2 peak, 1001 total, 0 pruned + NFA: 500 matched (len 2/2/2.0), 0 mismatched + NFA: 0 absorbed, 500 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=1000.00 loops=1) +(10 rows) + +-- Large dataset with absorption +CREATE VIEW rpr_ev_perf_large_absorb AS +SELECT count(*) OVER w +FROM generate_series(1, 1000) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 100 <> 0, B AS v % 100 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_large_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 1000) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 100 <> 0, B AS v % 100 = 0 +);'); + rpr_explain_filter +------------------------------------------------------------------------ + WindowAgg (actual rows=1000.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 1991 total, 0 merged + NFA Contexts: 2 peak, 1001 total, 0 pruned + NFA: 10 matched (len 100/100/100.0), 0 mismatched + NFA: 980 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=1000.00 loops=1) +(10 rows) + +-- High state merge ratio +CREATE VIEW rpr_ev_perf_high_merge AS +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B)+ C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_high_merge'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------- + PATTERN ((a | b)+ c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B)+ C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=500.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b)+ c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 9 peak, 3006 total, 0 merged + NFA Contexts: 3 peak, 501 total, 1 pruned + NFA: 166 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 332 skipped (len 1/2/1.5) + -> Function Scan on generate_series s (actual rows=500.00 loops=1) +(10 rows) + +-- ============================================================ +-- INITIAL vs no INITIAL comparison +-- ============================================================ +-- With INITIAL keyword +CREATE VIEW rpr_ev_initial_with AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_initial_with'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 91 total, 0 merged + NFA Contexts: 2 peak, 51 total, 0 pruned + NFA: 10 matched (len 5/5/5.0), 0 mismatched + NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- Without INITIAL keyword (same behavior currently) +CREATE VIEW rpr_ev_initial_without AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_initial_without'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 91 total, 0 merged + NFA Contexts: 2 peak, 51 total, 0 pruned + NFA: 10 matched (len 5/5/5.0), 0 mismatched + NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- ============================================================ +-- Quantifier Variations +-- ============================================================ +-- Plus quantifier +CREATE VIEW rpr_ev_quant_plus AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v % 4 <> 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_plus'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------- + PATTERN (a+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v % 4 <> 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=40.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 71 total, 0 merged + NFA Contexts: 3 peak, 41 total, 10 pruned + NFA: 10 matched (len 3/3/3.0), 0 mismatched + NFA: 20 absorbed (len 1/1/1.0), 0 skipped + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(10 rows) + +-- Star quantifier (zero or more) +CREATE VIEW rpr_ev_quant_star AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A* B) + DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_star'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a* b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A* B) + DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=40.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a*" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 102 total, 0 merged + NFA Contexts: 2 peak, 41 total, 10 pruned + NFA: 10 matched (len 3/3/3.0), 0 mismatched + NFA: 10 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(10 rows) + +-- Question mark (zero or one) +CREATE VIEW rpr_ev_quant_question AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A? B C) + DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_question'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------- + PATTERN (a? b c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A? B C) + DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=40.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a? b c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 82 total, 0 merged + NFA Contexts: 3 peak, 41 total, 10 pruned + NFA: 10 matched (len 3/3/3.0), 0 mismatched + NFA: 0 absorbed, 20 skipped (len 1/2/1.5) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(10 rows) + +-- Exact count {n} +CREATE VIEW rpr_ev_quant_exact AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3} B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_exact'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------- + PATTERN (a{3} b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3} B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{3} b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 51 total, 0 merged + NFA Contexts: 5 peak, 51 total, 0 pruned + NFA: 10 matched (len 4/4/4.0), 10 mismatched (len 4/4/4.0) + NFA: 0 absorbed, 30 skipped (len 1/3/2.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- Range {n,m} +CREATE VIEW rpr_ev_quant_range AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,4} B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_range'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------- + PATTERN (a{2,4} b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,4} B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2,4} b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 7 peak, 101 total, 0 merged + NFA Contexts: 5 peak, 51 total, 0 pruned + NFA: 10 matched (len 5/5/5.0), 0 mismatched + NFA: 0 absorbed, 40 skipped (len 1/4/2.5) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- At least {n,} +CREATE VIEW rpr_ev_quant_atleast AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3,} B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_atleast'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------- + PATTERN (a{3,} b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3,} B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{3,}" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 86 total, 0 merged + NFA Contexts: 2 peak, 51 total, 0 pruned + NFA: 5 matched (len 10/10/10.0), 0 mismatched + NFA: 40 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- ============================================================ +-- Regression Tests for Statistics Accuracy +-- ============================================================ +-- Verify state count accuracy +-- Pattern A+ B with 20 rows should show predictable state behavior +CREATE VIEW rpr_ev_reg_state_count AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_state_count'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 37 total, 0 merged + NFA Contexts: 2 peak, 21 total, 0 pruned + NFA: 4 matched (len 5/5/5.0), 0 mismatched + NFA: 12 absorbed (len 1/1/1.0), 4 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Verify context count with known absorption +CREATE VIEW rpr_ev_reg_ctx_absorb AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B C) + DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_ctx_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------- + PATTERN (a+ b c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B C) + DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=30.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 52 total, 0 merged + NFA Contexts: 3 peak, 31 total, 6 pruned + NFA: 3 matched (len 9/9/9.0), 0 mismatched + NFA: 18 absorbed (len 1/1/1.0), 3 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(10 rows) + +-- Verify match length with fixed-length pattern +CREATE VIEW rpr_ev_reg_matchlen AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_matchlen'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------- + PATTERN (a b c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=30.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 31 total, 0 merged + NFA Contexts: 3 peak, 31 total, 10 pruned + NFA: 10 matched (len 3/3/3.0), 0 mismatched + NFA: 0 absorbed, 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(10 rows) + +-- ============================================================ +-- Alternation Pattern Tests +-- ============================================================ +-- Simple alternation +CREATE VIEW rpr_ev_alt_simple AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) C) + DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_simple'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------ + PATTERN ((a | b) c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) C) + DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'' +);'); + rpr_explain_filter +------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b) c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 303 total, 0 merged + NFA Contexts: 3 peak, 101 total, 40 pruned + NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 20 skipped (len 1/1/1.0) + -> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1) +(10 rows) + +-- Multiple items in alternation +CREATE VIEW rpr_ev_alt_multi_item AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | C | D) E) + DEFINE + A AS cat = 'A', B AS cat = 'B', C AS cat = 'C', + D AS cat = 'D', E AS cat = 'E' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_multi_item'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------------- + PATTERN ((a | b | c | d) e) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | C | D) E) + DEFINE + A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'', + D AS cat = ''D'', E AS cat = ''E'' +);'); + rpr_explain_filter +------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b | c | d) e + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 505 total, 0 merged + NFA Contexts: 3 peak, 101 total, 0 pruned + NFA: 20 matched (len 2/2/2.0), 60 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 20 skipped (len 1/1/1.0) + -> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1) +(10 rows) + +-- Alternation with quantifiers +CREATE VIEW rpr_ev_alt_with_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B)+ C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_with_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------- + PATTERN ((a | b)+ c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B)+ C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b)+ c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 9 peak, 306 total, 0 merged + NFA Contexts: 3 peak, 51 total, 1 pruned + NFA: 16 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 32 skipped (len 1/2/1.5) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- Multiple alternatives (4+) +CREATE VIEW rpr_ev_alt_four_plus AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A | B | C | D | E) + DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_four_plus'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------------- + PATTERN (a | b | c | d | e) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A | B | C | D | E) + DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b | c | d | e) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 7 peak, 606 total, 0 merged + NFA Contexts: 2 peak, 101 total, 0 pruned + NFA: 100 matched (len 1/1/1.0), 0 mismatched + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(9 rows) + +-- Alternation at start +CREATE VIEW rpr_ev_alt_at_start AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B) C D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_at_start'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------- + PATTERN ((a | b) c d) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B) C D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=60.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b) c d + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 183 total, 0 merged + NFA Contexts: 3 peak, 61 total, 16 pruned + NFA: 15 matched (len 3/3/3.0), 14 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 15 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=60.00 loops=1) +(10 rows) + +-- Multiple sequential alternations +CREATE VIEW rpr_ev_alt_sequential AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B) C (D | E) F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_sequential'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------------------- + PATTERN ((a | b) c (d | e) f) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B) C (D | E) F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=100.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b) c (d | e) f + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 337 total, 0 merged + NFA Contexts: 3 peak, 101 total, 67 pruned + NFA: 0 matched, 33 mismatched (len 2/4/3.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(9 rows) + +-- Quantified alternatives +CREATE VIEW rpr_ev_alt_quantified AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+ | B+) C) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_quantified'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------- + PATTERN ((a+ | b+) c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+ | B+) C) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=60.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+" | b+") c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 223 total, 0 merged + NFA Contexts: 3 peak, 61 total, 1 pruned + NFA: 20 matched (len 2/2/2.0), 19 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 20 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=60.00 loops=1) +(10 rows) + +-- Alternation at end +CREATE VIEW rpr_ev_alt_at_end AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (C | D)) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_at_end'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------- + PATTERN (a b (c | d)) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (C | D)) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=60.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b (c | d) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 89 total, 0 merged + NFA Contexts: 3 peak, 61 total, 32 pruned + NFA: 14 matched (len 3/3/3.0), 0 mismatched + NFA: 0 absorbed, 14 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=60.00 loops=1) +(10 rows) + +-- Nested ALT at start of branch inside outer ALT +-- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element +CREATE VIEW rpr_ev_alt_nested_start AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A ((B | C) D | E)) + DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_nested_start'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------------- + PATTERN (a ((b | c) d | e)) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A ((B | C) D | E)) + DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a ((b | c) d | e) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 37 total, 0 merged + NFA Contexts: 3 peak, 21 total, 17 pruned + NFA: 0 matched, 3 mismatched (len 3/3/3.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(9 rows) + +-- Nested ALT at end of branch inside outer ALT +-- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch +CREATE VIEW rpr_ev_alt_nested_end AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C (A | B) | D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_nested_end'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------------- + PATTERN (c (a | b) | d) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C (A | B) | D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (c (a | b) | d) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 73 total, 0 merged + NFA Contexts: 3 peak, 21 total, 10 pruned + NFA: 5 matched (len 1/1/1.0), 5 mismatched (len 2/2/2.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(9 rows) + +-- Quantified group as the first alternation branch +-- Pattern: ((A B)+ | C) - leading group branch must open the enclosing paren +CREATE VIEW rpr_ev_alt_grp_first AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ | C) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_first'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------- + PATTERN ((a b)+ | c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ | C) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a' b')+" | c) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 67 total, 0 merged + NFA Contexts: 3 peak, 21 total, 7 pruned + NFA: 9 matched (len 1/2/1.4), 0 mismatched + NFA: 0 absorbed, 4 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Quantified group as the last alternation branch +-- Pattern: (C | (A B)+) - trailing group branch, no separator follows +CREATE VIEW rpr_ev_alt_grp_last AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | (A B)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_last'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------- + PATTERN (c | (a b)+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | (A B)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (c | (a' b')+") + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 67 total, 0 merged + NFA Contexts: 3 peak, 21 total, 7 pruned + NFA: 9 matched (len 1/2/1.4), 0 mismatched + NFA: 0 absorbed, 4 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Quantified group as the middle branch of a three-way alternation +-- Pattern: (C | (A B)+ | D) - separator before D must survive the group branch +CREATE VIEW rpr_ev_alt_grp_mid AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | (A B)+ | D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_mid'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------------- + PATTERN (c | (a b)+ | d) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | (A B)+ | D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (c | (a' b')+" | d) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 88 total, 0 merged + NFA Contexts: 3 peak, 21 total, 2 pruned + NFA: 14 matched (len 1/2/1.3), 0 mismatched + NFA: 0 absorbed, 4 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Quantified group as the first branch of a three-way alternation +-- Pattern: ((A B)+ | C | D) - leading group branch with two following branches +CREATE VIEW rpr_ev_alt_grp_first3 AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ | C | D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_first3'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------------- + PATTERN ((a b)+ | c | d) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ | C | D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a' b')+" | c | d) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 88 total, 0 merged + NFA Contexts: 3 peak, 21 total, 2 pruned + NFA: 14 matched (len 1/2/1.3), 0 mismatched + NFA: 0 absorbed, 4 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Bounded-quantifier group as the first alternation branch +-- Pattern: ((A B){2} | C) - leading group branch with a range quantifier +CREATE VIEW rpr_ev_alt_grp_bounded AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){2} | C) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------------- + PATTERN ((a b){2} | c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){2} | C) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a b){2} | c) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 63 total, 0 merged + NFA Contexts: 3 peak, 21 total, 11 pruned + NFA: 5 matched (len 1/1/1.0), 4 mismatched (len 3/3/3.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(9 rows) + +-- Two quantified groups in one alternation +-- Pattern: ((A B)+ | (C D)+) - both branches are groups +CREATE VIEW rpr_ev_alt_grp_both AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ | (C D)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_both'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------------ + PATTERN ((a b)+ | (c d)+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ | (C D)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a' b')+" | (c' d')+") + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 72 total, 0 merged + NFA Contexts: 3 peak, 21 total, 2 pruned + NFA: 9 matched (len 2/2/2.0), 0 mismatched + NFA: 0 absorbed, 9 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Leading group branch in an alternation nested in a sequence +-- Pattern: (((A B)+ | C) D) - inner alternation opens with a group branch +CREATE VIEW rpr_ev_alt_grp_seq_head AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B)+ | C) D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_seq_head'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------------- + PATTERN (((a b)+ | c) d) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B)+ | C) D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a' b')+" | c) d + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 67 total, 0 merged + NFA Contexts: 3 peak, 21 total, 6 pruned + NFA: 5 matched (len 2/2/2.0), 4 mismatched (len 3/3/3.0) + NFA: 0 absorbed, 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Trailing group branch in an alternation nested in a sequence +-- Pattern: ((C | (A B)+) D) - group as last branch, then a sequence element +CREATE VIEW rpr_ev_alt_grp_seq_tail AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((C | (A B)+) D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_seq_tail'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------------- + PATTERN ((c | (a b)+) d) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((C | (A B)+) D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (c | (a' b')+") d + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 67 total, 0 merged + NFA Contexts: 3 peak, 21 total, 6 pruned + NFA: 5 matched (len 2/2/2.0), 4 mismatched (len 3/3/3.0) + NFA: 0 absorbed, 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Quantified alternation whose first branch is a quantified group +-- Pattern: (((A B){2} | C)+) - single-ALT group wraps a leading group branch +CREATE VIEW rpr_ev_alt_grp_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B){2} | C)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------------ + PATTERN (((a b){2} | c)+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B){2} | C)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a b){2} | c)+ + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 78 total, 0 merged + NFA Contexts: 3 peak, 21 total, 11 pruned + NFA: 5 matched (len 1/1/1.0), 4 mismatched (len 3/3/3.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(9 rows) + +-- Unit (1,1) group as an alternation branch (emits no BEGIN/END) +-- Pattern: ((A B) | C) - control: takes the variable path, not deparse_rpr_group +CREATE VIEW rpr_ev_alt_grp_unit AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B) | C) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_unit'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------ + PATTERN ((a b) | c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B) | C) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b | c) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 63 total, 0 merged + NFA Contexts: 2 peak, 21 total, 7 pruned + NFA: 9 matched (len 1/2/1.4), 0 mismatched + NFA: 0 absorbed, 4 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Quantified variable as the first alternation branch +-- Pattern: (A+ | C) - control: deparse_rpr_var already opens the leading paren +CREATE VIEW rpr_ev_alt_var_first AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ | C) + DEFINE A AS v % 4 = 0, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_var_first'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------- + PATTERN (a+ | c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ | C) + DEFINE A AS v % 4 = 0, C AS v % 4 = 2 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+" | c) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 68 total, 0 merged + NFA Contexts: 3 peak, 21 total, 10 pruned + NFA: 10 matched (len 1/1/1.0), 0 mismatched + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(9 rows) + +-- Quantified group as the last branch of a three-way alternation +-- Pattern: (C | D | (A B)+) - control: trailing group needs no separator +CREATE VIEW rpr_ev_alt_grp_last3 AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | D | (A B)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_last3'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------------- + PATTERN (c | d | (a b)+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | D | (A B)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (c | d | (a' b')+") + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 88 total, 0 merged + NFA Contexts: 3 peak, 21 total, 2 pruned + NFA: 14 matched (len 1/2/1.3), 0 mismatched + NFA: 0 absorbed, 4 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Alternation nested in a leading branch must not swallow the trailing branch +-- Pattern: (D (A | B) | E) - inherited limit bounds the inner alternation +CREATE VIEW rpr_ev_alt_inner_bounded AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (D (A | B) | E) + DEFINE A AS v % 5 = 0, B AS v % 5 = 1, D AS v % 5 = 2, E AS v % 5 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_inner_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------------- + PATTERN (d (a | b) | e) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (D (A | B) | E) + DEFINE A AS v % 5 = 0, B AS v % 5 = 1, D AS v % 5 = 2, E AS v % 5 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (d (a | b) | e) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 71 total, 0 merged + NFA Contexts: 3 peak, 21 total, 12 pruned + NFA: 4 matched (len 1/1/1.0), 4 mismatched (len 2/2/2.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(9 rows) + +-- Group mid-branch followed by a sequence element needs no separator before it +-- Pattern: (C | (A B)+ D) - relative-next blocks a spurious separator at D +CREATE VIEW rpr_ev_alt_grp_then_seq AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | (A B)+ D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_then_seq'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------------- + PATTERN (c | (a b)+ d) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | (A B)+ D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (c | (a' b')+" d) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 88 total, 0 merged + NFA Contexts: 3 peak, 21 total, 6 pruned + NFA: 10 matched (len 1/1/1.0), 4 mismatched (len 3/3/3.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(9 rows) + +-- Quantified group wrapping a lone alternation: the ALT supplies the parens +-- Pattern: ((A | B)+) - loneAlt path, single pair of parens +CREATE VIEW rpr_ev_grp_lone_alt AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_lone_alt'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------- + PATTERN ((a | b)+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b)+ + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 93 total, 0 merged + NFA Contexts: 3 peak, 21 total, 10 pruned + NFA: 6 matched (len 1/2/1.7), 0 mismatched + NFA: 0 absorbed, 4 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Quantified group wrapping a sequence whose last element is an alternation +-- Pattern: ((A (B | C))+) - group paren plus a nested alternation paren +CREATE VIEW rpr_ev_grp_seq_alt AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A (B | C))+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_seq_alt'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------------- + PATTERN ((a (b | c))+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A (B | C))+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a (b | c))+ + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 35 total, 0 merged + NFA Contexts: 3 peak, 21 total, 12 pruned + NFA: 4 matched (len 2/2/2.0), 0 mismatched + NFA: 0 absorbed, 4 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Quantified group wrapping a sequence whose first element is an alternation +-- Pattern: (((A | B) C)+) - leading nested alternation inside a group sequence +CREATE VIEW rpr_ev_grp_alt_seq AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A | B) C)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_alt_seq'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------------- + PATTERN (((a | b) c)+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A | B) C)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a | b) c)+ + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 78 total, 0 merged + NFA Contexts: 3 peak, 21 total, 6 pruned + NFA: 5 matched (len 2/2/2.0), 4 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Alternation non-last in a non-last branch, stacked three deep: each level's +-- inherited limit must bound the inner alternation against the next branch +-- Pattern: (((A | B) C | D) E | F) - three nested inherited-limit boundaries +CREATE VIEW rpr_ev_alt_stack3 AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A | B) C | D) E | F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, + D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack3'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------------------ + PATTERN (((a | b) c | d) e | f) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A | B) C | D) E | F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, + D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (((a | b) c | d) e | f) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 147 total, 0 merged + NFA Contexts: 3 peak, 21 total, 4 pruned + NFA: 6 matched (len 1/2/1.5), 7 mismatched (len 2/3/2.4) + NFA: 0 absorbed, 3 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Same interaction stacked four deep, to exercise the induction one step further +-- Pattern: ((((A | B) C | D) E | F) G | H) - four nested inherited-limit boundaries +CREATE VIEW rpr_ev_alt_stack4 AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((((A | B) C | D) E | F) G | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack4'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------------------------- + PATTERN ((((a | b) c | d) e | f) g | h) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((((A | B) C | D) E | F) G | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((((a | b) c | d) e | f) g | h) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 7 peak, 189 total, 0 merged + NFA Contexts: 3 peak, 21 total, 6 pruned + NFA: 4 matched (len 1/2/1.5), 8 mismatched (len 2/3/2.6) + NFA: 0 absorbed, 2 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Three-deep stack whose innermost branch is a quantified group: the group's +-- skip-target jump must not be mistaken for a branch separator at any depth +-- Pattern: (((A | B)+ C | D) E | F) - inherited limit plus loneAlt at the base +CREATE VIEW rpr_ev_alt_stack3_grp AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A | B)+ C | D) E | F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, + D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack3_grp'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------------------- + PATTERN (((a | b)+ c | d) e | f) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A | B)+ C | D) E | F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, + D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (((a | b)+ c | d) e | f) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 11 peak, 177 total, 0 merged + NFA Contexts: 4 peak, 21 total, 4 pruned + NFA: 6 matched (len 1/2/1.5), 7 mismatched (len 2/4/3.1) + NFA: 0 absorbed, 3 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Alternation trailing a paren-less sequence (last element of a non-last +-- branch, no same-depth sibling to bound it), nested three deep +-- Pattern: (A (B (C | D) | E) | F) - each inner alternation is branch-tail +CREATE VIEW rpr_ev_alt_tail3 AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B (C | D) | E) | F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, + D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail3'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------------------ + PATTERN (a (b (c | d) | e) | f) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B (C | D) | E) | F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, + D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a (b (c | d) | e) | f) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 75 total, 0 merged + NFA Contexts: 3 peak, 21 total, 11 pruned + NFA: 6 matched (len 1/3/2.0), 0 mismatched + NFA: 0 absorbed, 3 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- Same branch-tail alternation nested four deep +-- Pattern: (A (B (C (D | E) | F) | G) | H) - branch-tail alternation x4 +CREATE VIEW rpr_ev_alt_tail4 AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B (C (D | E) | F) | G) | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail4'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------------------------- + PATTERN (a (b (c (d | e) | f) | g) | h) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B (C (D | E) | F) | G) | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a (b (c (d | e) | f) | g) | h) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 75 total, 0 merged + NFA Contexts: 3 peak, 21 total, 14 pruned + NFA: 4 matched (len 1/4/2.5), 0 mismatched + NFA: 0 absorbed, 2 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- A nested alternation tail neighbouring a multi-element sequence branch: the +-- branch boundary must split "...branch-tail ALT" from a plain "G A" sequence +-- Pattern: (A (B (C (D | E) | F) | G A) | H) - seq branch beside an ALT tail +CREATE VIEW rpr_ev_alt_tail_seqbranch AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B (C (D | E) | F) | G A) | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail_seqbranch'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------------------------------- + PATTERN (a (b (c (d | e) | f) | g a) | h) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B (C (D | E) | F) | G A) | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a (b (c (d | e) | f) | g a) | h) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 75 total, 0 merged + NFA Contexts: 3 peak, 21 total, 14 pruned + NFA: 4 matched (len 1/4/2.5), 0 mismatched + NFA: 0 absorbed, 2 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- A nested alternation that is sibling-bounded by a trailing sequence element +-- at the outer level (the ALT is not the branch tail; G follows it in-branch) +-- Pattern: ((A (B (C | D) | E) | F) G | H) - ALT bounded by a following element +CREATE VIEW rpr_ev_alt_mid_seqtail AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A (B (C | D) | E) | F) G | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_mid_seqtail'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------------------------- + PATTERN ((a (b (c | d) | e) | f) g | h) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A (B (C | D) | E) | F) G | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=20.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a (b (c | d) | e) | f) g | h) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 113 total, 0 merged + NFA Contexts: 3 peak, 21 total, 12 pruned + NFA: 4 matched (len 1/2/1.5), 2 mismatched (len 4/4/4.0) + NFA: 0 absorbed, 2 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(10 rows) + +-- ============================================================ +-- Group Pattern Tests +-- ============================================================ +-- Simple group +CREATE VIEW rpr_ev_grp_simple AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_simple'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------- + PATTERN ((a b)+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=40.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a' b')+" + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 61 total, 0 merged + NFA Contexts: 3 peak, 41 total, 0 pruned + NFA: 1 matched (len 40/40/40.0), 0 mismatched + NFA: 19 absorbed (len 2/2/2.0), 20 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(10 rows) + +-- Group with bounded quantifier +CREATE VIEW rpr_ev_grp_bounded AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B){2,4}) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------- + PATTERN ((a b){2,4}) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B){2,4}) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=40.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b){2,4} + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 51 total, 0 merged + NFA Contexts: 3 peak, 41 total, 5 pruned + NFA: 5 matched (len 8/8/8.0), 0 mismatched + NFA: 0 absorbed, 30 skipped (len 1/2/1.5) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(10 rows) + +-- Nested groups +CREATE VIEW rpr_ev_grp_nested AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A B){2})+) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_nested'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------- + PATTERN (((a b){2})+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A B){2})+) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=60.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a' b'){2}')+" + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 76 total, 0 merged + NFA Contexts: 4 peak, 61 total, 15 pruned + NFA: 1 matched (len 60/60/60.0), 0 mismatched + NFA: 14 absorbed (len 4/4/4.0), 30 skipped (len 1/2/1.5) + -> Function Scan on generate_series s (actual rows=60.00 loops=1) +(10 rows) + +-- Deep nesting (3+ levels) +CREATE VIEW rpr_ev_grp_deep AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((((A | B)+)+)+) + DEFINE A AS v % 2 = 0, B AS v % 2 = 1 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_deep'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------------- + PATTERN ((((a | b)+)+)+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((((A | B)+)+)+) + DEFINE A AS v % 2 = 0, B AS v % 2 = 1 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=40.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b)+ + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 243 total, 0 merged + NFA Contexts: 2 peak, 41 total, 0 pruned + NFA: 1 matched (len 40/40/40.0), 0 mismatched + NFA: 0 absorbed, 39 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(10 rows) + +-- Bounded quantifier on alternation +CREATE VIEW rpr_ev_grp_bounded_alt AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B){2,3} C) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_bounded_alt'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------------- + PATTERN ((a | b){2,3} c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B){2,3} C) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=60.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b){2,3} c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 8 peak, 320 total, 0 merged + NFA Contexts: 3 peak, 61 total, 2 pruned + NFA: 19 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 38 skipped (len 1/2/1.5) + -> Function Scan on generate_series s (actual rows=60.00 loops=1) +(10 rows) + +-- Nested groups with quantifiers +CREATE VIEW rpr_ev_grp_nested_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B)+ C)*) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_nested_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------- + PATTERN (((a b)+ c)*) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B)+ C)*) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=60.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a' b')+" c)* + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 9 peak, 178 total, 0 merged + NFA Contexts: 4 peak, 61 total, 20 pruned + NFA: 3 matched (len 0/57/19.0), 0 mismatched + NFA: 0 absorbed, 37 skipped (len 1/3/2.0) + -> Function Scan on generate_series s (actual rows=60.00 loops=1) +(10 rows) + +-- Partial nested quantification +CREATE VIEW rpr_ev_grp_partial_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A (B C)+)*) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_partial_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +-------------------------- + PATTERN ((a (b c)+)*) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A (B C)+)*) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=60.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a (b c)+)* + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 7 peak, 160 total, 0 merged + NFA Contexts: 4 peak, 61 total, 20 pruned + NFA: 3 matched (len 0/57/19.0), 0 mismatched + NFA: 0 absorbed, 37 skipped (len 1/3/2.0) + -> Function Scan on generate_series s (actual rows=60.00 loops=1) +(10 rows) + +-- ============================================================ +-- Window Function Combinations +-- ============================================================ +-- count(*) with pattern +CREATE VIEW rpr_ev_wfn_count AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_count'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=30.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 55 total, 0 merged + NFA Contexts: 2 peak, 31 total, 0 pruned + NFA: 6 matched (len 5/5/5.0), 0 mismatched + NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(10 rows) + +-- first_value with pattern +CREATE VIEW rpr_ev_wfn_first_value AS +SELECT first_value(v) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_first_value'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT first_value(v) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=30.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 55 total, 0 merged + NFA Contexts: 2 peak, 31 total, 0 pruned + NFA: 6 matched (len 5/5/5.0), 0 mismatched + NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(10 rows) + +-- last_value with pattern +CREATE VIEW rpr_ev_wfn_last_value AS +SELECT last_value(v) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_last_value'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT last_value(v) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=30.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 55 total, 0 merged + NFA Contexts: 2 peak, 31 total, 0 pruned + NFA: 6 matched (len 5/5/5.0), 0 mismatched + NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(10 rows) + +-- Multiple window functions +CREATE VIEW rpr_ev_wfn_multi AS +SELECT + count(*) OVER w, + first_value(v) OVER w, + last_value(v) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_multi'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT + count(*) OVER w, + first_value(v) OVER w, + last_value(v) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=30.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 55 total, 0 merged + NFA Contexts: 2 peak, 31 total, 0 pruned + NFA: 6 matched (len 5/5/5.0), 0 mismatched + NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(10 rows) + +-- ============================================================ +-- DEFINE Expression Variations +-- ============================================================ +-- Complex boolean expressions +CREATE VIEW rpr_ev_def_complex_bool AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE + A AS (v % 5 <> 0) AND (v % 3 <> 0), + B AS (v % 5 = 0) OR (v % 3 = 0) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_complex_bool'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE + A AS (v % 5 <> 0) AND (v % 3 <> 0), + B AS (v % 5 = 0) OR (v % 3 = 0) +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=50.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 78 total, 0 merged + NFA Contexts: 2 peak, 51 total, 6 pruned + NFA: 17 matched (len 2/3/2.6), 0 mismatched + NFA: 10 absorbed (len 1/1/1.0), 17 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(10 rows) + +-- Using PREV function +CREATE VIEW rpr_ev_def_prev AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (S U+ D+) + DEFINE + S AS TRUE, + U AS v > PREV(v), + D AS v < PREV(v) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_prev'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +---------------------- + PATTERN (s u+ d+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (S U+ D+) + DEFINE + S AS TRUE, + U AS v > PREV(v), + D AS v < PREV(v) +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=30.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: s u+ d+ + Nav Mark Lookback: 1 + Storage: Memory Maximum Storage: NkB + NFA States: 60 peak, 466 total, 0 merged + NFA Contexts: 31 peak, 31 total, 1 pruned + NFA: 0 matched, 29 mismatched (len 2/30/16.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(9 rows) + +-- Using 1-arg PREV (implicit offset 1) +CREATE VIEW rpr_ev_nav_prev1 AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS v > PREV(v) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT'; + line +------------------------- + PATTERN (a b+) + DEFINE + b AS (v > PREV(v)) ); +(3 rows) + +-- Using 1-arg NEXT (implicit offset 1) +CREATE VIEW rpr_ev_nav_next1 AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS v < NEXT(v) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT'; + line +------------------------- + PATTERN (a b+) + DEFINE + b AS (v < NEXT(v)) ); +(3 rows) + +-- Using 2-arg PREV (explicit offset) +CREATE VIEW rpr_ev_nav_prev2 AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS v > PREV(v, 2) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT'; + line +-------------------------------------- + PATTERN (a b+) + DEFINE + b AS (v > PREV(v, (2)::bigint)) ); +(3 rows) + +-- Using 2-arg NEXT (explicit offset) +CREATE VIEW rpr_ev_nav_next2 AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS v < NEXT(v, 2) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT'; + line +-------------------------------------- + PATTERN (a b+) + DEFINE + b AS (v < NEXT(v, (2)::bigint)) ); +(3 rows) + +-- Using NULL comparisons +CREATE VIEW rpr_ev_def_null AS +SELECT count(*) OVER w +FROM ( + SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v + FROM generate_series(1, 30) v +) t +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v IS NOT NULL, B AS v IS NULL +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_null'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------- + PATTERN (a+ b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM ( + SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v + FROM generate_series(1, 30) v +) t +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v IS NOT NULL, B AS v IS NULL +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=30.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 55 total, 0 merged + NFA Contexts: 2 peak, 31 total, 0 pruned + NFA: 6 matched (len 5/5/5.0), 0 mismatched + NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0) + -> Function Scan on generate_series v (actual rows=30.00 loops=1) +(10 rows) + +-- ============================================================ +-- Large Scale Statistics Verification +-- ============================================================ +-- 500 rows - verify statistics scale correctly +CREATE VIEW rpr_ev_scale_500rows AS +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B C) + DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_500rows'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +--------------------- + PATTERN (a+ b c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B C) + DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=500.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" b c + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 851 total, 0 merged + NFA Contexts: 3 peak, 501 total, 101 pruned + NFA: 50 matched (len 8/9/9.0), 0 mismatched + NFA: 299 absorbed (len 1/1/1.0), 50 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=500.00 loops=1) +(10 rows) + +-- High match count scenario +CREATE VIEW rpr_ev_scale_high_match AS +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_high_match'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------ + PATTERN (a b) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=500.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 501 total, 0 merged + NFA Contexts: 2 peak, 501 total, 0 pruned + NFA: 250 matched (len 2/2/2.0), 0 mismatched + NFA: 0 absorbed, 250 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=500.00 loops=1) +(10 rows) + +-- High skip count scenario +CREATE VIEW rpr_ev_scale_high_skip AS +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E) + DEFINE + A AS v % 100 = 1, + B AS v % 100 = 2, + C AS v % 100 = 3, + D AS v % 100 = 4, + E AS v % 100 = 5 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_high_skip'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------ + PATTERN (a b c d e) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E) + DEFINE + A AS v % 100 = 1, + B AS v % 100 = 2, + C AS v % 100 = 3, + D AS v % 100 = 4, + E AS v % 100 = 5 +);'); + rpr_explain_filter +----------------------------------------------------------------------- + WindowAgg (actual rows=500.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c d e + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 501 total, 0 merged + NFA Contexts: 3 peak, 501 total, 490 pruned + NFA: 5 matched (len 5/5/5.0), 0 mismatched + NFA: 0 absorbed, 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=500.00 loops=1) +(10 rows) + +-- +-- Planner optimization: optimize_window_clauses must not alter RPR frame +-- +-- optimize_window_clauses() replaces frame options via prosupport functions. +-- Affected functions: row_number, rank, dense_rank, percent_rank, cume_dist, +-- ntile. All would change the frame to ROWS UNBOUNDED PRECEDING, breaking +-- RPR's required ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING. +-- Test with row_number() as representative case. +-- +-- Without RPR: row_number() frame is optimized to ROWS UNBOUNDED PRECEDING +CREATE VIEW rpr_ev_opt_no_rpr AS +SELECT row_number() OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ORDER BY v + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +); +EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_no_rpr; + QUERY PLAN +-------------------------------------------------------------- + Subquery Scan on rpr_ev_opt_no_rpr + -> WindowAgg + Window: w AS (ORDER BY s.v ROWS UNBOUNDED PRECEDING) + -> Sort + Sort Key: s.v + -> Function Scan on generate_series s +(6 rows) + +-- With RPR: frame must remain ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +CREATE VIEW rpr_ev_opt_with_rpr AS +SELECT row_number() OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ORDER BY v + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + B AS v > PREV(v) +); +EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_with_rpr; + QUERY PLAN +-------------------------------------------------------------------------------------- + Subquery Scan on rpr_ev_opt_with_rpr + -> WindowAgg + Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: s.v + -> Function Scan on generate_series s +(8 rows) + +-- +-- Planner optimization: non-RPR and RPR windows that share the same base frame +-- after frame optimization are kept as separate WindowAgg nodes. +-- +CREATE VIEW rpr_ev_opt_mixed AS +SELECT + row_number() OVER w_normal AS rn_normal, + row_number() OVER w_rpr AS rn_rpr +FROM generate_series(1, 5) AS s(v) +WINDOW + w_normal AS (ORDER BY v RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), + w_rpr AS ( + ORDER BY v + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > 1 + ); +EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed; + QUERY PLAN +------------------------------------------------------------------------------------------ + Subquery Scan on rpr_ev_opt_mixed + -> WindowAgg + Window: w_rpr AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + -> WindowAgg + Window: w_normal AS (ORDER BY s.v ROWS UNBOUNDED PRECEDING) + -> Sort + Sort Key: s.v + -> Function Scan on generate_series s +(10 rows) + +-- +-- Planner optimization: find_window_run_conditions must not push down +-- RPR window function results as Run Conditions. +-- +-- find_window_run_conditions() pushes WHERE filters on monotonic window +-- functions into WindowAgg as Run Conditions for early termination. +-- With RPR's required frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED +-- FOLLOWING), the monotonic direction of the window function determines +-- which comparison operators allow pushdown: +-- INCREASING (<=): row_number, rank, dense_rank, percent_rank, +-- cume_dist, ntile +-- DECREASING (>): count(*). As the current row advances, the frame +-- (which ends at UNBOUNDED FOLLOWING) shrinks, so the +-- count decreases. +-- RPR window function results are match-dependent, not monotonic, so this +-- pushdown does not apply. Test with count(*) > 0 as a representative case. +-- +-- Without RPR: count(*) > 0 is pushed down as Run Condition +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT count(*) OVER w AS cnt + FROM generate_series(1, 10) AS s(v) + WINDOW w AS ( + ORDER BY v + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + ) +) t WHERE cnt > 0; + QUERY PLAN +-------------------------------------------------------------------------------------- + Subquery Scan on t + -> WindowAgg + Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Run Condition: (count(*) OVER w > 0) + -> Sort + Sort Key: s.v + -> Function Scan on generate_series s +(7 rows) + +-- With RPR: count(*) > 0 must not be pushed down as Run Condition +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT count(*) OVER w AS cnt + FROM generate_series(1, 10) AS s(v) + WINDOW w AS ( + ORDER BY v + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + B AS v > PREV(v) + ) +) t WHERE cnt > 0; + QUERY PLAN +-------------------------------------------------------------------------------------- + Subquery Scan on t + Filter: (t.cnt > 0) + -> WindowAgg + Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: s.v + -> Function Scan on generate_series s +(9 rows) + +-- ============================================================ +-- Nav Mark Lookback/Lookahead Tests +-- Verifies planner-computed navigation offsets for tuplestore trim. +-- Lookback: how far back from currentpos (PREV, LAST, compound PREV_LAST/NEXT_LAST). +-- Lookahead: how far forward from match_start (FIRST, compound PREV_FIRST/NEXT_FIRST). +-- ============================================================ +-- Prepare statement for host variable offset test below +PREPARE rpr_nav_offset_prep(int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > PREV(v, $1) +); +-- No navigation function: offset 0 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > 0 +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + -> Function Scan on generate_series s +(5 rows) + +-- NEXT only: no backward navigation, offset 0 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v < NEXT(v) +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + -> Function Scan on generate_series s +(5 rows) + +-- PREV(v): implicit offset 1 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > PREV(v) +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 1 + -> Function Scan on generate_series s +(5 rows) + +-- PREV(v, 3): explicit constant offset 3 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > PREV(v, 3) +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 3 + -> Function Scan on generate_series s +(5 rows) + +-- Two PREV with different offsets: max(1, 5) = 5 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(v, 1) < v AND PREV(v, 5) < v +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 5 + -> Function Scan on generate_series s +(5 rows) + +-- Host variable offset: custom plan resolves $1=2 to constant 2 +EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 2 + -> Function Scan on generate_series s +(5 rows) + +-- Force generic plan: offset becomes "runtime" (Param node) +SET plan_cache_mode = force_generic_plan; +EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: runtime + -> Function Scan on generate_series s +(5 rows) + +RESET plan_cache_mode; +DEALLOCATE rpr_nav_offset_prep; +-- FIRST(v): retain all (references match_start row) +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > FIRST(v) +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: 0 + Nav Mark Lookahead: 0 + -> Function Scan on generate_series s +(6 rows) + +-- LAST(v, 1): backward reach 1, same as PREV(v, 1) +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS LAST(v, 1) > 0 +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: 1 + -> Function Scan on generate_series s +(5 rows) + +-- LAST(v) without offset + PREV(v): no match_start dependency, offset 1 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS LAST(v) > PREV(v) +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 1 + -> Function Scan on generate_series s +(5 rows) + +-- Compound PREV(FIRST(val, 1), 2): lookback from match_start, firstOffset = 1-2 = -1 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(FIRST(v, 1), 2) > 0 +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: 0 + Nav Mark Lookahead: -1 + -> Function Scan on generate_series s +(6 rows) + +-- Compound NEXT(FIRST(val), 3): firstOffset = 0+3 = 3 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(FIRST(v), 3) > 0 +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: 0 + Nav Mark Lookahead: 3 + -> Function Scan on generate_series s +(6 rows) + +-- Compound PREV(LAST(val), 2): lookback = 0+2 = 2 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(LAST(v), 2) > 0 +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 2 + -> Function Scan on generate_series s +(5 rows) + +-- Compound NEXT(LAST(val, 1), 3): lookback = max(1-3, 0) = 0 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(LAST(v, 1), 3) > 0 +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: 0 + -> Function Scan on generate_series s +(5 rows) + +-- Compound PREV(LAST(val, N), M): constant near-overflow (N+M just fits int64) +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(LAST(v, 4611686018427387903), 4611686018427387903) IS NOT NULL +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: 9223372036854775806 + -> Function Scan on generate_series s +(5 rows) + +-- Compound PREV(LAST(val, N), M): constant overflow -> retain all +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(LAST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: retain all + -> Function Scan on generate_series s +(5 rows) + +-- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> infinite +-- N + M overflows int64; forward reach is unbounded, displayed as infinite. +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(FIRST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: 0 + Nav Mark Lookahead: infinite + -> Function Scan on generate_series s +(6 rows) + +-- Compound PREV(LAST(val, $1), $2): parameter lookback overflow -> retain all +-- EXPLAIN shows "runtime" (plan-level); EXPLAIN ANALYZE shows "retain all" +-- (executor-resolved). +PREPARE test_overflow_lookback(int8, int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(LAST(v, $1), $2) IS NOT NULL +); +SET plan_cache_mode = force_generic_plan; +EXPLAIN (COSTS OFF) EXECUTE test_overflow_lookback(4611686018427387904, 4611686018427387904); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: runtime + -> Function Scan on generate_series s +(5 rows) + +EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF) + EXECUTE test_overflow_lookback(4611686018427387904, 4611686018427387904); + QUERY PLAN +---------------------------------------------------------------------- + WindowAgg (actual rows=10.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: retain all + Storage: Memory Maximum Storage: 17kB + NFA States: 1 peak, 11 total, 0 merged + NFA Contexts: 2 peak, 11 total, 10 pruned + NFA: 0 matched, 0 mismatched + -> Function Scan on generate_series s (actual rows=10.00 loops=1) +(9 rows) + +RESET plan_cache_mode; +DEALLOCATE test_overflow_lookback; +-- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> infinite +PREPARE test_overflow_lookahead(int8, int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(FIRST(v, $1), $2) IS NOT NULL +); +SET plan_cache_mode = force_generic_plan; +EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF) + EXECUTE test_overflow_lookahead(4611686018427387904, 4611686018427387904); + QUERY PLAN +---------------------------------------------------------------------- + WindowAgg (actual rows=10.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: 0 + Nav Mark Lookahead: infinite + Storage: Memory Maximum Storage: 17kB + NFA States: 1 peak, 11 total, 0 merged + NFA Contexts: 2 peak, 11 total, 10 pruned + NFA: 0 matched, 0 mismatched + -> Function Scan on generate_series s (actual rows=10.00 loops=1) +(10 rows) + +RESET plan_cache_mode; +DEALLOCATE test_overflow_lookahead; +-- Runtime (non-overflow) lookahead: a small parameter offset under a generic +-- plan keeps the FIRST offset unresolved at plan time, so EXPLAIN reports +-- "Nav Mark Lookahead: runtime" rather than a concrete value. +PREPARE p_first_runtime(int8, int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(FIRST(v, $1), $2) IS NOT NULL +); +SET plan_cache_mode = force_generic_plan; +EXPLAIN (COSTS OFF) EXECUTE p_first_runtime(1, 1); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: 0 + Nav Mark Lookahead: runtime + -> Function Scan on generate_series s +(6 rows) + +RESET plan_cache_mode; +DEALLOCATE p_first_runtime; +-- PREV(v) + PREV(v, $1): NEEDS_EVAL path must account for implicit lookback=1 +-- Previously, eval_nav_max_offset_walker skipped PREV(v) when offset_arg was +-- NULL, causing maxOffset=0 when $1=0, which would trim the row needed by +-- PREV(v). Verify this executes without "cannot fetch row before mark" error. +PREPARE test_prev_implicit_offset(int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(v) IS NOT NULL AND PREV(v, $1) IS NOT NULL +); +EXECUTE test_prev_implicit_offset(0); + count +------- + 0 + 9 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +(10 rows) + +DEALLOCATE test_prev_implicit_offset; +-- NEEDS_EVAL executor offset paths: a Param nav offset stays non-Const under a +-- generic plan, so the planner marks the offset NEEDS_EVAL and the executor +-- resolves it at init via eval_define_offsets -> visit_nav_exec. Each query +-- below exercises a different navigation arm of that walker. +-- Simple FIRST(v, $1): forward-reach FIRST arm. +PREPARE test_eval_first(int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS FIRST(v, $1) IS NOT NULL +); +SET plan_cache_mode = force_generic_plan; +EXECUTE test_eval_first(1); + count +------- + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +(10 rows) + +RESET plan_cache_mode; +DEALLOCATE test_eval_first; +-- Bare LAST(v, $1): backward-reach LAST-with-offset arm. +PREPARE test_eval_last(int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS LAST(v, $1) >= 0 +); +SET plan_cache_mode = force_generic_plan; +EXECUTE test_eval_last(1); + count +------- + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +(10 rows) + +RESET plan_cache_mode; +DEALLOCATE test_eval_last; +-- Compound NEXT(LAST(v, $1), $2): backward-reach NEXT_LAST arm. +PREPARE test_eval_nextlast(int8, int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(LAST(v, $1), $2) > 0 +); +SET plan_cache_mode = force_generic_plan; +EXECUTE test_eval_nextlast(1, 1); + count +------- + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +(10 rows) + +RESET plan_cache_mode; +DEALLOCATE test_eval_nextlast; +-- Compound PREV(FIRST(v, $1), $2): forward-reach PREV_FIRST arm. +PREPARE test_eval_prevfirst(int8, int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(FIRST(v, $1), $2) IS NOT NULL +); +SET plan_cache_mode = force_generic_plan; +EXECUTE test_eval_prevfirst(1, 1); + count +------- + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +(10 rows) + +RESET plan_cache_mode; +DEALLOCATE test_eval_prevfirst; +-- Runtime error: negative offset at execution time +PREPARE test_runtime_neg_offset(int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(v, $1) IS NOT NULL +); +EXECUTE test_runtime_neg_offset(-1); +ERROR: row pattern navigation offset must not be negative +DEALLOCATE test_runtime_neg_offset; +-- Runtime error: null offset at execution time +PREPARE test_runtime_null_offset(int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(v, $1) IS NOT NULL +); +EXECUTE test_runtime_null_offset(NULL); +ERROR: row pattern navigation offset must not be null +DEALLOCATE test_runtime_null_offset; diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out new file mode 100644 index 00000000000..d0b5cc5a44f --- /dev/null +++ b/src/test/regress/expected/rpr_integration.out @@ -0,0 +1,1608 @@ +-- ============================================================ +-- RPR Integration Tests +-- Planner optimization interaction tests for Row Pattern Recognition +-- ============================================================ +-- +-- Verifies that each planner optimization correctly handles RPR windows. +-- Even if individual optimizations are tested elsewhere, this file +-- provides a single checkpoint for all planner/RPR interactions. +-- +-- A. Planner Optimization Protection Tests +-- A1. Frame optimization bypass +-- A2. Run condition pushdown bypass +-- A3. Window dedup prevention (RPR vs non-RPR) +-- A4. Window dedup prevention (same PATTERN, different DEFINE) +-- A5. Unused window removal prevention +-- A6. Inverse transition bypass +-- A7. Cost estimation RPR awareness +-- A8. Subquery flattening prevention +-- A9. DEFINE expression non-propagation +-- A10. RPR + LIMIT +-- +-- B. Integration Scenario Tests +-- B1. RPR + CTE +-- B2. RPR + JOIN +-- B3. RPR + Set operations +-- B4. RPR + Prepared statements +-- B5. RPR + Partitioned table +-- B6. RPR + LATERAL +-- B7. RPR + Recursive CTE +-- B8. RPR + Incremental sort +-- B9. RPR + Volatile function in DEFINE +-- B10. RPR + Correlated subquery +-- +CREATE TABLE rpr_integ (id INT, val INT); +INSERT INTO rpr_integ VALUES + (1, 10), (2, 20), (3, 15), (4, 25), (5, 5), + (6, 30), (7, 35), (8, 20), (9, 40), (10, 45); +-- ============================================================ +-- A1. Frame optimization bypass +-- ============================================================ +-- Verify that optimize_window_clauses() does not apply frame +-- optimization to RPR windows. Both queries below use the same input +-- frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) with +-- row_number(), whose prosupport handles +-- SupportRequestOptimizeWindowClause and triggers frame rewriting. +-- In the non-RPR baseline the planner rewrites the frame to ROWS +-- UNBOUNDED PRECEDING, while in the RPR case the guard in +-- optimize_window_clauses() blocks the rewrite and the frame is +-- preserved as specified. +-- Non-RPR baseline: the planner rewrites the frame to ROWS UNBOUNDED PRECEDING. +EXPLAIN (COSTS OFF) +SELECT row_number() OVER w FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING); + QUERY PLAN +------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS UNBOUNDED PRECEDING) + -> Sort + Sort Key: id + -> Seq Scan on rpr_integ +(5 rows) + +-- RPR case: the frame is preserved as specified. +EXPLAIN (COSTS OFF) +SELECT row_number() OVER w FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: id + -> Seq Scan on rpr_integ +(7 rows) + +-- ============================================================ +-- A2. Run condition pushdown bypass +-- ============================================================ +-- Verify that find_window_run_conditions() does not push a monotonic +-- filter down as a Run Condition on RPR windows. RPR match counts are +-- determined by pattern matching rather than by a monotonic +-- accumulation over the frame, so a filter such as "cnt > 0" cannot be +-- used to stop evaluating the window function early. +-- Non-RPR baseline: the filter is expected to appear as a Run Condition. +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) +) t WHERE cnt > 0; + QUERY PLAN +----------------------------------------------------------------------------------------------- + Subquery Scan on t + -> WindowAgg + Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Run Condition: (count(*) OVER w > 0) + -> Sort + Sort Key: rpr_integ.id + -> Seq Scan on rpr_integ +(7 rows) + +-- RPR case: the filter must appear as a Filter above the WindowAgg, +-- not as a Run Condition. +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t WHERE cnt > 0; + QUERY PLAN +----------------------------------------------------------------------------------------------- + Subquery Scan on t + Filter: (t.cnt > 0) + -> WindowAgg + Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: rpr_integ.id + -> Seq Scan on rpr_integ +(9 rows) + +-- Verify that the RPR query still returns every row whose match count is +-- greater than zero, confirming the filter is evaluated above the +-- WindowAgg rather than cutting off pattern matching prematurely. +SELECT * FROM ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t WHERE cnt > 0 +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 3 | 15 | 2 + 5 | 5 | 3 + 8 | 20 | 3 +(4 rows) + +-- ============================================================ +-- A3. Window dedup prevention (RPR vs non-RPR) +-- ============================================================ +-- Verify that PostgreSQL does not merge an RPR window with a non-RPR +-- window even when both share the same ORDER BY and frame +-- specification. RPR pattern matching produces results that are +-- semantically different from a plain frame-based aggregate, so the +-- two windows must remain as separate WindowAgg nodes. Inline window +-- specs are used throughout this section because only inline windows +-- are subject to the dedup path; distinct named windows are always +-- kept separate regardless of equivalence. +-- Non-RPR baseline: two inline windows with identical spec are +-- deduped by the planner into a single WindowAgg node, confirming +-- that the dedup path is active for non-RPR windows. +EXPLAIN (COSTS OFF) +SELECT + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS cnt, + sum(val) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS total +FROM rpr_integ; + QUERY PLAN +-------------------------------------------------------------------------------- + WindowAgg + Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + -> Sort + Sort Key: id + -> Seq Scan on rpr_integ +(5 rows) + +-- An inline RPR window and an inline non-RPR window share the same +-- ORDER BY and frame but must remain as distinct WindowAgg nodes. +EXPLAIN (COSTS OFF) +SELECT + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS rpr_cnt, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS normal_cnt +FROM rpr_integ; + QUERY PLAN +-------------------------------------------------------------------------------------- + WindowAgg + Window: w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + -> WindowAgg + Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: id + -> Seq Scan on rpr_integ +(9 rows) + +-- Verify that the two windows return independent counts per row, +-- confirming they were not merged into a single WindowAgg. +SELECT + id, val, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS rpr_cnt, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS normal_cnt +FROM rpr_integ +ORDER BY id; + id | val | rpr_cnt | normal_cnt +----+-----+---------+------------ + 1 | 10 | 2 | 10 + 2 | 20 | 0 | 9 + 3 | 15 | 2 | 8 + 4 | 25 | 0 | 7 + 5 | 5 | 3 | 6 + 6 | 30 | 0 | 5 + 7 | 35 | 0 | 4 + 8 | 20 | 3 | 3 + 9 | 40 | 0 | 2 + 10 | 45 | 0 | 1 +(10 rows) + +-- ============================================================ +-- A4. Window dedup prevention (same PATTERN, different DEFINE) +-- ============================================================ +-- Verify that inline-window dedup does not merge two RPR windows +-- that share the same PATTERN structure but have different DEFINE +-- conditions. Even though the ORDER BY, frame, and PATTERN coincide, +-- the differing DEFINE expressions classify rows differently and +-- must therefore yield two separate WindowAgg nodes. Inline specs +-- are used here because dedup only applies to inline windows. +-- Baseline: two inline RPR windows that are structurally identical +-- (same PARTITION BY, ORDER BY, frame, PATTERN, and DEFINE) are deduped by the +-- parser into a single WindowAgg node, confirming that parser-level dedup is +-- active for RPR windows whose DEFINE matches. +EXPLAIN (COSTS OFF) +SELECT + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS cnt, + sum(val) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS total +FROM rpr_integ; + QUERY PLAN +-------------------------------------------------------------------------------- + WindowAgg + Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: id + -> Seq Scan on rpr_integ +(7 rows) + +-- Two inline RPR windows with the same PATTERN but opposite DEFINE +-- conditions must remain as separate WindowAgg nodes. +EXPLAIN (COSTS OFF) +SELECT + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS cnt_up, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val < PREV(val)) AS cnt_down +FROM rpr_integ; + QUERY PLAN +-------------------------------------------------------------------------------------- + WindowAgg + Window: w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> WindowAgg + Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: id + -> Seq Scan on rpr_integ +(11 rows) + +-- Verify that the two windows return different counts per row, +-- confirming the DEFINE conditions were not collapsed by dedup. +SELECT + id, val, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS cnt_up, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val < PREV(val)) AS cnt_down +FROM rpr_integ +ORDER BY id; + id | val | cnt_up | cnt_down +----+-----+--------+---------- + 1 | 10 | 2 | 0 + 2 | 20 | 0 | 2 + 3 | 15 | 2 | 0 + 4 | 25 | 0 | 2 + 5 | 5 | 3 | 0 + 6 | 30 | 0 | 0 + 7 | 35 | 0 | 2 + 8 | 20 | 3 | 0 + 9 | 40 | 0 | 0 + 10 | 45 | 0 | 0 +(10 rows) + +-- ============================================================ +-- A5. Unused window removal prevention +-- ============================================================ +-- Verify that remove_unused_subquery_outputs() does not drop an RPR +-- window function when the outer query does not reference its result. +-- The WindowAgg node performs the pattern match itself; without it, +-- the match would be silently skipped. The plan must contain a +-- WindowAgg node beneath the outer Aggregate. +EXPLAIN (COSTS OFF) +SELECT count(*) FROM ( + SELECT count(*) OVER w FROM rpr_integ + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > PREV(val)) +) t; + QUERY PLAN +------------------------------------------------------------------------- + Aggregate + -> WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 1 + -> Seq Scan on rpr_integ +(6 rows) + +SELECT count(*) FROM ( + SELECT count(*) OVER w FROM rpr_integ + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > PREV(val)) +) t; + count +------- + 10 +(1 row) + +-- The DEFINE expression references PREV(val), so the window must be +-- preserved even if the outer query only aggregates over the count. +-- The plan must still contain a WindowAgg with the PATTERN/DEFINE +-- intact. +EXPLAIN (COSTS OFF) +SELECT count(*), sum(c) FROM ( + SELECT count(*) OVER w AS c FROM rpr_integ + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > PREV(val)) +) t; + QUERY PLAN +------------------------------------------------------------------------- + Aggregate + -> WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 1 + -> Seq Scan on rpr_integ +(6 rows) + +SELECT count(*), sum(c) FROM ( + SELECT count(*) OVER w AS c FROM rpr_integ + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > PREV(val)) +) t; + count | sum +-------+----- + 10 | 6 +(1 row) + +-- The DEFINE expression contains no navigation, but the RPR window +-- must still be preserved because the match structure itself affects +-- the count. The plan must retain the WindowAgg. +EXPLAIN (COSTS OFF) +SELECT count(*), sum(c) FROM ( + SELECT count(*) OVER w AS c FROM rpr_integ + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS TRUE) +) t; + QUERY PLAN +------------------------------------------------------------------------- + Aggregate + -> WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + -> Seq Scan on rpr_integ +(6 rows) + +SELECT count(*), sum(c) FROM ( + SELECT count(*) OVER w AS c FROM rpr_integ + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS TRUE) +) t; + count | sum +-------+----- + 10 | 10 +(1 row) + +-- XXX: "val" is non-resjunk in the subquery output and is not +-- referenced by the outer query. Without a guard, +-- remove_unused_subquery_outputs() would replace it with NULL in +-- the subquery output, and that replacement propagates to the +-- scan's targetlist -- DEFINE would then evaluate with NULL +-- inputs. The targetlist has no way to distinguish "exposed to +-- the outer query" from "referenced only by DEFINE", so the +-- optimization cannot be applied selectively. The column guard +-- in allpaths.c blocks this replacement for any column referenced +-- by an RPR DEFINE clause, keeping the WindowAgg with DEFINE +-- active in the plan. +EXPLAIN (COSTS OFF) +SELECT count(*) FROM ( + SELECT val, count(*) OVER w FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t; + QUERY PLAN +----------------------------------------------------------------------------------------------- + Aggregate + -> WindowAgg + Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: rpr_integ.id + -> Seq Scan on rpr_integ +(8 rows) + +SELECT count(*) FROM ( + SELECT val, count(*) OVER w FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t; + count +------- + 10 +(1 row) + +-- The same guard must also cover a whole-row Var. Writing the bare +-- relation name (rpr_integ) in DEFINE resolves to a whole-row Var, whose +-- attribute number is 0. remove_unused_subquery_outputs() matches the +-- guard on attribute number, so the whole-row Var is retained as a single +-- entry while the unused scalar "val" output is still replaced with NULL; +-- DEFINE evaluates against the intact row, and the match is unchanged. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT sum(c) FROM ( + SELECT val, count(*) OVER w AS c FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS rpr_integ IS NOT NULL) +) t; + QUERY PLAN +----------------------------------------------------------------------------------------------- + Aggregate + Output: sum((count(*) OVER w)) + -> WindowAgg + Output: NULL::integer, count(*) OVER w, rpr_integ.id, rpr_integ.* + Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 0 + -> Sort + Output: rpr_integ.id, rpr_integ.* + Sort Key: rpr_integ.id + -> Seq Scan on public.rpr_integ + Output: rpr_integ.id, rpr_integ.* +(12 rows) + +SELECT sum(c) FROM ( + SELECT val, count(*) OVER w AS c FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS rpr_integ IS NOT NULL) +) t; + sum +----- + 10 +(1 row) + +-- ============================================================ +-- A6. Inverse transition bypass +-- ============================================================ +-- Verify that RPR windows do not use the moving aggregate (inverse +-- transition) optimization. Moving aggregates maintain state by +-- adding arriving rows and subtracting leaving rows, but an RPR +-- reduced frame is not a sliding window; the set of rows included in +-- the frame is determined by pattern matching and cannot be derived +-- incrementally from the previous frame. +-- sum() would normally be eligible for the moving aggregate +-- optimization; under RPR it must be computed from scratch over each +-- reduced frame, and the returned values must match the pattern. +-- Note: inverse-transition selection is not exposed in the plan, so +-- there is no direct EXPLAIN assertion for it. The structural +-- guarantee is that RPR uses its own navigation mark, distinct from +-- the moving-aggregate mark, so the inverse-transition path is +-- never reached on the RPR side. This test verifies that +-- separation indirectly: if inverse transition leaked into the RPR +-- path, state would mix across match boundaries and pattern_sum +-- would diverge from the expected output, failing the regression. +SELECT id, val, + sum(val) OVER w AS pattern_sum +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id; + id | val | pattern_sum +----+-----+------------- + 1 | 10 | 30 + 2 | 20 | + 3 | 15 | 40 + 4 | 25 | + 5 | 5 | 70 + 6 | 30 | + 7 | 35 | + 8 | 20 | 105 + 9 | 40 | + 10 | 45 | +(10 rows) + +-- A custom aggregate logs how its transition functions are called: msfunc +-- appends '+' for a forward transition, minvfunc appends '-' for an inverse +-- transition. Over an RPR reduced frame only '+' appears, confirming the +-- inverse-transition path is not used, and the logged values show which rows +-- each frame feeds the aggregate. +CREATE FUNCTION rpr_logging_sfunc(text, anyelement) RETURNS text AS +$$ SELECT COALESCE($1, '') || '*' || quote_nullable($2) $$ LANGUAGE SQL IMMUTABLE; +CREATE FUNCTION rpr_logging_msfunc(text, anyelement) RETURNS text AS +$$ SELECT COALESCE($1, '') || '+' || quote_nullable($2) $$ LANGUAGE SQL IMMUTABLE; +CREATE FUNCTION rpr_logging_minvfunc(text, anyelement) RETURNS text AS +$$ SELECT $1 || '-' || quote_nullable($2) $$ LANGUAGE SQL IMMUTABLE; +CREATE AGGREGATE rpr_logging_agg (anyelement) +( + stype = text, + sfunc = rpr_logging_sfunc, + mstype = text, + msfunc = rpr_logging_msfunc, + minvfunc = rpr_logging_minvfunc +); +-- Match 1 is id1..id3 (v = NULL,'a','b') and match 2 is id4..id6 +-- (v = NULL,'c','d'); B extends the match while k increases. Every frame +-- shows '+' only (no '-'), and the two matches never share state across their +-- boundary. +SELECT id, v, + rpr_logging_agg(v) OVER w AS logged +FROM (VALUES + (1, 1, NULL), + (2, 3, 'a'), + (3, 5, 'b'), + (4, 2, NULL), + (5, 4, 'c'), + (6, 6, 'd') +) AS t(id, k, v) +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS k > PREV(k)) +ORDER BY id; + id | v | logged +----+---+--------------- + 1 | | +NULL+'a'+'b' + 2 | a | + 3 | b | + 4 | | +NULL+'c'+'d' + 5 | c | + 6 | d | +(6 rows) + +DROP AGGREGATE rpr_logging_agg(anyelement); +DROP FUNCTION rpr_logging_sfunc(text, anyelement); +DROP FUNCTION rpr_logging_msfunc(text, anyelement); +DROP FUNCTION rpr_logging_minvfunc(text, anyelement); +-- ============================================================ +-- A7. Cost estimation RPR awareness +-- ============================================================ +-- cost_windowagg() must account for DEFINE expression evaluation cost. +-- Verify RPR WindowAgg cost > non-RPR WindowAgg cost. +CREATE FUNCTION get_windowagg_cost(query text) RETURNS numeric AS $$ +DECLARE + plan json; + cost numeric; +BEGIN + EXECUTE 'EXPLAIN (FORMAT JSON) ' || query INTO plan; + cost := (plan->0->'Plan'->>'Total Cost')::numeric; + RETURN cost; +END; +$$ LANGUAGE plpgsql; +SELECT get_windowagg_cost( + 'SELECT count(*) OVER w FROM rpr_integ + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+ C+) DEFINE B AS val > PREV(val), C AS val < PREV(val))') + > + get_windowagg_cost( + 'SELECT count(*) OVER w FROM rpr_integ + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)') + AS rpr_cost_is_higher; + rpr_cost_is_higher +-------------------- + t +(1 row) + +DROP FUNCTION get_windowagg_cost(text); +-- ============================================================ +-- A8. Subquery flattening prevention +-- ============================================================ +-- Verify that a subquery containing an RPR window is not flattened +-- into the outer query. is_simple_subquery() already blocks pullup +-- for subqueries with window functions in general; this test confirms +-- the rule continues to apply to RPR windows, so EXPLAIN must still +-- show a Subquery Scan above the RPR WindowAgg. +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) sub +WHERE cnt > 0; + QUERY PLAN +----------------------------------------------------------------------------------------------- + Subquery Scan on sub + Filter: (sub.cnt > 0) + -> WindowAgg + Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: rpr_integ.id + -> Seq Scan on rpr_integ +(9 rows) + +-- ============================================================ +-- A9. DEFINE expression non-propagation +-- ============================================================ +-- Verify that DEFINE expressions are not propagated into the +-- targetlist of any upper WindowAgg node. Only the column references +-- consumed by DEFINE should be passed up; the full DEFINE expression +-- is meaningful only inside the RPR WindowAgg that owns it. +-- EXPLAIN VERBOSE is therefore expected to show a clean targetlist on +-- the outer WindowAgg, with no DEFINE-derived expression leaking in. +-- Note: columns referenced by DEFINE (e.g., "val") may appear as +-- resjunk entries in upper WindowAgg targetlists -- but that is harmless. +-- The claim here is limited to the full DEFINE boolean expression. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT + count(*) OVER w_rpr AS rpr_cnt, + count(*) OVER w_normal AS normal_cnt +FROM rpr_integ +WINDOW + w_rpr AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)), + w_normal AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING); + QUERY PLAN +--------------------------------------------------------------------------------------------------- + WindowAgg + Output: (count(*) OVER w_rpr), count(*) OVER w_normal, id, val + Window: w_normal AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + -> WindowAgg + Output: id, val, count(*) OVER w_rpr + Window: w_rpr AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Output: id, val + Sort Key: rpr_integ.id + -> Seq Scan on public.rpr_integ + Output: id, val +(13 rows) + +SELECT + count(*) OVER w_rpr AS rpr_cnt, + count(*) OVER w_normal AS normal_cnt +FROM rpr_integ +WINDOW + w_rpr AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)), + w_normal AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) +ORDER BY rpr_cnt DESC, normal_cnt DESC; + rpr_cnt | normal_cnt +---------+------------ + 3 | 6 + 3 | 3 + 2 | 10 + 2 | 8 + 0 | 9 + 0 | 7 + 0 | 5 + 0 | 4 + 0 | 2 + 0 | 1 +(10 rows) + +-- ============================================================ +-- A10. RPR + LIMIT +-- ============================================================ +-- LIMIT must not interfere with RPR pattern matching. The Limit +-- node must sit above the WindowAgg so that pattern matching runs +-- on the full partition first; the result is then a prefix of the +-- un-LIMITed output. Pushing Limit below the WindowAgg would +-- truncate input before matching and silently drop valid matches. +EXPLAIN (COSTS OFF) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +LIMIT 5; + QUERY PLAN +------------------------------------------------------------------------------------- + Limit + -> WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: id + -> Seq Scan on rpr_integ +(8 rows) + +-- Reference: un-LIMITed result against which the LIMIT 5 result is +-- compared. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 2 + 4 | 25 | 0 + 5 | 5 | 3 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 20 | 3 + 9 | 40 | 0 + 10 | 45 | 0 +(10 rows) + +-- LIMIT 5 case; the first five rows must match the reference above. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +LIMIT 5; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 2 + 4 | 25 | 0 + 5 | 5 | 3 +(5 rows) + +-- ============================================================ +-- B1. RPR + CTE +-- ============================================================ +-- Verify that an RPR window embedded inside a CTE behaves the same as +-- a direct RPR query: +-- (1) A single-reference CTE is inlined by the planner and yields +-- per-row results identical to the direct RPR query. +-- (2) A multi-reference CTE is materialized (CTE Scan appears in +-- the plan) so pattern matching runs once, and every reference +-- observes the same match results. +-- Baseline: direct RPR produces the per-row reference output. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 2 + 4 | 25 | 0 + 5 | 5 | 3 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 20 | 3 + 9 | 40 | 0 + 10 | 45 | 0 +(10 rows) + +-- Single-reference CTE: plan has no "CTE rpr_result" scope, showing +-- the CTE was inlined into the surrounding query. +EXPLAIN (COSTS OFF) +WITH rpr_result AS ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) +SELECT id, val, cnt FROM rpr_result ORDER BY id; + QUERY PLAN +----------------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: rpr_integ.id + -> Seq Scan on rpr_integ +(7 rows) + +-- Result must match the baseline row-for-row. +WITH rpr_result AS ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) +SELECT id, val, cnt FROM rpr_result ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 2 + 4 | 25 | 0 + 5 | 5 | 3 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 20 | 3 + 9 | 40 | 0 + 10 | 45 | 0 +(10 rows) + +-- Multi-reference CTE (self-join): plan has a "CTE rpr_result" scope +-- and CTE Scan nodes on both sides, showing the CTE was materialized +-- and pattern matching ran only once. +EXPLAIN (COSTS OFF) +WITH rpr_result AS ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) +SELECT r1.id, r1.cnt +FROM rpr_result r1 +JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt +WHERE r1.cnt > 0 +ORDER BY r1.id; + QUERY PLAN +------------------------------------------------------------------------------------------------- + Merge Join + Merge Cond: ((r2.id = r1.id) AND (r2.cnt = r1.cnt)) + CTE rpr_result + -> WindowAgg + Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: rpr_integ.id + -> Seq Scan on rpr_integ + -> Incremental Sort + Sort Key: r2.id, r2.cnt + Presorted Key: r2.id + -> CTE Scan on rpr_result r2 + -> Sort + Sort Key: r1.id, r1.cnt + -> CTE Scan on rpr_result r1 + Filter: (cnt > 0) +(18 rows) + +-- Result: both references see the same match counts, so the self-join +-- preserves all matched rows from the baseline. +WITH rpr_result AS ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) +SELECT r1.id, r1.cnt +FROM rpr_result r1 +JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt +WHERE r1.cnt > 0 +ORDER BY r1.id; + id | cnt +----+----- + 1 | 2 + 3 | 2 + 5 | 3 + 8 | 3 +(4 rows) + +-- ============================================================ +-- B2. RPR + JOIN +-- ============================================================ +-- Verify that an RPR subquery can be joined with another relation. +-- Two aspects are checked against a non-RPR baseline: +-- (1) Flattening: a non-RPR subquery is pulled up by the planner +-- (no Subquery Scan in the plan); an RPR subquery is kept +-- un-flattened (Subquery Scan above WindowAgg). +-- (2) Join correctness: the join aligns each RPR match row with +-- the dimension-table row on the same key. +CREATE TABLE rpr_integ2 (id INT, label TEXT); +INSERT INTO rpr_integ2 VALUES + (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'), + (6, 'f'), (7, 'g'), (8, 'h'), (9, 'i'), (10, 'j'); +-- Baseline: a non-RPR subquery is flattened by the planner. No +-- Subquery Scan node appears; the inner SELECT is merged into the +-- outer join. +EXPLAIN (COSTS OFF) +SELECT r.id, r.val, j.label +FROM (SELECT id, val FROM rpr_integ) r +JOIN rpr_integ2 j ON r.id = j.id +ORDER BY r.id; + QUERY PLAN +-------------------------------------- + Merge Join + Merge Cond: (j.id = rpr_integ.id) + -> Sort + Sort Key: j.id + -> Seq Scan on rpr_integ2 j + -> Sort + Sort Key: rpr_integ.id + -> Seq Scan on rpr_integ +(8 rows) + +-- RPR subquery JOIN: the Subquery Scan is preserved above the +-- WindowAgg, confirming the RPR subquery is not flattened. +EXPLAIN (COSTS OFF) +SELECT r.id, r.cnt, j.label +FROM ( + SELECT id, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) r +JOIN rpr_integ2 j ON r.id = j.id +WHERE r.cnt > 0 +ORDER BY r.id; + QUERY PLAN +----------------------------------------------------------------------------------------------------- + Merge Join + Merge Cond: (r.id = j.id) + -> Subquery Scan on r + Filter: (r.cnt > 0) + -> WindowAgg + Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: rpr_integ.id + -> Seq Scan on rpr_integ + -> Sort + Sort Key: j.id + -> Seq Scan on rpr_integ2 j +(14 rows) + +-- Result: matched RPR rows align with dimension rows on id, showing +-- the join correctly pairs per-row match counts with their labels. +SELECT r.id, r.cnt, j.label +FROM ( + SELECT id, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) r +JOIN rpr_integ2 j ON r.id = j.id +WHERE r.cnt > 0 +ORDER BY r.id; + id | cnt | label +----+-----+------- + 1 | 2 | a + 3 | 2 | c + 5 | 3 | e + 8 | 3 | h +(4 rows) + +-- ============================================================ +-- B3. RPR + Set operations +-- ============================================================ +-- Verify that RPR results combine correctly with non-RPR results +-- under a UNION ALL. The plan must show an Append node with two +-- independent child plans: the RPR branch with Pattern/DEFINE active, +-- and the non-RPR branch with a plain WindowAgg. Each child scans +-- the base relation on its own and contributes its rows to the +-- unioned output. +-- Plan: Append with two independent children. The RPR branch has a +-- WindowAgg carrying Pattern/Nav Mark Lookback; the non-RPR branch +-- has a plain WindowAgg with no pattern metadata. +EXPLAIN (COSTS OFF) +SELECT id, cnt, 'rpr' AS source FROM ( + SELECT id, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t WHERE cnt > 0 +UNION ALL +SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source +FROM rpr_integ +ORDER BY source, id; + QUERY PLAN +----------------------------------------------------------------------------------------------------------- + Sort + Sort Key: ('rpr'::text), t.id + -> Append + -> Subquery Scan on t + Filter: (t.cnt > 0) + -> WindowAgg + Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: rpr_integ.id + -> Seq Scan on rpr_integ + -> WindowAgg + Window: w1 AS (ORDER BY rpr_integ_1.id) + -> Sort + Sort Key: rpr_integ_1.id + -> Seq Scan on rpr_integ rpr_integ_1 +(17 rows) + +-- Result: rows from both branches are present in the unioned output. +-- The RPR branch emits only matched rows (cnt > 0), while the +-- non-RPR branch emits all rows with its own count values. +SELECT id, cnt, 'rpr' AS source FROM ( + SELECT id, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t WHERE cnt > 0 +UNION ALL +SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source +FROM rpr_integ +ORDER BY source, id; + id | cnt | source +----+-----+-------- + 1 | 1 | normal + 2 | 2 | normal + 3 | 3 | normal + 4 | 4 | normal + 5 | 5 | normal + 6 | 6 | normal + 7 | 7 | normal + 8 | 8 | normal + 9 | 9 | normal + 10 | 10 | normal + 1 | 2 | rpr + 3 | 2 | rpr + 5 | 3 | rpr + 8 | 3 | rpr +(14 rows) + +-- ============================================================ +-- B4. RPR + Prepared statements +-- ============================================================ +-- Verify that RPR queries survive the prepared-statement path by +-- exercising both plancache modes with a parameter that feeds into +-- RPR's navigation offset (PREV(val, $1)). The parameter surfaces +-- the RPR-specific plancache difference: +-- - custom plan: "Nav Mark Lookback" is resolved to the literal +-- parameter value at plan time (e.g., "Nav Mark Lookback: 1"). +-- - generic plan: "Nav Mark Lookback" is deferred to execution and +-- appears as "Nav Mark Lookback: runtime" in the plan. +-- The result must be identical under both modes. +-- Register the prepared statement; DEFINE uses PREV(val, $1) so the +-- parameter reaches RPR's navigation machinery. +PREPARE rpr_prev(int) AS +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val, $1)) +ORDER BY id; +-- Custom plan: Nav Mark Lookback resolved to the literal 1. +SET plan_cache_mode = force_custom_plan; +EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: id + -> Seq Scan on rpr_integ +(7 rows) + +EXECUTE rpr_prev(1); + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 2 + 4 | 25 | 0 + 5 | 5 | 3 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 20 | 3 + 9 | 40 | 0 + 10 | 45 | 0 +(10 rows) + +-- Generic plan: Nav Mark Lookback deferred to execution, shown as +-- "runtime" in the plan. Result must match the custom-plan result +-- exactly. +SET plan_cache_mode = force_generic_plan; +EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: runtime + -> Sort + Sort Key: id + -> Seq Scan on rpr_integ +(7 rows) + +EXECUTE rpr_prev(1); + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 2 + 4 | 25 | 0 + 5 | 5 | 3 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 20 | 3 + 9 | 40 | 0 + 10 | 45 | 0 +(10 rows) + +-- Negative runtime nav offset under the generic plan: init clamps it to 0 for +-- trim sizing, but the per-row navigation rejects the negative offset. +EXECUTE rpr_prev(-1); +ERROR: row pattern navigation offset must not be negative +RESET plan_cache_mode; +DEALLOCATE rpr_prev; +-- ============================================================ +-- B5. RPR + Partitioned table +-- ============================================================ +-- Verify that RPR pattern matching works correctly when the source +-- relation is partitioned. The planner must gather rows from every +-- partition into a single ordered stream before RPR can see them, +-- because pattern matching is sequential across the entire +-- partition-by group and cannot be performed independently on each +-- table partition. +CREATE TABLE rpr_part (id INT, val INT) PARTITION BY RANGE (id); +CREATE TABLE rpr_part_1 PARTITION OF rpr_part FOR VALUES FROM (1) TO (6); +CREATE TABLE rpr_part_2 PARTITION OF rpr_part FOR VALUES FROM (6) TO (11); +INSERT INTO rpr_part SELECT id, val FROM rpr_integ; +-- Plan: partition scans are combined with Append (or Merge Append), +-- sorted into a single ordered stream, and fed into one WindowAgg +-- that performs RPR pattern matching across the combined stream. +EXPLAIN (COSTS OFF) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_part +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id; + QUERY PLAN +---------------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY rpr_part.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: rpr_part.id + -> Append + -> Seq Scan on rpr_part_1 + -> Seq Scan on rpr_part_2 +(9 rows) + +-- Baseline: the same query against the non-partitioned rpr_integ +-- produces the per-row reference output. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 2 + 4 | 25 | 0 + 5 | 5 | 3 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 20 | 3 + 9 | 40 | 0 + 10 | 45 | 0 +(10 rows) + +-- Result against the partitioned table must match the baseline +-- row-for-row. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_part +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 2 + 4 | 25 | 0 + 5 | 5 | 3 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 20 | 3 + 9 | 40 | 0 + 10 | 45 | 0 +(10 rows) + +DROP TABLE rpr_part; +-- ============================================================ +-- B6. RPR + LATERAL +-- ============================================================ +-- RPR inside a LATERAL subquery. Qualified column references from +-- the outer query are not yet supported in DEFINE, so this tests +-- the basic case where LATERAL provides the correlation filter +-- (WHERE id <= o.id) and DEFINE uses only local columns. The plan +-- must show a Nested Loop driving the outer relation into the inner +-- subquery scan, with the RPR WindowAgg re-executed for each outer +-- row and the correlation surfacing as a scan-level Filter on +-- "id <= o.id". +-- Plan: Nested Loop with the RPR WindowAgg in the inner leg, driven +-- by the filtered outer rows (o.id IN (5, 10)). +EXPLAIN (COSTS OFF) +SELECT o.id AS outer_id, r.id, r.cnt +FROM rpr_integ o, +LATERAL ( + SELECT id, count(*) OVER w AS cnt + FROM rpr_integ + WHERE id <= o.id + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) r +WHERE r.cnt > 0 AND o.id IN (5, 10) +ORDER BY o.id, r.id; + QUERY PLAN +----------------------------------------------------------------------------------------------------------- + Sort + Sort Key: o.id, r.id + -> Nested Loop + -> Seq Scan on rpr_integ o + Filter: (id = ANY ('{5,10}'::integer[])) + -> Subquery Scan on r + Filter: (r.cnt > 0) + -> WindowAgg + Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: rpr_integ.id + -> Seq Scan on rpr_integ + Filter: (id <= o.id) +(15 rows) + +-- Result: for each of the two outer ids (5 and 10), the LATERAL +-- subquery produces RPR match counts over the restricted input. +SELECT o.id AS outer_id, r.id, r.cnt +FROM rpr_integ o, +LATERAL ( + SELECT id, count(*) OVER w AS cnt + FROM rpr_integ + WHERE id <= o.id + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) r +WHERE r.cnt > 0 AND o.id IN (5, 10) +ORDER BY o.id, r.id; + outer_id | id | cnt +----------+----+----- + 5 | 1 | 2 + 5 | 3 | 2 + 10 | 1 | 2 + 10 | 3 | 2 + 10 | 5 | 3 + 10 | 8 | 3 +(6 rows) + +-- ============================================================ +-- B7. RPR + Recursive CTE +-- ============================================================ +-- Verify that RPR is rejected inside a recursive query. +-- ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5 (R010) cite CREATE +-- RECURSIVE VIEW examples and state that "row pattern matching +-- is prohibited in recursive queries". The formal rule lives in +-- ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)f): a potentially +-- recursive shall not contain a or . Per 3)e), every +-- under WITH RECURSIVE is "potentially +-- recursive", so the rejection covers the base (non-recursive) +-- leg too, not just the self-referencing leg. +-- WITH RECURSIVE: RPR in the base leg is rejected even though the +-- base leg never references the recursive CTE name. +WITH RECURSIVE seq AS ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) + UNION ALL + SELECT id + 100, val, cnt FROM seq WHERE id < 3 +) +SELECT id, val, cnt FROM seq ORDER BY id; +ERROR: cannot use row pattern recognition in a recursive query +LINE 6: PATTERN (A B+) + ^ +-- CREATE RECURSIVE VIEW: rewritten by makeRecursiveViewSelect() +-- into WITH RECURSIVE, so the same rejection applies. This is +-- the form ISO/IEC 19075-5 6.17.5 cites verbatim. +CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS + SELECT id, val, count(*) OVER w + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)); +ERROR: cannot use row pattern recognition in a recursive query +LINE 6: PATTERN (A B+) + ^ +-- ============================================================ +-- B8. RPR + Incremental sort +-- ============================================================ +-- Verify that RPR pattern matching works correctly when the input +-- to WindowAgg arrives via an incremental sort. The index on (id) +-- provides presorted input for the first ORDER BY key, so +-- "ORDER BY id, val" lets the planner use Incremental Sort to order +-- only on the second key. The plan must show Incremental Sort +-- below the RPR WindowAgg, and RPR must produce the same per-row +-- match counts as it would with a plain Sort. +CREATE INDEX rpr_integ_id_idx ON rpr_integ (id); +SET enable_seqscan = off; +-- Plan: RPR WindowAgg above an Incremental Sort above an Index Scan. +-- The Incremental Sort declares "Presorted Key: id" and sorts only +-- on val within each id group. +EXPLAIN (COSTS OFF) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id, val + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)); + QUERY PLAN +------------------------------------------------------------------------------------ + WindowAgg + Window: w AS (ORDER BY id, val ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Incremental Sort + Sort Key: id, val + Presorted Key: id + -> Index Scan using rpr_integ_id_idx on rpr_integ +(8 rows) + +-- Result: RPR over the incrementally sorted stream produces match +-- counts per row. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id, val + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id, val; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 2 + 4 | 25 | 0 + 5 | 5 | 3 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 20 | 3 + 9 | 40 | 0 + 10 | 45 | 0 +(10 rows) + +RESET enable_seqscan; +DROP INDEX rpr_integ_id_idx; +-- ============================================================ +-- B9. RPR + Volatile function in DEFINE +-- ============================================================ +-- Volatile functions in DEFINE are rejected in the planner. Under +-- RPR's NFA engine the same row's DEFINE predicate may be evaluated +-- multiple times (backtracking, PREV/NEXT navigation), so a volatile +-- result would make pattern matching non-deterministic. STABLE and +-- IMMUTABLE callees are accepted. +-- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val) + AND length('x') = 1 + AND to_char(date '2026-01-01', 'YYYY') = '2026') +ORDER BY id; + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 2 + 4 | 25 | 0 + 5 | 5 | 3 + 6 | 30 | 0 + 7 | 35 | 0 + 8 | 20 | 3 + 9 | 40 | 0 + 10 | 45 | 0 +(10 rows) + +-- Volatile (random) is rejected. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val) AND random() >= 0.0) +ORDER BY id; +ERROR: volatile functions are not allowed in DEFINE clause +LINE 6: DEFINE B AS val > PREV(val) AND random() >= 0.0) + ^ +-- ============================================================ +-- B10. RPR + Correlated subquery in WHERE +-- ============================================================ +-- Verify that an RPR window placed inside a correlated scalar +-- subquery is executed once per outer row. DEFINE still references +-- only local columns (qualified refs from the outer query are not +-- supported in DEFINE); the correlation lives in the subquery's +-- WHERE clause as "i.id <= o.id". The plan must show a SubPlan +-- attached to the outer scan, with the RPR WindowAgg driven by a +-- per-row scan filter carrying the correlation predicate. +-- Plan: SubPlan attached to the outer Seq Scan; the inner scan +-- carries "Filter: (id <= o.id)", confirming the correlation is +-- evaluated per outer row. +EXPLAIN (COSTS OFF) +SELECT o.id, o.val, + (SELECT count(*) OVER w + FROM rpr_integ i + WHERE i.id <= o.id + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) + ORDER BY id + LIMIT 1) AS first_cnt +FROM rpr_integ o +ORDER BY o.id; + QUERY PLAN +----------------------------------------------------------------------------------------------------- + Sort + Sort Key: o.id + -> Seq Scan on rpr_integ o + SubPlan expr_1 + -> Limit + -> WindowAgg + Window: w AS (ORDER BY i.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: i.id + -> Seq Scan on rpr_integ i + Filter: (id <= o.id) +(13 rows) + +-- Result: each outer row receives the first_cnt from its own +-- correlated RPR subquery. +SELECT o.id, o.val, + (SELECT count(*) OVER w + FROM rpr_integ i + WHERE i.id <= o.id + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) + ORDER BY id + LIMIT 1) AS first_cnt +FROM rpr_integ o +ORDER BY o.id; + id | val | first_cnt +----+-----+----------- + 1 | 10 | 0 + 2 | 20 | 2 + 3 | 15 | 2 + 4 | 25 | 2 + 5 | 5 | 2 + 6 | 30 | 2 + 7 | 35 | 2 + 8 | 20 | 2 + 9 | 40 | 2 + 10 | 45 | 2 +(10 rows) + +-- A column referenced only by DEFINE must not keep an unrelated column that +-- merely shares its attribute number. DEFINE references a (rpr_over1); c +-- (rpr_over2) has the same attno but is unused, so it must be dropped. +CREATE TABLE rpr_over1 (a int); +CREATE TABLE rpr_over2 (c int); +INSERT INTO rpr_over1 VALUES (1),(2),(3); +INSERT INTO rpr_over2 VALUES (1),(2),(3); +EXPLAIN (VERBOSE, COSTS OFF) +SELECT cnt FROM ( + SELECT a AS oa, c AS oc, count(*) OVER w AS cnt + FROM rpr_over1 CROSS JOIN rpr_over2 + WINDOW w AS (ORDER BY a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (X+) DEFINE X AS a > 0) +) s; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Subquery Scan on s + Output: s.cnt + -> WindowAgg + Output: rpr_over1.a, NULL::integer, count(*) OVER w + Window: w AS (ORDER BY rpr_over1.a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: x+" + Nav Mark Lookback: 0 + -> Sort + Output: rpr_over1.a + Sort Key: rpr_over1.a + -> Nested Loop + Output: rpr_over1.a + -> Seq Scan on public.rpr_over1 + Output: rpr_over1.a + -> Materialize + -> Seq Scan on public.rpr_over2 +(16 rows) + +DROP TABLE rpr_over1, rpr_over2; +-- Cleanup +DROP TABLE rpr_integ; +DROP TABLE rpr_integ2; diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out new file mode 100644 index 00000000000..22d244707c6 --- /dev/null +++ b/src/test/regress/expected/rpr_nfa.out @@ -0,0 +1,4971 @@ +-- ============================================================ +-- RPR NFA Tests +-- Tests for Row Pattern Recognition NFA Runtime Execution +-- ============================================================ +-- +-- This test suite validates the NFA (Non-deterministic Finite +-- Automaton) runtime execution engine in nodeWindowAgg.c, +-- focusing on update_reduced_frame and related functions. +-- +-- Test Strategy: +-- Diagonal pattern style using ARRAY flags to explicitly +-- control which pattern variables match at each row. +-- +-- Test Coverage: +-- Basic NFA Flow (match->absorb->advance) +-- Absorption Optimization +-- Context Lifecycle Management +-- Advance Phase (Epsilon Transitions) +-- Match Phase (Variable Matching) +-- Frame Boundary Handling +-- State Management (Deduplication) +-- Statistics and Diagnostics +-- Quantifier Runtime Behavior +-- Pathological Pattern Protection +-- Alternation Runtime Behavior +-- Deep Nested Groups +-- SKIP Options (Runtime) +-- INITIAL Mode (Runtime) +-- Frame Boundary Variations +-- Special Partition Cases +-- DEFINE Special Cases +-- Absorption Dynamic Flags +-- Zero-Consumption Cycle Detection +-- +-- Responsibility: +-- - NFA runtime execution paths +-- - Context/State lifecycle management +-- - Runtime boundary conditions and protections +-- +-- NOT tested here (covered in other files): +-- - Pattern parsing/optimization (rpr_base.sql) +-- - EXPLAIN output (rpr_explain.sql) +-- - PREV/NEXT semantics (rpr.sql) +-- ============================================================ +-- ============================================================ +-- Basic NFA Flow +-- ============================================================ +-- Simple sequential pattern +WITH test_sequential AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (5, ARRAY['_']) -- No match + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_sequential +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {B} | | + 3 | {C} | | + 4 | {D} | | + 5 | {_} | | +(5 rows) + +-- Quantified pattern (A+ B+ C+) +WITH test_quantified AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['B']), + (6, ARRAY['C']), + (7, ARRAY['C']), + (8, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_quantified +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B+ C+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 7 + 2 | {A} | 2 | 7 + 3 | {A} | 3 | 7 + 4 | {B} | | + 5 | {B} | | + 6 | {C} | | + 7 | {C} | | + 8 | {_} | | +(8 rows) + +-- Optional pattern (A B? C) +WITH test_optional AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['C']), -- B skipped + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['C']), -- B matched + (6, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_optional +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B? C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {C} | | + 3 | {A} | 3 | 5 + 4 | {B} | | + 5 | {C} | | + 6 | {_} | | +(6 rows) + +-- Alternation pattern (A (B|C) D) +WITH test_alternation AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), -- First branch + (3, ARRAY['D']), + (4, ARRAY['A']), + (5, ARRAY['C']), -- Second branch + (6, ARRAY['D']), + (7, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alternation +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A (B | C) D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {B} | | + 3 | {D} | | + 4 | {A} | 4 | 6 + 5 | {C} | | + 6 | {D} | | + 7 | {_} | | +(7 rows) + +-- ============================================================ +-- Absorption Optimization +-- ============================================================ +-- Absorbable pattern (A+) +WITH test_absorbable AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_absorbable +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | 2 | 4 + 3 | {A} | 3 | 4 + 4 | {A} | 4 | 4 + 5 | {_} | | +(5 rows) + +-- Mixed absorbable/non-absorbable ((A+) | B) +WITH test_mixed_absorption AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_mixed_absorption +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A+) | B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {A} | 2 | 3 + 3 | {A} | 3 | 3 + 4 | {B} | 4 | 4 + 5 | {_} | | +(5 rows) + +-- State coverage (same elemIdx, different count) +WITH test_state_coverage AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_state_coverage +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A{2,} B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | 2 | 4 + 3 | {A} | | + 4 | {B} | | + 5 | {_} | | +(5 rows) + +-- Reluctant pattern (A+?) - not absorbable +-- Compare with greedy A+ above: reluctant excluded from absorption. +-- Each context produces minimum match independently. +WITH test_reluctant_absorption AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_absorption +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+?) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 1 + 2 | {A} | 2 | 2 + 3 | {A} | 3 | 3 + 4 | {A} | 4 | 4 + 5 | {_} | | +(5 rows) + +-- Absorption with fixed suffix: A+ B +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) + +-- 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) + +-- Non-absorbable: A B+ (unbounded not in first position) +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) + +-- GROUP merge enables absorption: (A B) (A B)+ optimized to (A B){2,} +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) + +-- Two consecutive unbounded groups: (A B)+ (C D)+ +-- The leading group (A B)+ is absorbable (unbounded multi-element); (C D)+ is +-- a distinct sibling group that does not merge with it. When the leading group +-- exits into the sibling, its body leaf-VAR count must be cleared so it does +-- not leak into the sibling's shared depth slot. +WITH test_absorb_two_groups AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, ARRAY['C']), + (8, ARRAY['D']) + ) 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_two_groups +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+ (C D)+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {B} | | + 3 | {C} | | + 4 | {D} | | + 5 | {A} | 5 | 8 + 6 | {B} | | + 7 | {C} | | + 8 | {D} | | +(8 rows) + +-- Fixed-length group absorption: (A B{2})+ C +-- B{2} has min == max, equivalent to unrolling to (A B B)+ C +WITH test_absorb_fixedlen AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['B']), + (4, ARRAY['A']), + (5, ARRAY['B']), + (6, ARRAY['B']), + (7, ARRAY['A']), + (8, ARRAY['B']), + (9, ARRAY['B']), + (10, ARRAY['C']), + (11, 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_fixedlen +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B{2})+ 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 | 10 + 2 | {B} | | + 3 | {B} | | + 4 | {A} | | + 5 | {B} | | + 6 | {B} | | + 7 | {A} | | + 8 | {B} | | + 9 | {B} | | + 10 | {C} | | + 11 | {X} | | +(11 rows) + +-- Consecutive vars merged to fixed-length: (A B B)+ -> (A B{2})+ +WITH test_absorb_consecutive AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['B']), + (4, ARRAY['A']), + (5, ARRAY['B']), + (6, ARRAY['B']), + (7, ARRAY['A']), + (8, ARRAY['B']), + (9, ARRAY['B']), + (10, 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_consecutive +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B B)+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 9 + 2 | {B} | | + 3 | {B} | | + 4 | {A} | | + 5 | {B} | | + 6 | {B} | | + 7 | {A} | | + 8 | {B} | | + 9 | {B} | | + 10 | {X} | | +(10 rows) + +-- Nested fixed-length group absorption: (A (B C){2} D)+ E +-- Inner group {2} has min == max; absorbable via recursive check +-- step_size = 1 + (1+1)*2 + 1 = 6 +WITH test_absorb_nested_fixedlen AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['B']), + (5, ARRAY['C']), + (6, ARRAY['D']), + (7, ARRAY['A']), + (8, ARRAY['B']), + (9, ARRAY['C']), + (10, ARRAY['B']), + (11, ARRAY['C']), + (12, ARRAY['D']), + (13, ARRAY['E']), + (14, 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_nested_fixedlen +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A (B C){2} 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 | 13 + 2 | {B} | | + 3 | {C} | | + 4 | {B} | | + 5 | {C} | | + 6 | {D} | | + 7 | {A} | | + 8 | {B} | | + 9 | {C} | | + 10 | {B} | | + 11 | {C} | | + 12 | {D} | | + 13 | {E} | | + 14 | {X} | | +(14 rows) + +-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F +-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; 2 iterations + F = 41 rows +WITH test_absorb_doubly_nested AS ( + SELECT v AS id, ARRAY[ + CASE + WHEN v % 41 IN (1, 21) THEN 'A' + WHEN v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35) THEN 'B' + WHEN v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18, + 23,24,25, 27,28,29, 32,33,34, 36,37,38) THEN 'C' + WHEN v % 41 IN (10, 19, 30, 39) THEN 'D' + WHEN v % 41 IN (20, 40) THEN 'E' + WHEN v % 41 = 0 THEN 'F' + ELSE 'X' + END + ] AS flags + FROM generate_series(1, 82) AS s(v) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_absorb_doubly_nested +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A ((B C C C){2} D){2} 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 | 41 + 2 | {B} | | + 3 | {C} | | + 4 | {C} | | + 5 | {C} | | + 6 | {B} | | + 7 | {C} | | + 8 | {C} | | + 9 | {C} | | + 10 | {D} | | + 11 | {B} | | + 12 | {C} | | + 13 | {C} | | + 14 | {C} | | + 15 | {B} | | + 16 | {C} | | + 17 | {C} | | + 18 | {C} | | + 19 | {D} | | + 20 | {E} | | + 21 | {A} | | + 22 | {B} | | + 23 | {C} | | + 24 | {C} | | + 25 | {C} | | + 26 | {B} | | + 27 | {C} | | + 28 | {C} | | + 29 | {C} | | + 30 | {D} | | + 31 | {B} | | + 32 | {C} | | + 33 | {C} | | + 34 | {C} | | + 35 | {B} | | + 36 | {C} | | + 37 | {C} | | + 38 | {C} | | + 39 | {D} | | + 40 | {E} | | + 41 | {F} | | + 42 | {A} | 42 | 82 + 43 | {B} | | + 44 | {C} | | + 45 | {C} | | + 46 | {C} | | + 47 | {B} | | + 48 | {C} | | + 49 | {C} | | + 50 | {C} | | + 51 | {D} | | + 52 | {B} | | + 53 | {C} | | + 54 | {C} | | + 55 | {C} | | + 56 | {B} | | + 57 | {C} | | + 58 | {C} | | + 59 | {C} | | + 60 | {D} | | + 61 | {E} | | + 62 | {A} | | + 63 | {B} | | + 64 | {C} | | + 65 | {C} | | + 66 | {C} | | + 67 | {B} | | + 68 | {C} | | + 69 | {C} | | + 70 | {C} | | + 71 | {D} | | + 72 | {B} | | + 73 | {C} | | + 74 | {C} | | + 75 | {C} | | + 76 | {B} | | + 77 | {C} | | + 78 | {C} | | + 79 | {C} | | + 80 | {D} | | + 81 | {E} | | + 82 | {F} | | +(82 rows) + +-- 3-level END chain: ((A (B C){2}){2})+ +-- Tests END(BC{2}) -> END(A..{2}) -> END(+) chaining +-- 2 iterations of +, each 10 rows: (A B C B C)(A B C B C) +WITH test_absorb_3level_end AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), -- 1st + iter, 1st {2}, A + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['B']), + (5, ARRAY['C']), -- 1st (BC){2} done + (6, ARRAY['A']), -- 1st + iter, 2nd {2}, A + (7, ARRAY['B']), + (8, ARRAY['C']), + (9, ARRAY['B']), + (10, ARRAY['C']), -- 2nd (BC){2} done, 1st {2} done, 1st + iter done + (11, ARRAY['A']), -- 2nd + iter, 1st {2}, A + (12, ARRAY['B']), + (13, ARRAY['C']), + (14, ARRAY['B']), + (15, ARRAY['C']), + (16, ARRAY['A']), -- 2nd + iter, 2nd {2}, A + (17, ARRAY['B']), + (18, ARRAY['C']), + (19, ARRAY['B']), + (20, ARRAY['C']), -- 2nd + iter done + (21, ARRAY['X']) -- no match, + ends + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_absorb_3level_end +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A (B C){2}){2})+) + 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 | 20 + 2 | {B} | | + 3 | {C} | | + 4 | {B} | | + 5 | {C} | | + 6 | {A} | | + 7 | {B} | | + 8 | {C} | | + 9 | {B} | | + 10 | {C} | | + 11 | {A} | | + 12 | {B} | | + 13 | {C} | | + 14 | {B} | | + 15 | {C} | | + 16 | {A} | | + 17 | {B} | | + 18 | {C} | | + 19 | {B} | | + 20 | {C} | | + 21 | {X} | | +(21 rows) + +-- Multiple unbounded: A+ B+ (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) + +-- ============================================================ +-- Context Lifecycle +-- ============================================================ +-- Multiple overlapping contexts (SKIP TO NEXT ROW) +WITH test_overlapping_contexts AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_overlapping_contexts +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | 2 | 4 + 3 | {A} | 3 | 4 + 4 | {B} | | + 5 | {_} | | +(5 rows) + +-- Failed context cleanup (early failure) +WITH test_context_cleanup AS ( + SELECT * FROM (VALUES + (1, ARRAY['_']), -- Pruned at first row + (2, ARRAY['A']), + (3, ARRAY['_']), -- Mismatched after row 2 + (4, ARRAY['A']), + (5, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_context_cleanup +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {_} | | + 2 | {A} | | + 3 | {_} | | + 4 | {A} | 4 | 5 + 5 | {B} | | +(5 rows) + +-- Partition end (incomplete contexts) +WITH test_partition_end AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']) + -- Pattern requires B, but partition ends + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_partition_end +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | | + 2 | {A} | | + 3 | {A} | | +(3 rows) + +-- Completed context encountered during processing +-- Pattern (A | B C D): Ctx1 takes long B->C->D path, while Ctx2 takes +-- short A path and completes first. Next row sees Ctx2 +-- with states=NULL and skips it. +WITH test_completed_ctx AS ( + SELECT * FROM (VALUES + (1, ARRAY['B', '_']), + (2, ARRAY['C', 'A']), + (3, ARRAY['D', '_']), + (4, ARRAY['_', '_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_completed_ctx +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A | B C D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B,_} | 1 | 3 + 2 | {C,A} | 2 | 2 + 3 | {D,_} | | + 4 | {_,_} | | +(4 rows) + +-- Reluctant context lifecycle (A+? B with SKIP TO NEXT ROW) +-- A+? exits early but if B not available, falls back to loop. +-- Contexts not absorbed (reluctant), so multiple survive. +WITH test_reluctant_context AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (4, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_context +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+? B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {A} | 2 | 3 + 3 | {B} | | + 4 | {_} | | +(4 rows) + +-- ============================================================ +-- Advance Phase (Epsilon Transitions) +-- ============================================================ +-- Nested groups ((A B)+) +WITH test_nested_groups AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nested_groups +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A B)+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 6 + 2 | {B} | | + 3 | {A} | 3 | 6 + 4 | {B} | | + 5 | {A} | 5 | 6 + 6 | {B} | | + 7 | {_} | | +(7 rows) + +-- Multiple alternation branches (A (B|C|D) E) +WITH test_multi_alt AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['E']), + (4, ARRAY['A']), + (5, ARRAY['C']), + (6, ARRAY['E']), + (7, ARRAY['A']), + (8, ARRAY['D']), + (9, ARRAY['E']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_multi_alt +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A (B | C | D) E) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags), + E AS 'E' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {B} | | + 3 | {E} | | + 4 | {A} | 4 | 6 + 5 | {C} | | + 6 | {E} | | + 7 | {A} | 7 | 9 + 8 | {D} | | + 9 | {E} | | +(9 rows) + +-- Optional VAR at start (A? B C) +WITH test_optional_var AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), -- A skipped + (2, ARRAY['C']), + (3, ARRAY['A']), -- A matched + (4, ARRAY['B']), + (5, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_optional_var +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A? B C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | 1 | 2 + 2 | {C} | | + 3 | {A} | 3 | 5 + 4 | {B} | 4 | 5 + 5 | {C} | | +(5 rows) + +-- Nested alternation ((A|B) (C|D)) +WITH test_nested_alt AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['C']), -- A C + (3, ARRAY['A']), + (4, ARRAY['D']), -- A D + (5, ARRAY['B']), + (6, ARRAY['C']), -- B C + (7, ARRAY['B']), + (8, ARRAY['D']) -- B D + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nested_alt +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A | B) (C | D)) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {C} | | + 3 | {A} | 3 | 4 + 4 | {D} | | + 5 | {B} | 5 | 6 + 6 | {C} | | + 7 | {B} | 7 | 8 + 8 | {D} | | +(8 rows) + +-- Mixed greedy/reluctant sequence: A+? B+ (reluctant A, greedy B) +-- A exits as early as possible, B consumes the rest greedily +WITH test_mixed_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A','B']), + (4, ARRAY['B']), + (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_mixed_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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 5 + 2 | {A} | | + 3 | {A,B} | | + 4 | {B} | | + 5 | {B} | | +(5 rows) + +-- Optional reluctant group: (A B)?? C +-- Reluctant group entry tries skip first, but the skip path needs C +-- at row 1 which is A -> skip fails. Enter path succeeds: A(1) B(2) C(3). +WITH test_optional_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_optional_reluctant +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} | | +(3 rows) + +-- Non-leading reluctant optional VAR: (B A?? C) +-- Reluctant A?? should prefer to skip, matching B(1) C(2) with A left +-- unmatched (match_end 2). The leading/group reluctant cases above go through +-- the begin path; this exercises the non-leading skip path, +-- which must honor reluctant ordering too. +WITH test_nonleading_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['A', 'C']), + (3, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nonleading_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (B A?? C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | 1 | 2 + 2 | {A,C} | | + 3 | {C} | | +(3 rows) + +-- Reluctant outer quantifier over a nullable reluctant body: SQL/RPR +-- semantics call for the shortest (empty) match. In the count=2 boundary and single-quantifier controls localize the behaviour: only +-- the all-reluctant case (rr) should differ. +WITH t(id, isa) AS (VALUES (1, true), (2, true), (3, true), (4, false)) +SELECT id, + count(*) OVER gg AS gg, -- (A?)+ greedy / greedy + count(*) OVER gr AS gr, -- (A??)+ greedy / reluctant + count(*) OVER rg AS rg, -- (A?)+? reluctant / greedy + count(*) OVER rr AS rr, -- (A??)+? reluctant / reluctant + count(*) OVER rr2 AS rr2, -- (A??){2,}? reluctant, min>=2 boundary + count(*) OVER ca AS ca, -- A?? single reluctant control + count(*) OVER cs AS cs -- A*? single reluctant control +FROM t +WINDOW gg AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+) DEFINE A AS isa), + gr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+) DEFINE A AS isa), + rg AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+?) DEFINE A AS isa), + rr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+?) DEFINE A AS isa), + rr2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??){2,}?) DEFINE A AS isa), + ca AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??) DEFINE A AS isa), + cs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?) DEFINE A AS isa) +ORDER BY id; + id | gg | gr | rg | rr | rr2 | ca | cs +----+----+----+----+----+-----+----+---- + 1 | 3 | 3 | 1 | 0 | 0 | 0 | 0 + 2 | 0 | 0 | 1 | 0 | 0 | 0 | 0 + 3 | 0 | 0 | 1 | 0 | 0 | 0 | 0 + 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 +(4 rows) + +-- Doubly-nested reluctant nullable group: (((A??){2,}?){2,}?). Reluctant +-- quantifiers disable optimizer flattening, so both levels survive and the +-- inner group's END->next lands on the outer END. This exercises the +-- END->END count increment in the EMPTY_LOOP fast-forward (count < min). +WITH t(id, isa) AS (VALUES (1, true), (2, true), (3, false)) +SELECT id, count(*) OVER w AS c +FROM t +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A??){2,}?){2,}?) + DEFINE A AS isa +) +ORDER BY id; + id | c +----+--- + 1 | 0 + 2 | 0 + 3 | 0 +(3 rows) + +-- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C) +-- Like the VAR case above but a multi-element group; it goes through the +-- begin path, which already honors reluctant ordering. +-- Reluctant (A X)?? should skip, matching B(1) C(2), with the group skipped +-- to the following C (not to FIN). +WITH test_nonleading_reluctant_group AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['A', 'C']), + (3, ARRAY['X']), + (4, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nonleading_reluctant_group +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (B (A X)?? C) + DEFINE + A AS 'A' = ANY(flags), + X AS 'X' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | 1 | 2 + 2 | {A,C} | | + 3 | {X} | | + 4 | {C} | | +(4 rows) + +-- Reluctant nullable group with a required follower: ((A??){2,}? B). +-- min=2 forces the reluctant fast-forward to loop back (it cannot exit to +-- FIN until B matches), exercising the loop-back route_to_elem second call +-- site in nfa_advance_end. B fails at the group's exit row, so only the +-- first match survives. +WITH test_reluctant_nullable_follower AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (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 test_reluctant_nullable_follower +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A??){2,}? B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {A} | | + 3 | {B} | | + 4 | {X} | | +(4 rows) + +-- Greedy/reluctant sequence: A+ B+? (greedy A, reluctant B at end) +-- A consumes greedily, B+? exits to FIN after minimum match +WITH test_greedy_then_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A','B']), + (3, ARRAY['B']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_greedy_then_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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {A,B} | | + 3 | {B} | | + 4 | {B} | | +(4 rows) + +-- Reluctant optional group skip-to-FIN +-- When a reluctant optional group's skip path reaches FIN, the group +-- entry path is abandoned. +-- Pattern: C (A B)?? -- after C matches, the reluctant group (A B)?? +-- prefers to skip. Skip goes to FIN (group is last element), so +-- the match completes with just C. +WITH test_begin_skip_fin AS ( + SELECT * FROM (VALUES + (1, ARRAY['C']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (4, ARRAY['C','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_begin_skip_fin +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (C (A B)??) + DEFINE + C AS 'C' = ANY(flags), + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {C} | 1 | 1 + 2 | {A} | | + 3 | {B} | | + 4 | {C,A} | 4 | 4 + 5 | {B} | | +(5 rows) + +-- ============================================================ +-- Match Phase +-- ============================================================ +-- Simple VAR with END next (A B C all min=max=1) +WITH test_simple_var AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_simple_var +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {B} | | + 3 | {C} | | + 4 | {_} | | +(4 rows) + +-- VAR max exceeded (A{2,3}) +WITH test_max_exceeded AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), -- Max = 3 + (4, ARRAY['A']), -- Exceeds max, state removed + (5, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_max_exceeded +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A{2,3} B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | | + 2 | {A} | 2 | 5 + 3 | {A} | 3 | 5 + 4 | {A} | | + 5 | {B} | | +(5 rows) + +-- Non-matching VAR (DEFINE false) +WITH test_non_matching AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['_']), -- B not matched (DEFINE false) + (3, ARRAY['A']), + (4, ARRAY['B']), -- B matched + (5, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_non_matching +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | | + 2 | {_} | | + 3 | {A} | 3 | 5 + 4 | {B} | | + 5 | {C} | | +(5 rows) + +-- ============================================================ +-- Frame Boundary Handling +-- ============================================================ +-- Limited frame (ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING) +WITH test_limited_frame AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), -- Within 3 FOLLOWING + (5, ARRAY['B']), -- Beyond 3 FOLLOWING from row 1 + (6, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_limited_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | 2 | 4 + 3 | {A} | 3 | 4 + 4 | {B} | | + 5 | {B} | | + 6 | {_} | | +(6 rows) + +-- Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) +WITH test_unbounded_frame AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['B']) -- Far from start, but unbounded + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_unbounded_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 6 + 2 | {A} | 2 | 6 + 3 | {A} | 3 | 6 + 4 | {A} | 4 | 6 + 5 | {A} | 5 | 6 + 6 | {B} | | +(6 rows) + +-- Match exceeds frame boundary +WITH test_frame_exceeded AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']) + -- Frame ends at row 3 (2 FOLLOWING), B never appears + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_frame_exceeded +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | | + 2 | {A} | | + 3 | {A} | | +(3 rows) + +-- Frame boundary forced mismatch +-- Limited frame with enough rows so that a context's frame boundary +-- is exceeded while still processing. +WITH test_frame_boundary AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_frame_boundary +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | | + 2 | {A} | | + 3 | {A} | | + 4 | {A} | 4 | 6 + 5 | {A} | 5 | 6 + 6 | {B} | | +(6 rows) + +-- Reluctant with limited frame (A+? B with 2 FOLLOWING) +-- Reluctant exits early, B must be within frame boundary +WITH test_reluctant_frame AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (4, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+? B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {A} | 2 | 3 + 3 | {B} | | + 4 | {_} | | +(4 rows) + +-- ============================================================ +-- State Management +-- ============================================================ +-- Duplicate state creation +WITH test_duplicate_states AS ( + SELECT * FROM (VALUES + (1, ARRAY['A', 'B']), -- Both A and B match (creates duplicate states via different paths) + (2, ARRAY['C', '_']), + (3, ARRAY['D', '_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_duplicate_states +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A | B) C D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 3 + 2 | {C,_} | | + 3 | {D,_} | | +(3 rows) + +-- Reluctant duplicate state handling +-- (A+? | B+?) creates exit and loop states; exit paths may converge +WITH test_reluctant_dedup AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['A','B']), + (3, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_dedup +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A+? | B+?)) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 1 + 2 | {A,B} | 2 | 2 + 3 | {_} | | +(3 rows) + +-- Large pattern (stress free list) +WITH test_large_pattern AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (5, ARRAY['E']), + (6, ARRAY['F']), + (7, ARRAY['G']), + (8, ARRAY['H']), + (9, ARRAY['I']), + (10, ARRAY['J']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_large_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C D E F G H I J) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags), + E AS 'E' = ANY(flags), + F AS 'F' = ANY(flags), + G AS 'G' = ANY(flags), + H AS 'H' = ANY(flags), + I AS 'I' = ANY(flags), + J AS 'J' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 10 + 2 | {B} | | + 3 | {C} | | + 4 | {D} | | + 5 | {E} | | + 6 | {F} | | + 7 | {G} | | + 8 | {H} | | + 9 | {I} | | + 10 | {J} | | +(10 rows) + +-- Reduced frame map reallocation (> 1024 rows) +WITH test_map_realloc AS ( + SELECT id, CASE WHEN id % 2 = 1 THEN ARRAY['A'] ELSE ARRAY['B'] END AS flags + FROM generate_series(1, 1100) AS id +) +SELECT count(*), min(match_start), max(match_end) +FROM ( + SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end + FROM test_map_realloc + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) + ) +) sub; + count | min | max +-------+-----+------ + 1100 | 1 | 1100 +(1 row) + +-- ============================================================ +-- Statistics and Diagnostics +-- ============================================================ +-- Matched contexts +WITH test_matched AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_matched +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {B} | | + 3 | {A} | 3 | 4 + 4 | {B} | | +(4 rows) + +-- Pruned contexts (failed at first row) +WITH test_pruned AS ( + SELECT * FROM (VALUES + (1, ARRAY['_']), -- Pruned + (2, ARRAY['_']), -- Pruned + (3, ARRAY['A']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_pruned +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {_} | | + 2 | {_} | | + 3 | {A} | 3 | 4 + 4 | {B} | | +(4 rows) + +-- Mismatched contexts (failed after multiple rows) +WITH test_mismatched AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['_']), -- Mismatched after 2 rows + (4, ARRAY['A']), + (5, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_mismatched +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | | + 2 | {A} | | + 3 | {_} | | + 4 | {A} | 4 | 5 + 5 | {B} | | +(5 rows) + +-- Reluctant not absorbed (A+? with SKIP TO NEXT ROW) +-- Compare with greedy A+ below: reluctant is not absorbable, +-- so all contexts survive independently. +WITH test_reluctant_stats AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_stats +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+?) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 1 + 2 | {A} | 2 | 2 + 3 | {A} | 3 | 3 + 4 | {A} | 4 | 4 + 5 | {_} | | +(5 rows) + +-- Absorbed contexts +WITH test_absorbed AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_absorbed +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | 2 | 4 + 3 | {A} | 3 | 4 + 4 | {A} | 4 | 4 + 5 | {_} | | +(5 rows) + +-- Skipped contexts (SKIP TO NEXT ROW) +WITH test_skipped AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']) -- Completes match starting at row 1 + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_skipped +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | 2 | 4 + 3 | {A} | 3 | 4 + 4 | {B} | | +(4 rows) + +-- ============================================================ +-- Quantifier Runtime Behavior +-- ============================================================ +-- Large count handling (A{100}) +WITH test_large_count AS ( + SELECT i AS id, ARRAY['A'] AS flags + FROM generate_series(1, 105) i +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_large_count +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A{100}) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +-----+-------+-------------+----------- + 1 | {A} | 1 | 100 + 2 | {A} | 2 | 101 + 3 | {A} | 3 | 102 + 4 | {A} | 4 | 103 + 5 | {A} | 5 | 104 + 6 | {A} | 6 | 105 + 7 | {A} | | + 8 | {A} | | + 9 | {A} | | + 10 | {A} | | + 11 | {A} | | + 12 | {A} | | + 13 | {A} | | + 14 | {A} | | + 15 | {A} | | + 16 | {A} | | + 17 | {A} | | + 18 | {A} | | + 19 | {A} | | + 20 | {A} | | + 21 | {A} | | + 22 | {A} | | + 23 | {A} | | + 24 | {A} | | + 25 | {A} | | + 26 | {A} | | + 27 | {A} | | + 28 | {A} | | + 29 | {A} | | + 30 | {A} | | + 31 | {A} | | + 32 | {A} | | + 33 | {A} | | + 34 | {A} | | + 35 | {A} | | + 36 | {A} | | + 37 | {A} | | + 38 | {A} | | + 39 | {A} | | + 40 | {A} | | + 41 | {A} | | + 42 | {A} | | + 43 | {A} | | + 44 | {A} | | + 45 | {A} | | + 46 | {A} | | + 47 | {A} | | + 48 | {A} | | + 49 | {A} | | + 50 | {A} | | + 51 | {A} | | + 52 | {A} | | + 53 | {A} | | + 54 | {A} | | + 55 | {A} | | + 56 | {A} | | + 57 | {A} | | + 58 | {A} | | + 59 | {A} | | + 60 | {A} | | + 61 | {A} | | + 62 | {A} | | + 63 | {A} | | + 64 | {A} | | + 65 | {A} | | + 66 | {A} | | + 67 | {A} | | + 68 | {A} | | + 69 | {A} | | + 70 | {A} | | + 71 | {A} | | + 72 | {A} | | + 73 | {A} | | + 74 | {A} | | + 75 | {A} | | + 76 | {A} | | + 77 | {A} | | + 78 | {A} | | + 79 | {A} | | + 80 | {A} | | + 81 | {A} | | + 82 | {A} | | + 83 | {A} | | + 84 | {A} | | + 85 | {A} | | + 86 | {A} | | + 87 | {A} | | + 88 | {A} | | + 89 | {A} | | + 90 | {A} | | + 91 | {A} | | + 92 | {A} | | + 93 | {A} | | + 94 | {A} | | + 95 | {A} | | + 96 | {A} | | + 97 | {A} | | + 98 | {A} | | + 99 | {A} | | + 100 | {A} | | + 101 | {A} | | + 102 | {A} | | + 103 | {A} | | + 104 | {A} | | + 105 | {A} | | +(105 rows) + +-- Unlimited quantifier (A{10,}) +WITH test_unlimited AS ( + SELECT i AS id, ARRAY['A'] AS flags + FROM generate_series(1, 15) i + UNION ALL + SELECT 16, ARRAY['B'] +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_unlimited +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A{10,} B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 16 + 2 | {A} | 2 | 16 + 3 | {A} | 3 | 16 + 4 | {A} | 4 | 16 + 5 | {A} | 5 | 16 + 6 | {A} | 6 | 16 + 7 | {A} | | + 8 | {A} | | + 9 | {A} | | + 10 | {A} | | + 11 | {A} | | + 12 | {A} | | + 13 | {A} | | + 14 | {A} | | + 15 | {A} | | + 16 | {B} | | +(16 rows) + +-- Min boundary (A{3,5}) +WITH test_min_boundary AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), -- Min=3 reached, exit path available + (4, ARRAY['B']), -- Match ends at min + (5, ARRAY['A']), + (6, ARRAY['A']), + (7, ARRAY['A']), + (8, ARRAY['A']), + (9, ARRAY['A']), -- Count=5, max reached + (10, ARRAY['B']) -- Match ends at max + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_min_boundary +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A{3,5} B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | | + 3 | {A} | | + 4 | {B} | | + 5 | {A} | 5 | 10 + 6 | {A} | 6 | 10 + 7 | {A} | 7 | 10 + 8 | {A} | | + 9 | {A} | | + 10 | {B} | | +(10 rows) + +-- Max boundary exceeded (A{3,5}) +WITH test_max_boundary AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['A']), -- Count=6 > max=5, row 1 context removed + (7, ARRAY['B']) -- Row 1 context: no match (exceeded max) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_max_boundary +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A{3,5} B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | | + 2 | {A} | 2 | 7 + 3 | {A} | 3 | 7 + 4 | {A} | 4 | 7 + 5 | {A} | | + 6 | {A} | | + 7 | {B} | | +(7 rows) + +-- Greedy vs reluctant: A+ matches all rows, A+? matches minimum +WITH test_greedy_vs_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','_']), + (2, ARRAY['A','_']), + (3, ARRAY['A','B']), + (4, ARRAY['B','_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_greedy_vs_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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,_} | 1 | 4 + 2 | {A,_} | | + 3 | {A,B} | | + 4 | {B,_} | | +(4 rows) + +-- Same data, reluctant A+? exits at row 3 where B is first available +WITH test_greedy_vs_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','_']), + (2, ARRAY['A','_']), + (3, ARRAY['A','B']), + (4, ARRAY['B','_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_greedy_vs_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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,_} | 1 | 3 + 2 | {A,_} | | + 3 | {A,B} | | + 4 | {B,_} | | +(4 rows) + +-- Reluctant group: (A B)+? matches minimum 1 iteration +WITH test_reluctant_group AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_group +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 | 2 + 2 | {B} | | + 3 | {A} | 3 | 4 + 4 | {B} | | +(4 rows) + +-- A+? B (reluctant plus): exits A at first B availability +-- (Same scenario as greedy-vs-reluctant comparison above; retained for +-- standalone quantifier coverage alongside A{1,3}? and A{2,3}? below) +WITH test_reluctant_plus AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','_']), + (2, ARRAY['A','_']), + (3, ARRAY['A','B']), + (4, ARRAY['B','_']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_reluctant_plus +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 | 3 + 2 | {A,_} | | + 3 | {A,B} | | + 4 | {B,_} | | +(4 rows) + +-- A{1,3}? B (reluctant bounded): same data, bounded quantifier +WITH test_reluctant_bounded AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','_']), + (2, ARRAY['A','_']), + (3, ARRAY['A','B']), + (4, ARRAY['B','_']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,_} | 1 | 3 + 2 | {A,_} | | + 3 | {A,B} | | + 4 | {B,_} | | +(4 rows) + +-- A{3,5}? B (reluctant bounded mid-band): the VAR-level count +-- cycles through 3, 4, 5 within a single match attempt. Exercises +-- a reluctant bounded quantifier that absorbability analysis excludes +-- (reluctant quantifiers are never absorbable, so A stays +-- non-absorbable). +WITH test_reluctant_mid_band AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_mid_band +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3,5}? B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 6 + 2 | {A} | | + 3 | {A} | | + 4 | {A} | | + 5 | {A} | | + 6 | {B} | | +(6 rows) + +-- Nested quantifier flattening must not widen the matching language (H-1). +-- (A{k,})* with k >= 2 reaches repetition counts {0} UNION [k, INF); the gap +-- 1..k-1 is unreachable, so it must NOT collapse to A*. An isolated single A +-- must yield an EMPTY match (count 0), not a length-1 match. +WITH test_nested_quant_var AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), -- isolated A: (A{2,})* matches empty here, not 1 + (2, ARRAY['_']), + (3, ARRAY['A']), + (4, ARRAY['A']), -- run of 2: matched + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end, + count(*) OVER w AS match_count +FROM test_nested_quant_var +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 | match_count +----+-------+-------------+-----------+------------- + 1 | {A} | | | 0 + 2 | {_} | | | 0 + 3 | {A} | 3 | 4 | 2 + 4 | {A} | | | 0 + 5 | {_} | | | 0 +(5 rows) + +-- Same for a GROUP child: ((A B){2,})* must not collapse to (A B)*. +-- An isolated single (A B) pair must yield an EMPTY match (count 0). +WITH test_nested_quant_group AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), -- isolated (A B) pair: matches empty here + (2, ARRAY['B']), + (3, ARRAY['_']), + (4, ARRAY['A']), + (5, ARRAY['B']), + (6, ARRAY['A']), + (7, ARRAY['B']), -- run of 2 pairs: matched + (8, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end, + count(*) OVER w AS match_count +FROM test_nested_quant_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 | match_count +----+-------+-------------+-----------+------------- + 1 | {A} | | | 0 + 2 | {B} | | | 0 + 3 | {_} | | | 0 + 4 | {A} | 4 | 7 | 4 + 5 | {B} | | | 0 + 6 | {A} | | | 0 + 7 | {B} | | | 0 + 8 | {_} | | | 0 +(8 rows) + +-- ============================================================ +-- Pathological Pattern Runtime Protection +-- ============================================================ +-- Complex nested nullable ((A* B*)*) - Runtime protection +WITH test_complex_nested AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (4, ARRAY['B']), + (5, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_complex_nested +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A* B*)*) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | 2 | 4 + 3 | {B} | 3 | 4 + 4 | {B} | 4 | 4 + 5 | {C} | | +(5 rows) + +-- Nested nullable with quantifier ((A{0,3})*) +WITH test_nested_quantifier AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nested_quantifier +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A{0,3})*) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {A} | 2 | 3 + 3 | {A} | 3 | 3 + 4 | {B} | | +(4 rows) + +-- Reluctant nullable: A*? (prefers 0 matches) +-- A*? always takes skip path (0 iterations preferred) +WITH test_reluctant_nullable AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_nullable +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A*? B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | | + 2 | {A} | | + 3 | {A} | | + 4 | {_} | | +(4 rows) + +-- ============================================================ +-- Alternation Runtime Behavior +-- ============================================================ +-- Multi-branch alternation (A (B|C|D|E) F) +WITH test_multi_branch AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['F']), + (4, ARRAY['A']), + (5, ARRAY['C']), + (6, ARRAY['F']), + (7, ARRAY['A']), + (8, ARRAY['D']), + (9, ARRAY['F']), + (10, ARRAY['A']), + (11, ARRAY['E']), + (12, ARRAY['F']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_multi_branch +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A (B | C | D | E) F) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags), + E AS 'E' = ANY(flags), + F AS 'F' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {B} | | + 3 | {F} | | + 4 | {A} | 4 | 6 + 5 | {C} | | + 6 | {F} | | + 7 | {A} | 7 | 9 + 8 | {D} | | + 9 | {F} | | + 10 | {A} | 10 | 12 + 11 | {E} | | + 12 | {F} | | +(12 rows) + +-- Alternation with quantifiers (A+ | B+ | C+) +WITH test_alt_quantifiers AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['B']), + (6, ARRAY['C']), + (7, ARRAY['C']), + (8, ARRAY['C']), + (9, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_quantifiers +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ | B+ | C+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {A} | 2 | 3 + 3 | {A} | 3 | 3 + 4 | {B} | 4 | 5 + 5 | {B} | 5 | 5 + 6 | {C} | 6 | 9 + 7 | {C} | 7 | 9 + 8 | {C} | 8 | 9 + 9 | {C} | 9 | 9 +(9 rows) + +-- altPriority replacement (A B C | D) +-- D branch (higher altPriority) matches first at row 1, +-- then A B C branch (lower altPriority) replaces it at row 3. +WITH test_alt_replace AS ( + SELECT * FROM (VALUES + (1, ARRAY['A', 'D']), + (2, ARRAY['B', '_']), + (3, ARRAY['C', '_']), + (4, ARRAY['_', '_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_replace +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C | D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,D} | 1 | 3 + 2 | {B,_} | | + 3 | {C,_} | | + 4 | {_,_} | | +(4 rows) + +-- ALT lexical order takes priority over greedy (longer match). +-- Row 1 matches both A and B; A wins by lexical order (match 1-1). +WITH test_alt_lexical_order AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), -- A and B both match + (2, ARRAY['_','C']) -- only C matches (would continue B C) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_lexical_order +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,B} | 1 | 1 + 2 | {_,C} | | +(2 rows) + +-- ALT with reluctant: (A+? | B+) - A branch is reluctant, B is greedy. +-- Row 1 matches both A and B. A+? exits immediately (match 1-1). +WITH test_alt_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['B','_']), + (3, 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_alt_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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 1 + 2 | {B,_} | 2 | 3 + 3 | {B,_} | | +(3 rows) + +-- Optional first branch in ALT with quantifier: (A? | B){1,2} +-- First branch A? exit path may loop back to ALT and trigger cycle +-- detection during DFS. All branches must receive correct counts. +WITH test_alt_opt_first AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, 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_alt_opt_first +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A? | B){1,2})) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | 1 | 2 + 2 | {B} | 2 | 3 + 3 | {B} | 3 | 3 +(3 rows) + +-- Mixed A/B rows across iterations of (A? | B){1,2} +WITH test_alt_opt_mixed AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A','B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_opt_mixed +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A? | B){1,2})) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {B} | 2 | 3 + 3 | {A,B} | 3 | 3 +(3 rows) + +-- Reluctant variant: (A?? | B){1,2} +WITH test_alt_opt_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, 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_alt_opt_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A?? | B){1,2})) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | 1 | 2 + 2 | {B} | 2 | 3 + 3 | {B} | 3 | 3 +(3 rows) + +-- Overlapping match: A B C D E | B C D | C D E F (SKIP PAST LAST ROW) +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) + +-- Same with SKIP TO NEXT ROW: three overlapping matches +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) + +-- Longer pattern fails, shorter survives: A+ B C D E | B+ C +WITH test_overlap1b AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (5, ARRAY['X']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_overlap1b +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B C D E | B+ C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags), + E AS 'E' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | | + 2 | {B} | 2 | 3 + 3 | {C} | | + 4 | {D} | | + 5 | {X} | | +(5 rows) + +-- Long B sequence with different endings: A B+ C | B+ D +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) + +-- Greedy with late failure ("betrayal"): A B C+ D | A B +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) + +-- Multiple TRUE per row: overlapping pattern variables +WITH test_multi_true AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['B','C']), + (3, ARRAY['C','D']), + (4, ARRAY['D','E']), + (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_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) + +-- Diagonal pattern with shifted multi-TRUE 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) + +-- ((A | B) C)+ - alternation inside group with outer quantifier +WITH test_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 test_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) + +-- ============================================================ +-- Deep Nested Groups +-- ============================================================ +-- Three-level nesting ((((A B)+)+)+) +WITH test_deep_nesting AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_deep_nesting +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((((A B)+)+)+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 6 + 2 | {B} | | + 3 | {A} | 3 | 6 + 4 | {B} | | + 5 | {A} | 5 | 6 + 6 | {B} | | + 7 | {_} | | +(7 rows) + +-- Multiple groups in nesting (((A B) (C D))+) +WITH test_nested_sequential AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, ARRAY['C']), + (8, ARRAY['D']), + (9, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nested_sequential +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A B) (C D))+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 8 + 2 | {B} | | + 3 | {C} | | + 4 | {D} | | + 5 | {A} | 5 | 8 + 6 | {B} | | + 7 | {C} | | + 8 | {D} | | + 9 | {_} | | +(9 rows) + +-- Nested END->END max reached +-- Inner group (A B){2} reaches max=2 -> exits to outer END +WITH test_end_nested_max AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, ARRAY['A']), + (8, ARRAY['B']), + (9, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_end_nested_max +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A B){2})+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 8 + 2 | {B} | | + 3 | {A} | 3 | 6 + 4 | {B} | | + 5 | {A} | 5 | 8 + 6 | {B} | | + 7 | {A} | | + 8 | {B} | | + 9 | {_} | | +(9 rows) + +-- Nested END->END between min/max +-- Inner group (A B){1,3} exits between min/max -> outer END count++ +WITH test_end_nested_mid AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, ARRAY['A']), + (8, ARRAY['B']), + (9, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_end_nested_mid +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A B){1,3})+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 8 + 2 | {B} | | + 3 | {A} | 3 | 8 + 4 | {B} | | + 5 | {A} | 5 | 8 + 6 | {B} | | + 7 | {A} | 7 | 8 + 8 | {B} | | + 9 | {_} | | +(9 rows) + +-- Nested reluctant group ((A B)+?) with following element C +-- Inner group exits after minimum 1 iteration +WITH test_nested_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nested_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A B)+? C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 5 + 2 | {B} | | + 3 | {A} | 3 | 5 + 4 | {B} | | + 5 | {C} | | +(5 rows) + +-- (A B){2} - group with exact quantifier +WITH test_group_exact 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 test_group_exact +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) + +-- Nested END->END fast-forward +-- When an inner group has a nullable body and count < min, the +-- fast-forward path exits through the outer END, incrementing +-- the outer group's count. +-- Pattern: ((A?){2,3}){2,3} -- nested groups, neither collapses +-- because the optimizer cannot safely multiply non-exact quantifiers. +-- Data has no A rows, forcing all-empty iterations via fast-forward. +WITH test_nested_ff AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nested_ff +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A?){2,3}){2,3}) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | | + 2 | {B} | | + 3 | {B} | | +(3 rows) + +-- ============================================================ +-- SKIP Options (Runtime) +-- ============================================================ +-- SKIP PAST LAST ROW (non-overlapping matches) +WITH test_skip_past AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_skip_past +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | | + 3 | {A} | | + 4 | {A} | | + 5 | {_} | | +(5 rows) + +-- SKIP TO NEXT ROW (overlapping matches) +WITH test_skip_next AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_skip_next +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | 2 | 4 + 3 | {A} | 3 | 4 + 4 | {A} | 4 | 4 + 5 | {_} | | +(5 rows) + +-- SKIP difference verification +WITH test_skip_diff AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT 'SKIP PAST' AS mode, id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_skip_diff +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +) +UNION ALL +SELECT 'SKIP NEXT' AS mode, id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_skip_diff +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +) +ORDER BY mode, id; + mode | id | flags | match_start | match_end +-----------+----+-------+-------------+----------- + SKIP NEXT | 1 | {A} | 1 | 2 + SKIP NEXT | 2 | {B} | | + SKIP NEXT | 3 | {A} | 3 | 4 + SKIP NEXT | 4 | {B} | | + SKIP PAST | 1 | {A} | 1 | 2 + SKIP PAST | 2 | {B} | | + SKIP PAST | 3 | {A} | 3 | 4 + SKIP PAST | 4 | {B} | | +(8 rows) + +-- Reluctant SKIP comparison: A+? with SKIP PAST vs SKIP NEXT +WITH test_reluctant_skip AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['_']) + ) AS t(id, flags) +) +SELECT 'SKIP PAST' AS mode, id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_skip +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) +) +UNION ALL +SELECT 'SKIP NEXT' AS mode, id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_skip +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) +) +ORDER BY mode, id; + mode | id | flags | match_start | match_end +-----------+----+-------+-------------+----------- + SKIP NEXT | 1 | {A} | 1 | 1 + SKIP NEXT | 2 | {A} | 2 | 2 + SKIP NEXT | 3 | {A} | 3 | 3 + SKIP NEXT | 4 | {_} | | + SKIP PAST | 1 | {A} | 1 | 1 + SKIP PAST | 2 | {A} | 2 | 2 + SKIP PAST | 3 | {A} | 3 | 3 + SKIP PAST | 4 | {_} | | +(8 rows) + +-- ============================================================ +-- INITIAL Mode (Runtime) +-- ============================================================ +-- Explicit INITIAL (after AFTER MATCH SKIP, per the grammar); same as the default +WITH test_initial_mode AS ( + SELECT * FROM (VALUES + (1, ARRAY['_']), -- Unmatched + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['_']), -- Unmatched + (5, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_initial_mode +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + INITIAL + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {_} | | + 2 | {A} | 2 | 3 + 3 | {A} | 3 | 3 + 4 | {_} | | + 5 | {A} | 5 | 5 +(5 rows) + +-- Default mode (include all rows) +WITH test_default_mode AS ( + SELECT * FROM (VALUES + (1, ARRAY['_']), -- Unmatched, but included + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['_']), -- Unmatched, but included + (5, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_default_mode +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {_} | | + 2 | {A} | 2 | 3 + 3 | {A} | 3 | 3 + 4 | {_} | | + 5 | {A} | 5 | 5 +(5 rows) + +-- Mode equivalence verification: explicit INITIAL equals the default mode +WITH test_mode_diff AS ( + SELECT * FROM (VALUES + (1, ARRAY['_']), + (2, ARRAY['A']), + (3, ARRAY['_']) + ) AS t(id, flags) +) +SELECT 'INITIAL' AS mode, COUNT(*) AS row_count +FROM ( + SELECT id FROM test_mode_diff + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + INITIAL + PATTERN (A) + DEFINE A AS 'A' = ANY(flags) + ) +) sub +UNION ALL +SELECT 'DEFAULT' AS mode, COUNT(*) AS row_count +FROM ( + SELECT id FROM test_mode_diff + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A) + DEFINE A AS 'A' = ANY(flags) + ) +) sub +ORDER BY mode; + mode | row_count +---------+----------- + DEFAULT | 3 + INITIAL | 3 +(2 rows) + +-- ============================================================ +-- Frame Boundary Variations +-- ============================================================ +-- Very limited frame (1 FOLLOWING) +WITH test_one_following AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), -- Within 1 FOLLOWING + (3, ARRAY['A']), -- Beyond 1 FOLLOWING from row 1 + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_one_following +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {B} | | + 3 | {A} | 3 | 4 + 4 | {B} | | +(4 rows) + +-- Medium frame (10 FOLLOWING) +WITH test_ten_following AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['A']), + (7, ARRAY['A']), + (8, ARRAY['A']), + (9, ARRAY['A']), + (10, ARRAY['A']), + (11, ARRAY['B']), -- Within 10 FOLLOWING from row 1 + (12, ARRAY['A']), + (13, ARRAY['B']) -- Beyond 10 FOLLOWING from row 1 + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_ten_following +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 11 + 2 | {A} | 2 | 11 + 3 | {A} | 3 | 11 + 4 | {A} | 4 | 11 + 5 | {A} | 5 | 11 + 6 | {A} | 6 | 11 + 7 | {A} | 7 | 11 + 8 | {A} | 8 | 11 + 9 | {A} | 9 | 11 + 10 | {A} | 10 | 11 + 11 | {B} | | + 12 | {A} | 12 | 13 + 13 | {B} | | +(13 rows) + +-- Exact boundary match +WITH test_exact_boundary AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['B']) -- Exactly at 4 FOLLOWING (frame end) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_exact_boundary +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 4 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 5 + 2 | {A} | 2 | 5 + 3 | {A} | 3 | 5 + 4 | {A} | 4 | 5 + 5 | {B} | | +(5 rows) + +-- N FOLLOWING + SKIP TO NEXT ROW: overlapping matches bounded by frame +-- Row 1: frame [1,4], A(1-3) B(4) -> match +-- Row 2: frame [2,5], A(2-3) B(4) -> match +-- Row 3: frame [3,6], A(3) B(4) -> match +-- Row 5: frame [5,6], A(5) B(6) -> match +WITH test_n_skip_next AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_n_skip_next +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | 2 | 4 + 3 | {A} | 3 | 4 + 4 | {B} | | + 5 | {A} | 5 | 6 + 6 | {B} | | +(6 rows) + +-- Frame exactly 1 row short of potential match +-- From row 1: A A A B needs 4 rows but frame holds 3 -> no match +-- From row 2: A A B fits in 3-row frame -> match +WITH test_frame_one_short AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_frame_one_short +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | | + 2 | {A} | 2 | 4 + 3 | {A} | 3 | 4 + 4 | {B} | | + 5 | {A} | 5 | 6 + 6 | {B} | | +(6 rows) + +-- ============================================================ +-- Special Partition Cases +-- ============================================================ +-- Empty partition (0 rows) +WITH test_empty_partition AS ( + SELECT * FROM (VALUES + (1, 1, ARRAY['A']), + (2, 2, ARRAY['_']) -- Different partition + ) AS t(id, part, flags) +) +SELECT id, part, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_empty_partition +WHERE part = 99 -- No rows match +WINDOW w AS ( + PARTITION BY part + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A) + DEFINE + A AS 'A' = ANY(flags) +); + id | part | flags | match_start | match_end +----+------+-------+-------------+----------- +(0 rows) + +-- Single row partition +WITH test_single_row AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_single_row +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 1 +(1 row) + +-- All rows fail matching (all DEFINE false) +WITH test_all_fail AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_all_fail +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE + A AS false -- All rows fail +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | | + 2 | {A} | | + 3 | {A} | | +(3 rows) + +-- Partition end with absorbable pattern +-- SKIP PAST LAST ROW + unbounded frame + all rows match A +-- Triggers absorb in !rowExists path at partition boundary. +WITH test_absorb_partition_end AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_absorb_partition_end +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 5 + 2 | {A} | | + 3 | {A} | | + 4 | {A} | | + 5 | {A} | | +(5 rows) + +-- ============================================================ +-- DEFINE Special Cases +-- ============================================================ +-- Undefined variable in DEFINE +WITH test_undefined_var AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['X']), -- B not defined, defaults to TRUE + (3, ARRAY['C']), + (4, ARRAY['A']), + (5, ARRAY['_']), -- B defaults to TRUE, but no flags + (6, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_undefined_var +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C) + DEFINE + A AS 'A' = ANY(flags), + -- B is undefined, defaults to TRUE + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {X} | | + 3 | {C} | | + 4 | {A} | 4 | 6 + 5 | {_} | | + 6 | {C} | | +(6 rows) + +-- ============================================================ +-- Absorption Dynamic Flags +-- ============================================================ +-- Partial absorbable pattern ((A+) B) +WITH test_partial_absorbable AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_partial_absorbable +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A+) B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | 2 | 4 + 3 | {A} | 3 | 4 + 4 | {B} | | + 5 | {_} | | +(5 rows) + +-- Dynamic flag update ((A+) | B) +WITH test_dynamic_flags AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_dynamic_flags +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A+) | B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {A} | 2 | 3 + 3 | {A} | 3 | 3 + 4 | {B} | 4 | 4 + 5 | {A} | 5 | 5 + 6 | {B} | 6 | 6 +(6 rows) + +-- Non-absorbable context during absorption +-- Pattern (A B)+ C: A,B in absorbable group, C is not. +-- When END exits to C, the cloned context becomes non-absorbable. +WITH test_non_absorbable AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['C']), + (6, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_non_absorbable +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+ C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 5 + 2 | {B} | | + 3 | {A} | | + 4 | {B} | | + 5 | {C} | | + 6 | {_} | | +(6 rows) + +-- Absorption skipped when no absorbable state remains +-- Pattern (A B)+ C D with SKIP PAST LAST ROW +-- After reaching C (non-absorbable), no absorbable state remains. +-- On next row (D), the early return fires. +WITH test_absorption_early_return AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['C']), + (6, ARRAY['D']), + (7, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_absorption_early_return +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+ C D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 6 + 2 | {B} | | + 3 | {A} | | + 4 | {B} | | + 5 | {C} | | + 6 | {D} | | + 7 | {_} | | +(7 rows) + +-- Coverage failure: older can't cover newer's states +-- Pattern A+ | B+ with SKIP PAST LAST ROW. +-- Row 1: only A -> Ctx1 takes A branch only (B fails). +-- Row 2: A and B -> Ctx2 takes both branches. +-- Absorption: Ctx1 has A but no B -> can't cover Ctx2's B state -> fails. +WITH test_coverage_fail AS ( + SELECT * FROM (VALUES + (1, ARRAY['A', '_']), + (2, ARRAY['A', 'B']), + (3, ARRAY['A', '_']), + (4, ARRAY['A', '_']), + (5, ARRAY['_', '_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_coverage_fail +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ | B+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,_} | 1 | 4 + 2 | {A,B} | | + 3 | {A,_} | | + 4 | {A,_} | | + 5 | {_,_} | | +(5 rows) + +-- Absorb skips completed context (older->states==NULL) +-- Pattern A+ | B+ with SKIP PAST LAST ROW. +-- Row 1: A only -> Ctx1 takes A branch. Row 2: B only -> Ctx1 A fails (completed). +-- Ctx2 takes B branch. Absorption: Ctx1 states==NULL -> skip. +WITH test_older_completed AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['B']), + (4, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_older_completed +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ | B+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 1 + 2 | {B} | 2 | 3 + 3 | {B} | | + 4 | {_} | | +(4 rows) + +-- Absorb skips a context with no absorbable state +-- Pattern A+ | B C with SKIP PAST LAST ROW (only A+ branch absorbable). +-- Row 1: B only -> Ctx1 takes B branch (non-absorbable), advances to C. +-- Row 2: C,A -> Ctx1 C matches (no absorbable state). Ctx2 takes A (absorbable). +-- Absorption: Ctx1 has no absorbable state -> skip. +WITH test_older_non_absorbable AS ( + SELECT * FROM (VALUES + (1, ARRAY['B', '_']), + (2, ARRAY['C', 'A']), + (3, ARRAY['_', 'A']), + (4, ARRAY['_', '_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_older_non_absorbable +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ | B C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B,_} | 1 | 2 + 2 | {C,A} | | + 3 | {_,A} | 3 | 3 + 4 | {_,_} | | +(4 rows) + +-- Reluctant branch in ALT not absorbable: (A+?) | B +-- A+? is reluctant so not absorbable. Compare with greedy (A+) | B above. +WITH test_reluctant_alt_absorption AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_alt_absorption +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A+?) | B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 1 + 2 | {A} | 2 | 2 + 3 | {A} | 3 | 3 + 4 | {B} | 4 | 4 + 5 | {_} | | +(5 rows) + +-- ============================================================ +-- Zero-Consumption Cycle Detection +-- ============================================================ +-- Cycle prevention at count > 0: (A*)* inner skip cycles at count=3 +WITH test_cycle_nonzero AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']) -- Inner A* matches 0, cycles at count=3 + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_cycle_nonzero +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A*)*) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {A} | 2 | 3 + 3 | {A} | 3 | 3 + 4 | {B} | | +(4 rows) + +-- Cycle with mixed nullables: (A* B*)* multiple nullable paths +WITH test_cycle_mixed AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_cycle_mixed +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A* B*)*) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {B} | 2 | 3 + 3 | {A} | 3 | 3 + 4 | {C} | | +(4 rows) + +-- ============================================================ +-- Standard Clause 7: Formal Pattern Matching Rules +-- ISO/IEC 19075-5, Clause 7 +-- ============================================================ +-- ------------------------------------------------------------ +-- 7.2.2 Alternation: first alternative is preferred +-- ------------------------------------------------------------ +-- (A | B): A preferred over B when both could match +-- Row 1 has both A and B flags: A should be chosen (first alternative) +WITH test_alt_prefer AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['B']), + (3, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_prefer +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A | B)) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 1 + 2 | {B} | 2 | 2 + 3 | {A} | 3 | 3 +(3 rows) + +-- (A{1,2} | B{2,3}): all A-matches before all B-matches +-- Standard example: preferment order is AA, A, BBB, BB +-- Rows 1-2 have both A and B: greedy A{1,2} should match 1-2 +WITH test_alt_quantified AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['A','B']), + (3, ARRAY['B']), + (4, ARRAY['B']), + (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_alt_quantified +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A{1,2} | B{2,3})) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 2 + 2 | {A,B} | 2 | 2 + 3 | {B} | 3 | 5 + 4 | {B} | 4 | 5 + 5 | {B} | | +(5 rows) + +-- ------------------------------------------------------------ +-- 7.2.3 Concatenation: lexicographic ordering +-- ------------------------------------------------------------ +-- ((A | B) (C | D)): preferment order is AC, AD, BC, BD +-- Row 1 matches A and B, Row 2 matches C and D +-- Preferred match: A then C (first alternatives in both positions) +WITH test_concat_lex AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['C','D']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_concat_lex +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (C | D)) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 2 + 2 | {C,D} | | +(2 rows) + +-- ((A | B) C): first alt (A) fails, second alt (B) succeeds +-- Tests backtracking: row 1 has only B, row 2 has C +WITH test_concat_backtrack AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['C']), + (3, ARRAY['A']), + (4, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_concat_backtrack +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A | B) C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | 1 | 2 + 2 | {C} | | + 3 | {A} | 3 | 4 + 4 | {C} | | +(4 rows) + +-- ------------------------------------------------------------ +-- 7.2.4 Quantification: greedy/reluctant, lexicographic > length +-- ------------------------------------------------------------ +-- V{2,4} greedy: longer match preferred +WITH test_quant_greedy AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_quant_greedy +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,4}) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {A} | | + 3 | {A} | | + 4 | {B} | | +(4 rows) + +-- V{2,4}? reluctant: shorter match preferred +WITH test_quant_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 test_quant_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,4}?) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {A} | | + 3 | {A} | | + 4 | {B} | | +(4 rows) + +-- ((A|B){1,2}) greedy: lexicographic > length +-- Standard example: preferment AA, AB, A, BA, BB, B +-- Single A preferred over B-starting longer match +WITH test_quant_lex_greedy AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, 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_quant_lex_greedy +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A | B){1,2})) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 2 + 2 | {B} | | +(2 rows) + +-- ((A|B){1,2}?) reluctant: lexicographic > length +-- Standard example: preferment A, AA, AB, B, BA, BB +-- Single A preferred over any B-starting match +WITH test_quant_lex_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, 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_quant_lex_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A | B){1,2}?)) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 1 + 2 | {B} | 2 | 2 +(2 rows) + +-- ------------------------------------------------------------ +-- 7.2.6 Anchors (not yet implemented - syntax error expected) +-- ------------------------------------------------------------ +-- ^ anchor: not yet supported +SELECT count(*) OVER w FROM (SELECT 1 AS v) t +WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (^ A) DEFINE A AS TRUE); +ERROR: syntax error at or near "^" +LINE 3: PATTERN (^ A) DEFINE A AS TRUE); + ^ +-- $ anchor: not yet supported +SELECT count(*) OVER w FROM (SELECT 1 AS v) t +WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A $) DEFINE A AS TRUE); +ERROR: syntax error at or near "$" +LINE 3: PATTERN (A $) DEFINE A AS TRUE); + ^ +-- ------------------------------------------------------------ +-- 7.2.8 Infinite repetitions of empty matches +-- (Perl lower-bound stopping rule) +-- ------------------------------------------------------------ +-- Standard examples from 7.2.8: +-- (A?){0,3}: allowed strings include STR00=(), STR01=(A), STR02=(empty), +-- STR03=(AA), STR04=(A,empty), STR07=(AAA), STR08=(AA,empty) +-- (A?){1,3}: same as {0,3} but STR00 excluded (min=1 not met) +-- (A?){2,3}: STR03-06 (len 2) and STR07,08,11,12 (len 3) are valid +-- STR06=(STRE,STRE) IS valid because non-final STRE at +-- position 1 fills the lower bound +-- (A??)*B: Standard 7.2.8 introductory example +-- "matched against a sequence of rows for which the only feasible +-- matching is: B" +-- A?? is reluctant, prefers empty. * is greedy but Perl rule stops +-- after empty match with min(=0) satisfied. +-- Expected: each B row matches alone (A?? empty, * stops, B matches) +WITH test_empty_reluctant_star AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_empty_reluctant_star +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 | {B} | 1 | 1 + 2 | {B} | 2 | 2 + 3 | {C} | | +(3 rows) + +-- (A?){0,3}: min=0, nullable inner. +-- A never matches but A? matches empty, satisfying min=0 immediately. +-- NFA reports 3 length-0 matches (one per row); first_value / last_value +-- are NULL because the window frame for an empty match has no rows. +WITH test_728_min0 AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, 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_728_min0 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){0,3}) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | | + 2 | {B} | | + 3 | {B} | | +(3 rows) + +-- (A?){1,3}: min=1, nullable inner. +-- A never matches; one empty iteration satisfies min=1. +-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame. +WITH test_728_min1 AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, 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_728_min1 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){1,3}) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | | + 2 | {B} | | + 3 | {B} | | +(3 rows) + +-- (A?){2,3}: min=2, nullable inner. Per ISO/IEC 19075-5 7.2.8 STR06 = (STRE STRE) +-- is valid: two empty iterations satisfy min=2. +-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame. +WITH test_728_min2 AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, 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_728_min2 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){2,3}) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | | + 2 | {B} | | + 3 | {B} | | +(3 rows) + +-- (A?){2,3} mixed: some rows match A, some don't +-- Rows 1-2: A matches, greedy takes 2 -> min satisfied (real match) +-- Row 3: A doesn't match, two empty iterations satisfy min=2 (length-0 match) +-- Row 4: A matches 1 real iter + 1 ff empty exit -> match 4-4 +WITH test_728_min2_mixed AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (4, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_728_min2_mixed +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){2,3}) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {A} | 2 | 2 + 3 | {B} | | + 4 | {A} | 4 | 4 +(4 rows) + +-- (A? B?){2,3}: multi-element nullable body with real matches +-- Body A? B? is nullable (both optional), but A and B DO match rows. +-- Real (non-empty) iterations loop back normally; fast-forward only +-- fires as a parallel exit path (EXIT ONLY, no greedy/reluctant loop). +-- Data: alternating A, B rows (6 rows) +-- Greedy: each row gets the longest match from its starting position. +-- Row 1: 3 iters (A@1,B@2)(A@3,B@4)(A@5,B@6) -> 1-6 +-- Row 5: 1 real iter + 1 ff empty exit -> 5-6 +WITH test_728_multi_body AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_728_multi_body +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? B?){2,3}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 6 + 2 | {B} | 2 | 6 + 3 | {A} | 3 | 6 + 4 | {B} | 4 | 6 + 5 | {A} | 5 | 6 + 6 | {B} | 6 | 6 +(6 rows) + +-- (A? B?){2,3}: pure empty body (nothing matches A or B). +-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame. +WITH test_728_multi_empty AS ( + SELECT * FROM (VALUES + (1, ARRAY['C']), + (2, ARRAY['C']), + (3, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_728_multi_empty +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? B?){2,3}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {C} | | + 2 | {C} | | + 3 | {C} | | +(3 rows) + +-- (A? B?){2,3}: mixed real and empty iterations +-- Row 1: iter1 real (A@1,B@2), iter2 at row 3 empty -> ff exit, match 1-2 +-- Row 3: C doesn't match A or B -> NULL +-- Row 4: iter1 real (A@4,B@5), iter2 at end empty -> ff exit, match 4-5 +WITH test_728_multi_mixed AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (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_728_multi_mixed +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? B?){2,3}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {B} | 2 | 2 + 3 | {C} | | + 4 | {A} | 4 | 5 + 5 | {B} | 5 | 5 +(5 rows) + +-- ------------------------------------------------------------ +-- 7.3 Pattern matching in theory and practice +-- ------------------------------------------------------------ +-- Standard's worked example: A? B+ with specific data +-- Preferment order: (A)(BBB), (A)(BB), (A)(B), ()(BBB), ()(BB), ()(B) +-- Row 1: A condition (price>100) is false -> A fails +-- Backtrack: empty A?, then B+ from row 1 +-- Expected: rows 1-3 match as B (A? takes empty match) +WITH test_73_example AS ( + SELECT * FROM (VALUES + (1, 60), + (2, 70), + (3, 40) + ) AS t(id, price) +) +SELECT id, price, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_73_example +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 price > 100, + B AS TRUE +); + id | price | match_start | match_end +----+-------+-------------+----------- + 1 | 60 | 1 | 3 + 2 | 70 | | + 3 | 40 | | +(3 rows) + diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out index c0bde1c5eec..2a267e6fc8d 100644 --- a/src/test/regress/expected/window.out +++ b/src/test/regress/expected/window.out @@ -1037,6 +1037,42 @@ FROM tenk1 WHERE unique1 < 10; 7 | 7 | 3 (10 rows) +SELECT nth_value(unique1,2) over (ORDER BY four rows between current row and 3 following exclude ties), + unique1, four +FROM tenk1 WHERE unique1 < 10; + nth_value | unique1 | four +-----------+---------+------ + 5 | 0 | 0 + 5 | 8 | 0 + 5 | 4 | 0 + 6 | 5 | 1 + 6 | 9 | 1 + 6 | 1 | 1 + 3 | 6 | 2 + 3 | 2 | 2 + | 3 | 3 + | 7 | 3 +(10 rows) + +SELECT last_value(unique1) over (ORDER BY four rows between 1 following and 2 following exclude ties), + unique1, four +FROM tenk1 WHERE unique1 < 12 ORDER BY four, unique1; + last_value | unique1 | four +------------+---------+------ + 1 | 0 | 0 + | 4 | 0 + 5 | 8 | 0 + 6 | 1 | 1 + | 5 | 1 + 10 | 9 | 1 + 3 | 2 | 2 + | 6 | 2 + 7 | 10 | 2 + | 3 | 3 + | 7 | 3 + | 11 | 3 +(12 rows) + SELECT sum(unique1) over (rows between 2 preceding and 1 preceding), unique1, four FROM tenk1 WHERE unique1 < 10; -- 2.43.0