From 6b2942c2f073ea748314e9fe23f406fd62093267 Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii Date: Sun, 30 Aug 2026 07:20:31 +0900 Subject: [PATCH v51 8/9] Row pattern recognition patch (tests: expected). --- src/test/regress/expected/rpr.out | 4217 ++++++++ src/test/regress/expected/rpr_base.out | 8915 +++++++++++++++++ src/test/regress/expected/rpr_explain.out | 7028 +++++++++++++ src/test/regress/expected/rpr_integration.out | 1917 ++++ src/test/regress/expected/rpr_nfa.out | 7009 +++++++++++++ 5 files changed, 29086 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..c4958c1b8d8 --- /dev/null +++ b/src/test/regress/expected/rpr.out @@ -0,0 +1,4217 @@ +-- +-- 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 the partition's first row (no earlier row to fetch) +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 the partition's last row (no later row to fetch) +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: DEFINE clause cannot contain volatile functions +-- 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: DEFINE clause cannot contain volatile functions +-- nextval is volatile, so a DEFINE that calls it is rejected +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: DEFINE clause cannot contain volatile functions +DROP SEQUENCE rpr_seq; +-- A volatile DEFINE is rejected in the planner, so a view that hides one is +-- created successfully and errors only 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: DEFINE clause cannot contain volatile functions +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): with A=any, B matches where the price beats the one two rows +-- back. On company1 (100, 200, 150, 140, 150, 90, 110, 130, 120, 130) that is +-- 200 -> 150, then 110 -> 130 -> 120, which stops where 130 only ties 130. +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): A matches while the price beats the one two rows ahead, so +-- company1 gives 200 on its own, since 150 only ties the 150 ahead of it, and +-- then 140, 150 up to where 90 falls short of 130. +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. A generic plan keeps +-- the parameter as a Param, so the offset is resolved at run time; a custom +-- plan would fold it to a constant and settle the reach at init. +SET plan_cache_mode = force_generic_plan; +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; +RESET plan_cache_mode; +-- 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, so the +-- first navigation result must survive the second fetch. B compares the +-- 1-back and 2-back tdate text. +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. +-- Each row reports only the match that starts at it. +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 +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 +-- Compound: an out-of-range inner offset must not skip validation of the outer +-- one. All four arms resolve their outer offset through the same call, so each +-- appears once, and the negative and the null case take two arms apiece. +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 PREV(FIRST(val, 99), -1) IS NULL +); +ERROR: row pattern navigation offset must not be negative +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 PREV(LAST(val, 99), NULL::int8) IS NULL +); +ERROR: row pattern navigation offset must not be null +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, 99), NULL::int8) IS NULL +); +ERROR: row pattern navigation offset must not be null +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(LAST(val, 99), -1) IS NULL +); +ERROR: row pattern navigation offset must not be negative +-- Same with a host variable, where the offset is not a Const the planner can +-- fold: one prepared statement, and only the outer offset decides the outcome. +-- The reach reads "runtime" here; a custom plan would fold it to 99 - 1 = 98. +SET plan_cache_mode = force_generic_plan; +PREPARE test_compound_illegal(int8, int8) AS +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 PREV(FIRST(val, $1), $2) IS NULL +); +EXPLAIN (COSTS OFF) EXECUTE test_compound_illegal(99, 1); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookahead: runtime + -> Sort + Sort Key: id + -> Seq Scan on rpr_nav +(7 rows) + +EXECUTE test_compound_illegal(99, 1); + id | val | count +----+-----+------- + 1 | 10 | 6 + 2 | 20 | 0 + 3 | 30 | 0 + 4 | 10 | 0 + 5 | 50 | 0 + 6 | 10 | 0 +(6 rows) + +EXECUTE test_compound_illegal(99, -1); +ERROR: row pattern navigation offset must not be negative +EXECUTE test_compound_illegal(99, NULL); +ERROR: row pattern navigation offset must not be null +EXECUTE test_compound_illegal(0, -1); +ERROR: row pattern navigation offset must not be negative +DEALLOCATE test_compound_illegal; +RESET plan_cache_mode; +-- An offset is settled before the first row is fetched, so a partition with no +-- rows at all rejects an illegal one just the same, and a legal one returns no +-- rows rather than failing. +CREATE TABLE rpr_nav_empty (id int, val int); +SELECT id, count(*) OVER w FROM rpr_nav_empty WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(val, -1) IS NULL +); +ERROR: row pattern navigation offset must not be negative +SELECT id, count(*) OVER w FROM rpr_nav_empty WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(val, 1) IS NULL +); + id | count +----+------- +(0 rows) + +SET plan_cache_mode = force_generic_plan; +PREPARE test_empty_offset(int8) AS +SELECT id, count(*) OVER w FROM rpr_nav_empty WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(val, $1) IS NULL +); +EXECUTE test_empty_offset(-1); +ERROR: row pattern navigation offset must not be negative +EXECUTE test_empty_offset(NULL); +ERROR: row pattern navigation offset must not be null +EXECUTE test_empty_offset(1); + id | count +----+------- +(0 rows) + +DEALLOCATE test_empty_offset; +RESET plan_cache_mode; +DROP TABLE rpr_nav_empty; +-- 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) +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 +-- 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 +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 +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. A single V: price falls to zero at the midpoint, then rises. +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 +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 when the reduced frame ends with NULLs +-- The search for a non-NULL value runs past the end of the 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 is true for a genuine NULL in the previous row +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..bd6904ecc66 --- /dev/null +++ b/src/test/regress/expected/rpr_base.out @@ -0,0 +1,8915 @@ +-- ============================================================ +-- 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 +-- +-- 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, permute, seek +CREATE TABLE rpr_keywords ( + id INT, + define INT, -- DEFINE keyword + initial INT, -- INITIAL keyword + past INT, -- PAST keyword + pattern INT, -- PATTERN keyword + permute INT, -- PERMUTE keyword + seek INT, -- SEEK keyword + skip INT -- SKIP keyword (pre-existing) +); +INSERT INTO rpr_keywords VALUES (1, 10, 20, 30, 40, 45, 50, 60); +SELECT id, define, initial, past, pattern, permute, seek, skip +FROM rpr_keywords +ORDER BY id; + id | define | initial | past | pattern | permute | seek | skip +----+--------+---------+------+---------+---------+------+------ + 1 | 10 | 20 | 30 | 40 | 45 | 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; +-- Pattern variables with no DEFINE entry +CREATE TABLE rpr_auto (id INT, val INT); +INSERT INTO rpr_auto VALUES (1, 10), (2, 20), (3, 30), (4, 15); +-- B has no DEFINE entry, so it matches every row +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 have no DEFINE entry, so they match every row +) +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; +-- Extra DEFINE variable not present in PATTERN +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; +-- A DEFINE predicate is evaluated only when its variable is tentatively +-- mapped. A is false at every row, so B is never reached; B's condition +-- (which would divide by zero) must never run, and every row is unmatched. +CREATE TABLE rpr_lazy (id INT, v INT); +INSERT INTO rpr_lazy VALUES (1, 1), (2, 2), (3, 3); +SELECT id, v, count(*) OVER w AS cnt +FROM rpr_lazy +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B) + DEFINE A AS v < 0, B AS 1 / (v - v) > 0 +) +ORDER BY id; + id | v | cnt +----+---+----- + 1 | 1 | 0 + 2 | 2 | 0 + 3 | 3 | 0 +(3 rows) + +DROP TABLE rpr_lazy; +-- ============================================================ +-- 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) +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 rejected by the +-- same execution-time check the literal 0 above reaches. +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) + +-- 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 bound must be between 1 and 2147483646 +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); +-- A greedy quantifier followed by a reluctant one over the same variable must +-- not be merged: the merged spellings A{2,3} and A{1,4} match all three rows +-- where these stop at two, so merging would change the preferred match. +SELECT id, count(*) OVER w FROM rpr_reluctant +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} A??) DEFINE A AS TRUE); + id | count +----+------- + 1 | 2 + 2 | 0 + 3 | 0 +(3 rows) + +SELECT id, count(*) OVER w FROM rpr_reluctant +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{1,2} A{0,2}?) DEFINE A AS TRUE); + id | count +----+------- + 1 | 2 + 2 | 0 + 3 | 1 +(3 rows) + +-- cascade: the reluctant middle VAR must stop the merge on both sides, where +-- the merged A{1,3} would match all three rows +SELECT id, count(*) OVER w FROM rpr_reluctant +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? A?? A) DEFINE A AS TRUE); + id | count +----+------- + 1 | 2 + 2 | 0 + 3 | 1 +(3 rows) + +-- *? (zero or more, reluctant) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A*?) + DEFINE A AS val > 0 +); + count +------- + 0 + 0 + 0 +(3 rows) + +-- +? (one or more, reluctant) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+?) + DEFINE A AS val > 0 +); + count +------- + 1 + 1 + 1 +(3 rows) + +-- ?? (zero or one, reluctant) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A??) + DEFINE A AS val > 0 +); + count +------- + 0 + 0 + 0 +(3 rows) + +-- {n,}? (n or more, reluctant) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2,}?) + DEFINE A AS val > 0 +); + count +------- + 2 + 0 + 0 +(3 rows) + +-- {n,m}? (n to m, reluctant) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1,3}?) + DEFINE A AS val > 0 +); + count +------- + 1 + 1 + 1 +(3 rows) + +-- {n}? (exactly n): min == max, so the reluctant flag is cleared and the +-- plan is indistinguishable from A{2}. This pins the normalization, not +-- shortest-match behaviour. +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) +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 bound must be between 1 and 2147483646 +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 ?) +-- Whitespace between the quantifier and "?" is insignificant: each form below +-- returns exactly what its unseparated counterpart above returns. +-- * ? (token separated) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A* ?) + DEFINE A AS val > 0 +); + count +------- + 0 + 0 + 0 +(3 rows) + +-- + ? (token separated) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ ?) + DEFINE A AS val > 0 +); + count +------- + 1 + 1 + 1 +(3 rows) + +-- {2,} ? (token separated) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2,} ?) + DEFINE A AS val > 0 +); + 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) +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) + +-- Two operator tokens where the first is not "?": the offending one is the +-- second, so that is what gets named and what the cursor points at +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: invalid token "?" after "*?" quantifier +LINE 6: PATTERN (A *? ?) + ^ +HINT: Valid quantifiers are: *, +, ?, {n}, {n,}, {,m}, {n,m}, each optionally followed by "?" for the reluctant version. +-- The first token is quoted as typed: stripping its "|" would name "*", and +-- "A* ?" is accepted, so the error would describe a pair the grammar takes +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: invalid token "?" after "*|" quantifier +LINE 6: PATTERN (A *| ?) + ^ +HINT: Valid quantifiers are: *, +, ?, {n}, {n,}, {,m}, {n,m}, each optionally followed by "?" for the reluctant version. +-- A first token that is no quantifier at all is itself the offending one, so it +-- is reported the same way as when it stands alone +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: unsupported quantifier "?+" +LINE 6: PATTERN (A ?+ ?) + ^ +HINT: Valid quantifiers are: *, +, ?, {n}, {n,}, {,m}, {n,m}, each optionally followed by "?" for the reluctant version. +-- The two tokens are reported separately, so the report cannot glue them into +-- a spelling that was never typed +SELECT COUNT(*) OVER w +FROM rpr_reluctant +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 +); +ERROR: invalid token "||" after "??" quantifier +LINE 6: PATTERN (A ?? || B) + ^ +HINT: Valid quantifiers are: *, +, ?, {n}, {n,}, {,m}, {n,m}, each optionally followed by "?" for the reluctant version. +-- The trailing "|" belongs to the alternation, not to the quantifier, so the +-- report drops it +SELECT COUNT(*) OVER w +FROM rpr_reluctant +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 +); +ERROR: invalid token "?" after "??" quantifier +LINE 6: PATTERN (A ?? ?|B) + ^ +HINT: Valid quantifiers are: *, +, ?, {n}, {n,}, {,m}, {n,m}, each optionally followed by "?" for the reluctant version. +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 (maximum 32767 elements, the FIN marker +-- included). Alternating distinct variables stop the optimizer from merging +-- consecutive elements, so each "A B" pair contributes two elements. +-- 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 TEMP TABLE rpr_nav0 (id int, v int); +INSERT INTO rpr_nav0 SELECT g, g*10 FROM generate_series(1, 5) g; +-- Two concurrently open portals of the SAME cached generic plan, with different +-- offset parameters. +-- +-- The parameterized cursor 'c' compiles to one plpgsql statement -> one SPI +-- cached plan. The recursive call OPENs a second portal of that same plan +-- (with a different offset) while the outer portal is already started but has +-- not yet FETCHed. +CREATE OR REPLACE FUNCTION rpr_nested(p_off int, depth int) +RETURNS SETOF text LANGUAGE plpgsql AS $$ +DECLARE + c CURSOR (o int) FOR + SELECT id, count(*) OVER w AS cnt + FROM rpr_nav0 + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS PREV(v, o) IS NULL); + r record; +BEGIN + OPEN c(p_off); + IF depth > 0 THEN + RETURN QUERY SELECT * FROM rpr_nested(p_off + 2, depth - 1); + END IF; + + LOOP + FETCH c INTO r; + EXIT WHEN NOT FOUND; + RETURN NEXT format('off=%s id=%s cnt=%s', p_off, r.id, r.cnt); + END LOOP; + CLOSE c; +END $$; +SET plan_cache_mode = force_generic_plan; +SELECT * FROM rpr_nested(1, 1); + rpr_nested +------------------ + off=3 id=1 cnt=1 + off=3 id=2 cnt=1 + off=3 id=3 cnt=1 + off=3 id=4 cnt=0 + off=3 id=5 cnt=0 + off=1 id=1 cnt=1 + off=1 id=2 cnt=0 + off=1 id=3 cnt=0 + off=1 id=4 cnt=0 + off=1 id=5 cnt=0 +(10 rows) + +RESET plan_cache_mode; +DROP FUNCTION rpr_nested(int, int); +CREATE TABLE rpr_nav (id INT, val INT); +INSERT INTO rpr_nav VALUES + (1, 10), (2, 20), (3, 15), (4, 25), (5, 30); +-- Avoid evaluating the inner argument expression when the target row is out +-- of range or does not exist. PREV misses on the first row of the partition +-- and NEXT on the last, so the two directions are checked separately. +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav t +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A) DEFINE A AS PREV(val is not null) is null); + id | cnt +----+----- + 1 | 1 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav t +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A) DEFINE A AS NEXT(val is not null) is null); + id | cnt +----+----- + 1 | 0 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 1 +(5 rows) + +-- FIRST and LAST cannot miss under PATTERN (A): the match is one row long, so +-- the target is the current row and the slot swap is elided. These two run +-- that path; the query further down is what pins what it writes. +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav t +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A) DEFINE A AS LAST(val is not null) is null); + id | cnt +----+----- + 1 | 0 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav t +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A) DEFINE A AS FIRST(val is not null) is null); + id | cnt +----+----- + 1 | 0 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +-- The elided path has to report the target row as present, since resnull may +-- still be null from a scan whose navigation missed. Here the navigation is +-- the whole DEFINE, so nothing overwrites resnull in between, and inlining the +-- function makes the offset a parameter: 1 misses, then 0 elides. +CREATE FUNCTION rpr_nav_off(k int) RETURNS SETOF bigint LANGUAGE sql STABLE AS $$ + SELECT count(*) OVER w FROM rpr_nav WHERE id = 1 + WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) DEFINE A AS PREV(val > 0, k)) $$; +SELECT o.k, f FROM (VALUES (1), (0)) o(k), LATERAL rpr_nav_off(o.k) f; + k | f +---+--- + 1 | 0 + 0 | 1 +(2 rows) + +DROP FUNCTION rpr_nav_off(int); +-- Under a longer pattern FIRST and LAST can miss. On the first row of a +-- match currentpos equals match_start, so an offset of one falls outside the +-- match in either direction, and the compound forms miss on their inner +-- navigation, which is a separate bound from the one the plain forms cross. +-- The argument does not propagate null, so a missing row is told apart from +-- a row of nulls. +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav t +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS LAST(val is not null, 1) is null); + id | cnt +----+----- + 1 | 1 + 2 | 1 + 3 | 1 + 4 | 1 + 5 | 1 +(5 rows) + +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav t +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS FIRST(val is not null, 1) is null); + id | cnt +----+----- + 1 | 1 + 2 | 1 + 3 | 1 + 4 | 1 + 5 | 1 +(5 rows) + +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav t +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(FIRST(val is not null, 1), 2) is null); + id | cnt +----+----- + 1 | 5 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav t +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(LAST(val is not null, 1), 2) is null); + id | cnt +----+----- + 1 | 3 + 2 | 0 + 3 | 0 + 4 | 1 + 5 | 1 +(5 rows) + +-- Not a duplicate of the PREV(val is not null) case: the argument here is +-- true where that one is false, and a missing row still has to win over both. +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav t +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A) DEFINE A AS PREV(val is null) is null); + id | cnt +----+----- + 1 | 1 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +-- Skipping the argument is data-dependent: restricted to the row PREV misses +-- on, a division by zero in it never runs; over the whole table NEXT reaches +-- a row for all but the last and it does. +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav t WHERE id = 1 +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A) DEFINE A AS PREV(val / 0) > 0); + id | cnt +----+----- + 1 | 0 +(1 row) + +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav t +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A) DEFINE A AS NEXT(val / 0) > 0); +ERROR: division by zero +-- Here the null reaches the DEFINE predicate itself instead of an IS NULL +-- An all-NULL target row would have made v IS NULL true and matched the +-- first row, so this pins the predicate side of the same behaviour. +WITH t(id, v) AS (VALUES (1, 10), (2, 20)) +SELECT id, count(*) OVER w AS cnt +FROM t +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A) DEFINE A AS PREV(v IS NULL)); + id | cnt +----+----- + 1 | 0 + 2 | 0 +(2 rows) + +-- Constant folding can leave a navigation argument with no column reference +-- at all (v folds to 10, so PREV(v IS NULL) becomes PREV(false)), which the +-- planner has to accept rather than re-run the parse-time rejection. +WITH t(id, v) AS (VALUES (1, 10)) +SELECT id, count(*) OVER w AS cnt +FROM t +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A) DEFINE A AS PREV(v IS NULL)); + id | cnt +----+----- + 1 | 0 +(1 row) + +-- XXX Folding evaluates the argument while planning, with the current row's +-- value standing in for the target row's, so this divides by zero even though +-- PREV has no row to navigate to. +WITH t(id, v) AS (VALUES (1, 10)) +SELECT id, count(*) OVER w AS cnt +FROM t +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A) DEFINE A AS PREV(v / 0) > 0); +ERROR: division by zero +-- 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 unless it folds away +CREATE FUNCTION prev(integer) RETURNS integer + LANGUAGE plpgsql VOLATILE AS 'BEGIN RETURN -999; END'; +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: DEFINE clause cannot contain volatile functions +-- The SQL body inlines and folds to a constant, so no volatile call +-- is left for the check to find +CREATE OR REPLACE 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 (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) + +-- No OVER references the window, so flattening the subquery drops it +-- before the check runs, the same way an unreferenced CTE is never planned +SELECT id FROM ( + SELECT id FROM nt + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS random() > 0.5)) s +ORDER BY id; + id +---- + 1 + 2 + 3 + 4 + 5 +(5 rows) + +-- ERROR: OFFSET 0 keeps the subquery, so its DEFINE is checked +SELECT id FROM ( + SELECT id FROM nt + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS random() > 0.5) OFFSET 0) sub; +ERROR: DEFINE clause cannot contain volatile functions +-- WHERE false makes the subquery rel dummy, so the planner never plans it +-- and nothing looks at its DEFINE +SELECT id FROM ( + SELECT id FROM nt + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS random() > 0.5) OFFSET 0) sub +WHERE false; + id +---- +(0 rows) + +-- The volatile is in a dead CASE arm that folds away, so nothing +-- volatile is left for the check to find +SELECT id FROM ( + SELECT id FROM nt + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS CASE WHEN false THEN random()::int > 0 + ELSE val > 5 END)) s +ORDER BY id; + id +---- + 1 + 2 + 3 + 4 + 5 +(5 rows) + +-- ERROR: folding can splice in a volatile that parse analysis never saw -- a +-- STABLE function whose default argument is volatile -- and the check runs late +-- enough to catch it +CREATE FUNCTION rpr_off_leak(n bigint DEFAULT (random() * 5)::bigint) + RETURNS bigint LANGUAGE sql STABLE AS 'SELECT n'; +SELECT count(*) OVER w FROM generate_series(1, 100) g(v) + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS v > PREV(v, rpr_off_leak())); +ERROR: DEFINE clause cannot contain volatile functions +DROP FUNCTION rpr_off_leak(bigint); +-- A UNION ALL leaf is flattened like any other subquery, so its +-- unreferenced window goes the same way +SELECT id FROM ( + SELECT id FROM nt + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS random() > 0.5) + UNION ALL + SELECT id FROM nt) s; + id +---- + 1 + 2 + 3 + 4 + 5 + 1 + 2 + 3 + 4 + 5 +(10 rows) + +-- An unreferenced CTE is never planned, so nothing looks at its +-- DEFINE +WITH unused AS ( + SELECT id FROM nt + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS random() > 0.5)) +SELECT 1; + ?column? +---------- + 1 +(1 row) + +-- ERROR: referencing it plans the CTE, and the check reaches the DEFINE there +WITH used AS ( + SELECT id FROM nt + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS random() > 0.5)) +SELECT count(*) FROM used; +ERROR: DEFINE clause cannot contain volatile functions +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; +-- PERMUTE +CREATE TABLE rpr_permute (id INT, val INT); +INSERT INTO rpr_permute VALUES (1, 10); +-- PERMUTE syntax is recognized, but the feature is not supported +SELECT COUNT(*) OVER w +FROM rpr_permute +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (PERMUTE(A)) + DEFINE A AS val > 0 +); +ERROR: PERMUTE is not supported +LINE 6: PATTERN (PERMUTE(A)) + ^ +HINT: Write the alternations out instead, or write "permute" to use it as a pattern variable. +-- rejected the same way for a list, and for sub-patterns of any shape +SELECT COUNT(*) OVER w +FROM rpr_permute +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (PERMUTE(A+ B, C | D)) + DEFINE A AS val > 0, B AS val > 1, C AS val > 2, D AS val > 3 +); +ERROR: PERMUTE is not supported +LINE 6: PATTERN (PERMUTE(A+ B, C | D)) + ^ +HINT: Write the alternations out instead, or write "permute" to use it as a pattern variable. +-- PERMUTE stays unreserved, so it is still usable as a pattern variable +SELECT COUNT(*) OVER w +FROM rpr_permute +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (PERMUTE A) + DEFINE PERMUTE AS val > 5, A AS val > 0 +); + count +------- + 0 +(1 row) + +-- Except immediately before a group: PERMUTE shifts on "(" whether or not a +-- comma follows, so such a variable lands on the not-supported error, and for +-- that user the alternations advice is beside the point. The hint has to name +-- the way out too. +SELECT COUNT(*) OVER w +FROM rpr_permute +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (PERMUTE (A | B)) + DEFINE PERMUTE AS val > 5, A AS val > 0, B AS val > 9 +); +ERROR: PERMUTE is not supported +LINE 6: PATTERN (PERMUTE (A | B)) + ^ +HINT: Write the alternations out instead, or write "permute" to use it as a pattern variable. +-- quoted, the same query runs +SELECT COUNT(*) OVER w +FROM rpr_permute +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ("permute" (A | B)) + DEFINE PERMUTE AS val > 5, A AS val > 0, B AS val > 9 +); + count +------- + 0 +(1 row) + +-- Deparse must quote such a variable, or a view holding one would reparse as +-- the PERMUTE syntax; other variables stay unquoted +CREATE VIEW rpr_permute_v AS + SELECT COUNT(*) OVER w AS cnt FROM rpr_permute + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ("permute" (A)) + DEFINE PERMUTE AS val > 5, A AS val > 0 + ); +SELECT pg_get_viewdef('rpr_permute_v'::regclass); + pg_get_viewdef +----------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_permute + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN ("permute" (a)) + + DEFINE + + permute AS (val > 5), + + a AS (val > 0)); +(1 row) + +-- Quoted even where no group follows: the deparser quotes the name wherever +-- it appears rather than looking ahead for the "(" that would make it +-- ambiguous +CREATE VIEW rpr_permute_v2 AS + SELECT COUNT(*) OVER w AS cnt FROM rpr_permute + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (PERMUTE A) + DEFINE PERMUTE AS val > 5, A AS val > 0 + ); +SELECT pg_get_viewdef('rpr_permute_v2'::regclass); + pg_get_viewdef +----------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_permute + + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN ("permute" a) + + DEFINE + + permute AS (val > 5), + + a AS (val > 0)); +(1 row) + +-- EXPLAIN deparses the compiled pattern with a printer of its own, so it has +-- to quote the same names ruleutils does. The alternation keeps the group +-- from being flattened away, which is what puts a "(" after the variable. +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w +FROM rpr_permute +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ("permute" (A | B)) + DEFINE PERMUTE AS val > 5, A AS val > 0, B AS val > 9 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: "permute" (a | b) + -> Sort + Sort Key: id + -> Seq Scan on rpr_permute +(6 rows) + +DROP VIEW rpr_permute_v, rpr_permute_v2; +DROP TABLE rpr_permute; +-- ============================================================ +-- 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) + +-- Quoting the deparser adds on its own: permute is unreserved, so the stored +-- rule holds a plain name and only the deparser knows it has to come back +-- quoted. Restoring this view is what proves it does. +CREATE VIEW rpr_serial_permute AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ("permute" (A | B)) + DEFINE PERMUTE AS val > 0, A AS val > 10, B AS val > 20); +SELECT pg_get_viewdef('rpr_serial_permute'::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 ("permute" (a | b)) + + DEFINE + + permute AS (val > 0), + + a AS (val > 10), + + b AS (val > 20)); +(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) + +-- Multi-relation view: a DEFINE column is deparsed with no qualifier, so it +-- must stay unambiguous across the join for the view to re-parse. This one +-- is left in place like the rest of the section, which is what puts an +-- unqualified DEFINE column through the pg_dump round trip at all. +CREATE TABLE rpr_serial_j (id INT, qty INT); +INSERT INTO rpr_serial_j VALUES (1, 5), (2, 7), (3, 9), (4, 11), (5, 13); +CREATE VIEW rpr_serial_join AS +SELECT s.id, count(*) OVER w AS cnt +FROM rpr_serial s JOIN rpr_serial_j j ON s.id = j.id +WINDOW w AS (ORDER BY s.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (UP+) DEFINE UP AS val > 0); +SELECT pg_get_viewdef('rpr_serial_join'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------- + SELECT s.id, + + count(*) OVER w AS cnt + + FROM (rpr_serial s + + JOIN rpr_serial_j j ON ((s.id = j.id))) + + WINDOW w AS (ORDER BY s.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (up+) + + DEFINE + + up AS (val > 0)); +(1 row) + +-- Ambiguity introduced after the view was created: ALTER TABLE adds a column +-- whose name already appears in the other side of the join, so the deparser +-- must qualify or alias it. sv3 shows the same text is rejected on a fresh +-- CREATE VIEW; sv4 shows the alias form that survives. These stay temporary +-- and are dropped at the end: sv is deliberately unrestorable, so leaving it +-- in place would hand pg_dump a view that cannot be restored. +CREATE TEMP TABLE sa (id int, price int); +CREATE TEMP TABLE sb (id int, qty int); +INSERT INTO sa VALUES (1,10),(2,20); +INSERT INTO sb VALUES (1,5),(2,7); +CREATE TEMP VIEW sv AS +SELECT a.id, count(*) OVER w AS cnt +FROM sa a JOIN sb b ON a.id = b.id +WINDOW w AS (ORDER BY a.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (UP+) DEFINE UP AS price > 0); +ALTER TABLE sb ADD COLUMN price int; +SELECT pg_get_viewdef('sv'::regclass, true); + pg_get_viewdef +------------------------------------------------------------------------------- + SELECT a.id, + + count(*) OVER w AS cnt + + FROM sa a + + JOIN sb b ON a.id = b.id + + WINDOW w AS (ORDER BY a.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (up+) + + DEFINE + + up AS price > 0); +(1 row) + +-- ERROR: the deparsed text above no longer re-parses +CREATE TEMP VIEW sv3 AS +SELECT a.id, count(*) OVER w AS cnt +FROM sa a JOIN sb b ON a.id = b.id +WINDOW w AS (ORDER BY a.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (UP+) DEFINE UP AS price > 0); +ERROR: column reference "price" is ambiguous +LINE 5: PATTERN (UP+) DEFINE UP AS price > 0); + ^ +CREATE TEMP VIEW sv4 AS +SELECT a.id, count(*) OVER w AS cnt +FROM sa a (id, price) JOIN sb b (id, qty, price_1) ON a.id = b.id +WINDOW w AS (ORDER BY a.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (UP+) DEFINE UP AS price > 0); +SELECT pg_get_viewdef('sv4'::regclass, true); + pg_get_viewdef +------------------------------------------------------------------------------- + SELECT a.id, + + count(*) OVER w AS cnt + + FROM sa a + + JOIN sb b(id, qty, price_1) ON a.id = b.id + + WINDOW w AS (ORDER BY a.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (up+) + + DEFINE + + up AS price > 0); +(1 row) + +SELECT * FROM sv4; + id | cnt +----+----- + 1 | 2 + 2 | 0 +(2 rows) + +SELECT * FROM sv; + id | cnt +----+----- + 1 | 2 + 2 | 0 +(2 rows) + +DROP VIEW sv4; +DROP VIEW sv; +DROP TABLE sa, sb; +-- 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. The +-- lexer glues the trailing '|' into one Op token; the grammar reattaches it as +-- the lowest-precedence alternation once the surrounding sequence is built. +-- 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 -- show when the '|B' +-- alternative is reachable. With "*" the first branch always succeeds, so B +-- never fires: the greedy form matches the whole run and the reluctant form +-- matches empty, and on a B row the empty match still outranks B. With "+" +-- the first branch fails on a B row, so there the B alternative fires; on an +-- A row the greedy form matches the run and the reluctant form one row. +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 | 0 | 0 | 1 | 1 + 5 | 6 | 1 | 0 | 1 | 1 + 6 | -2 | 0 | 0 | 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}, each optionally followed by "?" for the reluctant version. +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... + ^ +-- the dangling operator is blamed on the element it hangs off, not on the first +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: ...EN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B*|) 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: unsupported quantifier "*||" +LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFI... + ^ +HINT: Valid quantifiers are: *, +, ?, {n}, {n,}, {,m}, {n,m}, each optionally followed by "?" for the reluctant version. +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}, each optionally followed by "?" for the reluctant version. +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 token "*?" after "?" quantifier +LINE 1: ...N CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DEFI... + ^ +HINT: Valid quantifiers are: *, +, ?, {n}, {n,}, {,m}, {n,m}, each optionally followed by "?" for the reluctant version. +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}, each optionally followed by "?" for the reluctant version. +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}, each optionally followed by "?" for the reluctant version. +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}, each optionally followed by "?" for the reluctant version. +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}, each optionally followed by "?" for the reluctant version. +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}, each optionally followed by "?" for the reluctant version. +-- 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 token "??" after "?" quantifier +LINE 1: ...S BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A? ??) DEFINE... + ^ +HINT: Valid quantifiers are: *, +, ?, {n}, {n,}, {,m}, {n,m}, each optionally followed by "?" for the reluctant version. +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 used as a qualifier +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) + +-- 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) + ^ +-- DEFINE variables that do not appear in PATTERN +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. +-- Sibling navigations: prohibited, but they are not a deeper nesting, +-- so the inner navigation must be reported as not being the direct +-- argument rather than as a third level. +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(v)) > 0 +); +ERROR: row pattern navigation operation must be a direct argument of the outer navigation +LINE 6: DEFINE A AS PREV(FIRST(v) + LAST(v)) > 0 + ^ +HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. +-- Three navigations, but the inner one is again not the whole argument, so +-- that is what gets reported and the depth is not reached +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)) + 1) > 0 +); +ERROR: row pattern navigation operation must be a direct argument of the outer navigation +LINE 6: DEFINE A AS PREV(FIRST(PREV(v)) + 1) > 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) + +-- ============================================================ +-- 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} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Consecutive VAR merge: A{2} A{3} -> a{5} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2} A{3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{5} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Consecutive VAR merge: A+ A* -> a+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ A*) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Consecutive VAR merge: A A+ -> a{2,} +-- 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,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B){5}) ((A B){10})) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b){15} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Consecutive GROUP merge with unbounded: (A B)+ (A B)+ -> (a b){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ (A B)+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a~ b~){2,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,} +-- 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,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- PREFIX merge: A B (A B)+ -> (a b){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (A B)+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a~ b~){2,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- PREFIX and SUFFIX merge: A B (A B)+ A B -> (a b){3,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (A B)+ A B) DEFINE A AS val <= 40, B AS val > 40); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a~ b~){3,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Flatten nested: A ((B) (C)) -> a b c +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A ((B) (C))) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- 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)+ + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- ALT deduplicate: (A | B | A) -> (a | b) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B | A)+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b)+ + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- 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} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Quantifier multiply: (((A B){2}?){3}) -> (a b){6} +-- {2}? has a fixed count, so reluctance is normalized away and the +-- multiplication that (((A B){2}){3}) gets applies here too +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} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9} +-- outer exact, child range - optimization applies +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,3}){3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{6,9} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Quantifier NO multiply: (A{2}){2,3} stays as (a{2}){2,3} +-- outer range - gaps would occur (4,6 not 4,5,6), no optimization +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}){2,3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2}){2,3} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Quantifier NO multiply: (A{2}){2,} stays as (a{2}){2,} +-- outer unbounded - gaps would occur (4,6,8,... not 4,5,6,...), no optimization +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}){2,}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2}~){2,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Quantifier multiply: (A){2,} -> a{2,} +-- child exact 1 - no gaps, optimization applies +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A){2,}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Quantifier multiply: (A)+ -> a+ +-- child exact 1 - no gaps, optimization applies +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A)+) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Quantifier NO multiply: (A{2}){3,5} stays as (a{2}){3,5} +-- outer range, child exact > 1 - gaps would occur (6,8,10 not 6,7,8,9,10) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}){3,5}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2}){3,5} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Quantifier multiply refused: (A{2,3}){2,3} stays nested. +-- The counts [4,6] U [6,9] = [4,9] are contiguous, but a bounded child with a +-- lower bound to fall short of makes the nested form prefer a shorter match +-- than a{4,9} would. +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,3}){2,3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2,3}){2,3} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- 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} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Nested unbounded: (A*)* -> a* +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A*)*) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a*# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Nested unbounded: (A+)* -> a* +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+)*) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a*# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Nested unbounded: (A+)+ -> a+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+)+) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- 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,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- (A{2,3}){2,4} stays nested for the same reason, even though the counts +-- [4,6] U [6,9] U [8,12] = [4,12] are 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{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{2,3}){2,4} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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})? + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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})? + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Unwrap GROUP{1,1}: (A) -> a +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A)) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Unwrap GROUP{1,1}: (A B) -> a b +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Combined optimization: A A (B B)+ B B C C C -> a{2} (b{2}){2,} c{3} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A A (B B)+ B B C C C) + DEFINE A AS val <= 20, B AS val > 20 AND val <= 70, C AS val > 70); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2} (b{2}){2,} c{3} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Consecutive GROUP merge with unbounded: (A+) (A+) -> a{2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+) (A+)) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Consecutive GROUP merge finite: (A{10}){20} -> a{200} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{10}){20}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{200} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Different GROUP prevents merge: (A B){2} (C D){3} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){2} (C D){3}) + DEFINE A AS val <= 25, B AS val > 25 AND val <= 50, + C AS val > 50 AND val <= 75, D AS val > 75); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b){2} (c d){3} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Different children count prevents merge: (A B)+ (A B C)+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ (A B C)+) + DEFINE A AS val <= 33, B AS val > 33 AND val <= 66, C AS val > 66); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a~ b~)+# (a b c)+ + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- PREFIX only merge: A B (A B)+ -> (a b){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (A B)+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a~ b~){2,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- SUFFIX only merge: (A B)+ A B -> (a b){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ A B) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a~ b~){2,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Multiple SUFFIX absorption with skipUntil: (A B)+ A B A B C +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ A B A B C) + DEFINE A AS val <= 50, B AS val > 50 AND val <= 75, C AS val > 75); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a~ b~){3,}# c + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- PREFIX merge with remaining prefix: A B C D (C D)+ -> 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,} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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)+ + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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,} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- PREFIX merge with multiple quantifiers: A+ B* C? (A+ B* C?)+ -> (a+ b* c?){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ B* C? (A+ B* C?)+) + DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+# b* c?){2,} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- SUFFIX merge refused: (A B*)+ A B* stays as written. The body A B* has no +-- fixed row count, so folding the trailing copy into the group would move the +-- group's stop decision ahead of that copy's own choices. The PREFIX merge +-- just above keeps working on the same kind of body -- a leading copy is +-- mandatory, so it merges without reordering anything. +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* + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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) + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Reluctant optimization bypass: quantifier multiply (outer reluctant) +-- (A+){2,4}? stays nested where the greedy (A+){2,4} multiplies 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+){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,4}? + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Reluctant optimization bypass: quantifier multiply (inner reluctant) +-- (A{2,3}?){3} stays as (a{2,3}?){3} (greedy (A{2,3}){3} merges to a{6,9}) +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{2,3}?){3} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Fixed count normalizes away: (A{2}?){3} -> a{6}, same as (A{2}){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{2}?){3}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{6} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- {0,} is the only brace spelling that reaches an unbounded min 0 +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{0,}) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a*# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Bare {1} and {1,1} carry no quantifier, on a VAR or on a GROUP +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1} B{1,1} (C){1}) + DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- A reluctant {1,1} normalizes to a bare variable +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,1}? (B C){1}) + DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- 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)+? + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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)+? + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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+? + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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)+ + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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+# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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,} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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+# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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 + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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) + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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) + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- ============================================================ +-- Absorption Flag Display Tests +-- ============================================================ +-- Tests absorption marker display in EXPLAIN output +-- Markers: ~ = branch element, # = comparison point +-- 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+# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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~)+# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- ALT both absorbable: A+ | B+ -> (a+# | b+#) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ | B+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+# | b+#) + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- ALT one absorbable: A+ | B -> (a+# | b) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ | B) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+# | b) + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Sequence with absorbable start: A+ B -> a+# b +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+# b + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Complex nested: ((A+ B) | C) D | A B C - deeply nested ALT +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (((A+ B) | C) D | A B C) + DEFINE A AS val <= 30, B AS val <= 60, C AS val <= 80, D AS val > 80); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a+# b | c) d | a b c) + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- ALT branch tail not over-marked: A | (B C)+ (D E)+ -> (a | (b~ c~)+# (d e)+) +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 E)+) + DEFINE A AS val <= 20, B AS val <= 40, C AS val <= 60, D AS val <= 80, E AS val > 80); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | (b~ c~)+# (d e)+) + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Nested unbounded: (A+ | B)+ -> (a+# | b)+ (first iteration absorbable) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ | B)+) + DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+# | b)+ + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- ALT inside unbounded GROUP: (A+ B | A B)* -> (a+# b | a b)* (first iteration absorbable) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ B | A B)*) + DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+# b | a b)* + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Fixed-length group absorbable: (A{2} B{3})+ -> (a{2}~ b{3}~)+# +-- 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}~)+# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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~)+# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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}~)+# + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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})+ + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 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?)+ + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A B+) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Non-absorbable (no unbounded branch): (A | B){2,} -> (a | b){2,} (no markers) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A | B){2,}) DEFINE A AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b){2,} + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Non-absorbable (SKIP TO NEXT ROW): A+ -> a+ (no markers) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW PATTERN (A+) DEFINE A AS val > 0); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Non-absorbable (limited frame): A+ -> a+ (no markers) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0); + QUERY PLAN +---------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND '10'::bigint FOLLOWING) + Pattern: a+ + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- A marker on a quoted variable name lands outside the closing quote: a +-- keyword name takes the comparison point, "select"+ -> "select"+# +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 ("select"+ B) + DEFINE "select" AS val <= 50, B AS val > 50); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: "select"+# b + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- The same for the branch marker, on a name quoted for its space: +-- ("My Var" A)+ -> ("My Var"~ 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 (("My Var" A)+ B) + DEFINE "My Var" AS val <= 25, 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: ("My Var"~ a~)+# b + -> Sort + Sort Key: id + -> Seq Scan on rpr_plan +(6 rows) + +-- Reluctant {1}? quantifier: min == max, so the plan normalizes it away +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 b + -> Sort + Sort Key: val + -> Seq Scan on rpr_plan +(6 rows) + +-- ============================================================ +-- Absorption Analysis Tests +-- ============================================================ +-- Tests context absorption optimization (O(n^2) -> O(n)) +-- 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); +-- Min quantifier overflow causes optimization fallback (min == max case) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2000000000}){2}) + DEFINE A AS val > 0 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2000000000}){2} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +-- Max-only quantifier overflow causes optimization fallback +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{1,2000000000}){2}) + DEFINE A AS val > 0 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{1,2000000000}){2} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +-- 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 bound must be between 1 and 2147483646 +LINE 6: PATTERN ((A{2000000000,2147483647}){2}) + ^ +-- Nested unbounded with large min causes overflow fallback +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2000000000,}){2000000000,}) + DEFINE A AS val > 0 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a{2000000000,}#){2000000000,} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +-- Prefix mismatch causes optimization fallback +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (C D)+) + DEFINE A AS val > 0, B AS val > 5, C AS val > 10, D AS val > 15 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b (c d)+ + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +-- 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,} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +-- One more than that sum does not fit in int32. The fallback looks the +-- same as the case above; this one reaches the overflow check instead of the +-- >= INF comparison. +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{1073741825,} 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{1073741825,}# a{1073741823,} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +-- VAR merge falls back when the max sum lands exactly on INF. +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,1073741823} A{1,1073741824}) + DEFINE A AS val > 0 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{1,1073741823} a{1,1073741824} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +-- One below that sum is the largest max the merge may keep. +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,1073741822} A{1,1073741824}) + DEFINE A AS val > 0 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2,2147483646} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +-- One above that does not fit in int32; the overflow check rejects it. +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,1073741824} A{1,1073741824}) + DEFINE A AS val > 0 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{1,1073741824} a{1,1073741824} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +-- An operand that is already unbounded still merges. +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,1073741823} A{1,}) + DEFINE A AS val > 0 +); + QUERY PLAN +------------------------------------------------------------------------------- + WindowAgg + Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +-- 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,} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +-- Consecutive GROUP merge whose max sum is exactly INF causes fallback, +-- where one less merges. Without the guard the merged max would alias INF and +-- a bounded pattern would become unbounded. +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){1,1073741823} (A B){1,1073741824}) + 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){1,1073741823} (a b){1,1073741824} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +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){1,1073741822} (A B){1,1073741824}) + 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){2,2147483646} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +-- The prefix merge adds one iteration, so it declines a min already at +-- INF - 1 and a max already at INF - 1; one less than either merges. +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 (A B){2147483646,}) + 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 (a b){2147483646,} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +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 (A B){2147483645,}) + 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~){2147483646,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +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 (A B){1,2147483646}) + 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 (a b){1,2147483646} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +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 (A B){1,2147483645}) + 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){2,2147483646} + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +-- The suffix merge has the same boundary as the prefix merge. +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){2147483646,} A B) + 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~){2147483646,}# a b + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +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){2147483645,} A B) + 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~){2147483646,}# + -> Sort + Sort Key: id + -> Seq Scan on rpr_fallback +(6 rows) + +DROP TABLE rpr_fallback; +-- ============================================================ +-- Planner Integration Tests +-- ============================================================ +-- Tests full planning pipeline and WindowAgg plan node creation +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) + +-- ============================================================ +-- Subquery and CTE Tests +-- ============================================================ +-- 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 +-- ============================================================ +-- 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 +-- ============================================================ +-- 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 +-- ============================================================ +-- 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 +-- ============================================================ +-- 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_inline(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_inline(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_inline(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); +-- 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 + ^ +-- 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) + +-- 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. +-- Pattern nesting-depth boundary: 254 levels are accepted, 255 rejected. +-- Reluctant quantifiers are not subject to quantifier multiplication, so the +-- nesting survives optimization and still reaches the depth check. +-- ECHO is silenced so the generated deeply nested patterns do not flood the +-- expected output. +-- 254 nested GROUP{3,7}? -> depth 254 = maximum, accepted. +-- 255 nested GROUP{3,7}? -> depth 255 > maximum, rejected. +\set ECHO none + id | val | count +----+-----+------- + 1 | 10 | 0 + 2 | 20 | 0 +(2 rows) + +ERROR: pattern nesting too deep +DETAIL: Pattern nesting depth 255 exceeds maximum 254. +DROP TABLE rpr_errors; +-- ============================================================ +-- Basic Pattern Matching +-- ============================================================ +-- 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) + +-- 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) + +-- 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) + +-- 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) + +-- 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) + +-- 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) + +-- 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) + +-- (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) + +-- (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) + +-- (A+ | (A | B)+)* - nested alternation inside quantified group +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 +-- ============================================================ +-- Nested unbounded quantifiers that the optimizer collapses. +-- (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) + +-- (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) + +-- (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) + +-- (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) + +-- (((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; diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out new file mode 100644 index 00000000000..ecfe109547a --- /dev/null +++ b/src/test/regress/expected/rpr_explain.out @@ -0,0 +1,7028 @@ +-- ============================================================ +-- 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 + 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) +(9 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 + 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) +(8 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 + 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) +(8 rows) + +-- 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) + 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) +(9 rows) + +-- Sequential alternations at the same depth +-- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)" +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))* + 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) +(8 rows) + +-- ALT branch whose tail is a group +-- (A | (B C)+ (D E)+) means A | ((B C)+ (D E)+) by precedence, so the two +-- trailing groups must deparse inside one branch, not as separate branches. +CREATE VIEW rpr_ev_basic_deparse_alttail 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 % 6 = 1, B AS v % 6 = 2, C AS v % 6 = 3, D AS v % 6 = 4, E AS v % 6 = 5 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_deparse_alttail'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------------- + PATTERN (a | (b c)+ (d e)+) +(1 row) + +-- The viewdef above deparses the parse tree; this one deparses the compiled +-- element array, which enumerates ALT branches via the SEP terminators. +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 % 6 = 1, B AS v % 6 = 2, C AS v % 6 = 3, D AS v % 6 = 4, E AS v % 6 = 5 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=30.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | (b~ c~)+# (d e)+) + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 103 total, 0 merged + NFA Contexts: 3 peak, 31 total, 15 pruned + NFA: 10 matched (len 1/4/2.5), 0 mismatched + NFA: 0 absorbed, 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(9 rows) + +-- 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+# + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 76 total, 0 merged + NFA Contexts: 3 peak, 51 total, 25 pruned + NFA: 25 matched (len 1/1/1.0), 0 mismatched + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(8 rows) + +-- 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) + 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) +(9 rows) + +-- Complex pattern: quantified sequence with a star element +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+ + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 235 total, 0 merged + NFA Contexts: 3 peak, 101 total, 34 pruned + NFA: 33 matched (len 3/3/3.0), 0 mismatched + NFA: 0 absorbed, 33 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(9 rows) + +-- 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~)+# + 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) +(9 rows) + +-- State explosion pattern - many alternations +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} + 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) +(9 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) + 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) +(9 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 + 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) +(9 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,} + 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) +(9 rows) + +-- Folding a copy that sits between two GROUPs leaves those GROUPs +-- adjacent, which nothing had put side by side before, so the GROUP +-- merge runs once more to collect them. The copy reaches the fold as +-- a suffix of the GROUP before it. +-- (A B)+ A B (A B)+ A B -> (A B){4,} +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)+ 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~){4,}# + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 58 total, 0 merged + NFA Contexts: 3 peak, 41 total, 3 pruned + NFA: 1 matched (len 40/40/40.0), 0 mismatched + NFA: 19 absorbed (len 2/2/2.0), 17 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(9 rows) + +-- A leading copy reaches the fold as a prefix instead +-- A B (A B)+ A B (A B)+ -> (A B){4,} +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 (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~){4,}# + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 58 total, 0 merged + NFA Contexts: 3 peak, 41 total, 3 pruned + NFA: 1 matched (len 40/40/40.0), 0 mismatched + NFA: 19 absorbed (len 2/2/2.0), 17 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(9 rows) + +-- The same with bounded quantifiers +-- (A B){2} A B (A B){2} -> (A B){5} +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} A B (A B){2}) + 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){5} + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 41 total, 0 merged + NFA Contexts: 6 peak, 41 total, 16 pruned + NFA: 4 matched (len 10/10/10.0), 0 mismatched + NFA: 0 absorbed, 20 skipped (len 1/8/4.2) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(9 rows) + +-- Three GROUPs and two copies between them +-- (A B)+ A B (A B)+ A B (A B)+ -> (A B){5,} +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)+ A B (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~){5,}# + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 57 total, 0 merged + NFA Contexts: 3 peak, 41 total, 4 pruned + NFA: 1 matched (len 40/40/40.0), 0 mismatched + NFA: 19 absorbed (len 2/2/2.0), 16 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(9 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 + 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) +(9 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)+ + 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) +(8 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)+ + 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) +(9 rows) + +-- (A{2,})* must NOT flatten to a*: 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,}#)* + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 18 total, 0 merged + NFA Contexts: 3 peak, 7 total, 0 pruned + NFA: 4 matched (len 0/2/1.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) +(9 rows) + +-- Overlapping DEFINEs let two branches reach the same state, so this is the +-- only case here with a nonzero merged count; the alt_merge names above mean +-- the optimizer's merge of consecutive ALTs, not this counter. +CREATE VIEW rpr_ev_state_dedup 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 % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_dedup'), 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 % 3 = 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=40.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a | b){2,4} + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 201 total, 14 merged + NFA Contexts: 3 peak, 41 total, 14 pruned + NFA: 6 matched (len 3/3/3.0), 8 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 12 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(9 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 91 total, 0 merged + NFA Contexts: 2 peak, 51 total, 0 pruned + NFA: 10 matched (len 5/5/5.0), 0 mismatched + NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +-- 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+# + 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) +(9 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*# + 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) +(9 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 + Storage: Memory Maximum Storage: NkB + NFA States: 7 peak, 101 total, 0 merged + NFA Contexts: 5 peak, 51 total, 0 pruned + NFA: 10 matched (len 5/5/5.0), 0 mismatched + NFA: 0 absorbed, 40 skipped (len 1/4/2.5) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 101 total, 0 merged + NFA Contexts: 3 peak, 101 total, 80 pruned + NFA: 10 matched (len 3/3/3.0), 0 mismatched + NFA: 0 absorbed, 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(9 rows) + +-- Unbounded group followed by a trailing element +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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 134 total, 0 merged + NFA Contexts: 3 peak, 101 total, 34 pruned + NFA: 33 matched (len 3/3/3.0), 0 mismatched + NFA: 0 absorbed, 33 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(9 rows) + +-- 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 + 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) +(9 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 + 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) +(9 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 + 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) +(9 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}~)+# + 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) +(9 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 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) +(10 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 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) +(10 rows) + +-- Variable-length group body absorption: (A+ B)+ C +-- Body is not fixed-length, so the leading A+ is the comparison point +-- (Case 3); the group's own END gets no flag. +CREATE VIEW rpr_ev_ctx_absorb_group_greedy 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 NOT IN (0, 9), B AS v % 10 = 9, C AS v % 10 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_group_greedy'), 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 NOT IN (0, 9), B AS v % 10 = 9, C 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+# b)+ c + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 96 total, 0 merged + NFA Contexts: 3 peak, 51 total, 5 pruned + NFA: 5 matched (len 10/10/10.0), 0 mismatched + NFA: 35 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +-- No absorption when the enclosing group is reluctant: (A+ B)+? C +-- Same body, DEFINE and rows as above; only the group quantifier differs. +-- Compare: absorbed count 0 and no marker on a+, vs >0 and a+# above. +CREATE VIEW rpr_ev_ctx_no_absorb_reluctant_group 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 NOT IN (0, 9), B AS v % 10 = 9, C AS v % 10 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_no_absorb_reluctant_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, 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 NOT IN (0, 9), B AS v % 10 = 9, C 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+ b)+? c + Storage: Memory Maximum Storage: NkB + NFA States: 17 peak, 271 total, 0 merged + NFA Contexts: 10 peak, 51 total, 5 pruned + NFA: 5 matched (len 10/10/10.0), 0 mismatched + NFA: 0 absorbed, 40 skipped (len 1/9/5.4) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +-- A fixed count leaves reluctance nothing to decide, so (A+ B){2}? is +-- normalized to (A+ B){2} and absorbs where the unbounded (A+ B)+? above +-- does not. The two spellings below have to report the same marker and the +-- same absorbed count as each other; that is what the normalization buys. +CREATE VIEW rpr_ev_ctx_absorb_fixed_greedy 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){2}) + 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_fixed_greedy'), 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, 50) 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 % 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){2} + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 111 total, 0 merged + NFA Contexts: 3 peak, 51 total, 5 pruned + NFA: 5 matched (len 10/10/10.0), 0 mismatched + NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/5/3.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +CREATE VIEW rpr_ev_ctx_absorb_fixed_reluctant 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){2}?) + 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_fixed_reluctant'), 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, 50) 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 % 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){2} + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 111 total, 0 merged + NFA Contexts: 3 peak, 51 total, 5 pruned + NFA: 5 matched (len 10/10/10.0), 0 mismatched + NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/5/3.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 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) + 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) +(12 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+#) + 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) +(12 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 + 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) +(9 rows) + +-- Unbounded quantifier over a repeating cycle +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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 191 total, 0 merged + NFA Contexts: 2 peak, 101 total, 0 pruned + NFA: 10 matched (len 10/10/10.0), 0 mismatched + NFA: 80 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 396 total, 0 merged + NFA Contexts: 2 peak, 201 total, 4 pruned + NFA: 1 matched (len 196/196/196.0), 0 mismatched + NFA: 194 absorbed (len 1/1/1.0), 1 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=200.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 171 total, 0 merged + NFA Contexts: 3 peak, 101 total, 25 pruned + NFA: 5 matched (len 5/5/5.0), 5 mismatched (len 11/11/11.0) + NFA: 60 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(9 rows) + +-- ============================================================ +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 151 total, 0 merged + NFA Contexts: 3 peak, 101 total, 60 pruned + NFA: 10 matched (len 6/6/6.0), 0 mismatched + NFA: 20 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 115 total, 0 merged + NFA Contexts: 3 peak, 61 total, 15 pruned + NFA: 1 matched (len 30/30/30.0), 1 mismatched (len 26/26/26.0) + NFA: 42 absorbed (len 1/1/1.0), 1 skipped (len 1/1/1.0) + -> Function Scan on generate_series i (actual rows=60.00 loops=1) +(9 rows) + +-- ============================================================ +-- 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+", + + "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", + + "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", + + "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}", + + "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 + + 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) + +-- Absorbed contexts and a navigation, neither of which the case above emits, +-- so without this the XML spelling of the absorbed length group and of Nav +-- Mark goes unchecked. +CREATE VIEW rpr_ev_xml_absorb 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 AND PREV(v) IS NOT NULL, B AS v % 10 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_xml_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, FORMAT XML) +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 AND PREV(v) IS NOT NULL, B AS v % 10 = 0 +)'); + rpr_explain_filter +-------------------------------------------------------------------------------- + + + + + + + WindowAgg + + false + + false + + 100.00 + + 1 + + false + + w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)+ + a+# b+ + + 1 + + Memory + + 0 + + 4 + + 200 + + 0 + + 3 + + 101 + + 79 + + 10 + + 1 + + 10 + + 0 + + 9 + + 10 + + 9.9 + + 1 + + 1 + + 1.0 + + 1 + + 1 + + 1.0 + + + + + + Function Scan + + Outer + + false + + false + + generate_series + + s + + 100.00 + + 1 + + false + + + + + + + + + + + + + + +(1 row) + +-- A pattern that mismatches at all, which the two cases above never do, so +-- this is where the mismatch length group appears in XML. +CREATE VIEW rpr_ev_xml_mismatch 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 % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_xml_mismatch'), 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, FORMAT XML) +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 % 3 = 0 +)'); + rpr_explain_filter +-------------------------------------------------------------------------------- + + + + + + + WindowAgg + + false + + false + + 40.00 + + 1 + + false + + w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)+ + (a | b){2,4} + + Memory + + 0 + + 6 + + 201 + + 14 + + 3 + + 41 + + 0 + + 12 + + 14 + + 6 + + 8 + + 3 + + 3 + + 3.0 + + 2 + + 2 + + 2.0 + + 1 + + 1 + + 1.0 + + + + + + Function Scan + + Outer + + false + + false + + generate_series + + s + + 40.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 + 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) +(14 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 + 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) +(12 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 + -> Function Scan on generate_series s (actual rows=0.00 loops=1) +(4 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} + 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) +(8 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} + 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) +(8 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} + 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) +(8 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} + Storage: Memory Maximum Storage: NkB + NFA States: 6 peak, 13 total, 0 merged + NFA Contexts: 4 peak, 5 total, 0 pruned + NFA: 4 matched (len 0/2/0.8), 0 mismatched + -> Function Scan on generate_series s (actual rows=4.00 loops=1) +(8 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} + Storage: Memory Maximum Storage: NkB + NFA States: 7 peak, 24 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) +(8 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 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 2 total, 0 merged + NFA Contexts: 2 peak, 2 total, 0 pruned + NFA: 1 matched (len 1/1/1.0), 0 mismatched + -> Function Scan on generate_series s (actual rows=1.00 loops=1) +(8 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 6 total, 0 merged + NFA Contexts: 3 peak, 6 total, 4 pruned + NFA: 0 matched, 1 mismatched (len 5/5/5.0) + -> Function Scan on generate_series s (actual rows=5.00 loops=1) +(8 rows) + +-- 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+# + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 101 total, 0 merged + NFA Contexts: 2 peak, 51 total, 0 pruned + NFA: 1 matched (len 50/50/50.0), 0 mismatched + NFA: 49 absorbed (len 1/1/1.0), 0 skipped + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +-- ============================================================ +-- 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~)+# + 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) +(9 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) + 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) +(9 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 64 total, 0 merged + NFA Contexts: 3 peak, 51 total, 25 pruned + NFA: 12 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0) + NFA: 0 absorbed, 12 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 9 peak, 311 total, 0 merged + NFA Contexts: 7 peak, 101 total, 0 pruned + NFA: 10 matched (len 6/6/6.0), 40 mismatched (len 6/6/6.0) + NFA: 0 absorbed, 50 skipped (len 1/5/3.0) + -> Function Scan on generate_series s (actual rows=100.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 91 total, 0 merged + NFA Contexts: 3 peak, 51 total, 40 pruned + NFA: 5 matched (len 9/9/9.0), 0 mismatched + NFA: 0 absorbed, 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +-- ============================================================ +-- 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+ + 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) +(9 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+ + 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) +(9 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 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 1001 total, 0 merged + NFA Contexts: 2 peak, 1001 total, 0 pruned + NFA: 500 matched (len 2/2/2.0), 0 mismatched + NFA: 0 absorbed, 500 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=1000.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 1991 total, 0 merged + NFA Contexts: 2 peak, 1001 total, 0 pruned + NFA: 10 matched (len 100/100/100.0), 0 mismatched + NFA: 980 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=1000.00 loops=1) +(9 rows) + +-- Alternation with plus quantifier over 500 rows +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 + 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) +(9 rows) + +-- ============================================================ +-- INITIAL vs no INITIAL comparison +-- ============================================================ +-- INITIAL is the only supported mode (SEEK is unimplemented), so omitting the +-- keyword changes nothing at run time and deparse adds it back. Print the +-- whole viewdef: filtering to the PATTERN line would hide the INITIAL literal, +-- which is the only thing this test can pin. +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 pg_get_viewdef('rpr_ev_initial_without'::regclass); + pg_get_viewdef +----------------------------------------------------------------- + SELECT count(*) OVER w AS count + + FROM generate_series(1, 50) 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)); +(1 row) + +-- ============================================================ +-- 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+# + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 71 total, 0 merged + NFA Contexts: 3 peak, 41 total, 10 pruned + NFA: 10 matched (len 3/3/3.0), 0 mismatched + NFA: 20 absorbed (len 1/1/1.0), 0 skipped + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(9 rows) + +-- 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 + 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) +(9 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 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 82 total, 0 merged + NFA Contexts: 3 peak, 41 total, 10 pruned + NFA: 10 matched (len 3/3/3.0), 0 mismatched + NFA: 0 absorbed, 20 skipped (len 1/2/1.5) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 51 total, 0 merged + NFA Contexts: 5 peak, 51 total, 0 pruned + NFA: 10 matched (len 4/4/4.0), 10 mismatched (len 4/4/4.0) + NFA: 0 absorbed, 30 skipped (len 1/3/2.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 7 peak, 101 total, 0 merged + NFA Contexts: 5 peak, 51 total, 0 pruned + NFA: 10 matched (len 5/5/5.0), 0 mismatched + NFA: 0 absorbed, 40 skipped (len 1/4/2.5) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 86 total, 0 merged + NFA Contexts: 2 peak, 51 total, 0 pruned + NFA: 5 matched (len 10/10/10.0), 0 mismatched + NFA: 40 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +-- ============================================================ +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 37 total, 0 merged + NFA Contexts: 2 peak, 21 total, 0 pruned + NFA: 4 matched (len 5/5/5.0), 0 mismatched + NFA: 12 absorbed (len 1/1/1.0), 4 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=20.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 52 total, 0 merged + NFA Contexts: 3 peak, 31 total, 6 pruned + NFA: 3 matched (len 9/9/9.0), 0 mismatched + NFA: 18 absorbed (len 1/1/1.0), 3 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 31 total, 0 merged + NFA Contexts: 3 peak, 31 total, 10 pruned + NFA: 10 matched (len 3/3/3.0), 0 mismatched + NFA: 0 absorbed, 10 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(9 rows) + +-- ============================================================ +-- 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 + 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) +(9 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 + 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) +(9 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 + 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) +(9 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) + 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) +(8 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 + 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) +(9 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 + 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) +(8 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 + 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) +(9 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) + 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) +(9 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) + 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) +(8 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) + 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) +(8 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) + 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) +(9 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~)+#) + 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) +(9 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) + 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) +(9 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) + 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) +(9 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) + 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) +(8 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~)+#) + 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) +(9 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 + 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) +(9 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 + 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) +(9 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)+ + 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) +(8 rows) + +-- Unit (1,1) group as an alternation branch (emits no BEGIN/END) +-- Pattern: ((A B) | C) - control: a {1,1} group emits no BEGIN/END, so the +-- branch deparses as a plain sequence +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) + 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) +(9 rows) + +-- Quantified variable as the first alternation branch +-- Pattern: (A+ | C) - control: a quantified variable as the leading branch; +-- the ALT supplies the enclosing parentheses +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) + 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) +(8 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~)+#) + 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) +(9 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) + 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) +(8 rows) + +-- Group mid-branch followed by a sequence element needs no separator before it +-- Pattern: (C | (A B)+ D) - only the branch end takes a SEP, not 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) + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 67 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) +(8 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)+ + 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) +(9 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))+ + 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) +(9 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)+ + 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) +(9 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) + 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) +(9 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) + 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) +(9 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) + 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) +(9 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) + 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) +(9 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) + 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) +(9 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) + 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) +(9 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) + 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) +(9 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~)+# + 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) +(9 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} + Storage: Memory Maximum Storage: NkB + NFA States: 4 peak, 51 total, 0 merged + NFA Contexts: 3 peak, 41 total, 5 pruned + NFA: 5 matched (len 8/8/8.0), 0 mismatched + NFA: 0 absorbed, 30 skipped (len 1/2/1.5) + -> Function Scan on generate_series s (actual rows=40.00 loops=1) +(9 rows) + +-- 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}~)+# + 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) +(9 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)+ + 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) +(9 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 + 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) +(9 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)* + Storage: Memory Maximum Storage: NkB + NFA States: 9 peak, 178 total, 0 merged + NFA Contexts: 4 peak, 61 total, 0 pruned + NFA: 4 matched (len 0/57/14.2), 0 mismatched + NFA: 0 absorbed, 56 skipped (len 1/3/1.6) + -> Function Scan on generate_series s (actual rows=60.00 loops=1) +(9 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)+)* + Storage: Memory Maximum Storage: NkB + NFA States: 8 peak, 160 total, 0 merged + NFA Contexts: 4 peak, 61 total, 0 pruned + NFA: 4 matched (len 0/57/14.2), 0 mismatched + NFA: 0 absorbed, 56 skipped (len 1/3/1.6) + -> Function Scan on generate_series s (actual rows=60.00 loops=1) +(9 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 55 total, 0 merged + NFA Contexts: 2 peak, 31 total, 0 pruned + NFA: 6 matched (len 5/5/5.0), 0 mismatched + NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 55 total, 0 merged + NFA Contexts: 2 peak, 31 total, 0 pruned + NFA: 6 matched (len 5/5/5.0), 0 mismatched + NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 55 total, 0 merged + NFA Contexts: 2 peak, 31 total, 0 pruned + NFA: 6 matched (len 5/5/5.0), 0 mismatched + NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 55 total, 0 merged + NFA Contexts: 2 peak, 31 total, 0 pruned + NFA: 6 matched (len 5/5/5.0), 0 mismatched + NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=30.00 loops=1) +(9 rows) + +-- ============================================================ +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 78 total, 0 merged + NFA Contexts: 2 peak, 51 total, 6 pruned + NFA: 17 matched (len 2/3/2.6), 0 mismatched + NFA: 10 absorbed (len 1/1/1.0), 17 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=50.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 55 total, 0 merged + NFA Contexts: 2 peak, 31 total, 0 pruned + NFA: 6 matched (len 5/5/5.0), 0 mismatched + NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0) + -> Function Scan on generate_series v (actual rows=30.00 loops=1) +(9 rows) + +-- ============================================================ +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 851 total, 0 merged + NFA Contexts: 3 peak, 501 total, 101 pruned + NFA: 50 matched (len 8/9/9.0), 0 mismatched + NFA: 299 absorbed (len 1/1/1.0), 50 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=500.00 loops=1) +(9 rows) + +-- 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 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 501 total, 0 merged + NFA Contexts: 2 peak, 501 total, 0 pruned + NFA: 250 matched (len 2/2/2.0), 0 mismatched + NFA: 0 absorbed, 250 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=500.00 loops=1) +(9 rows) + +-- Sparse five-element pattern over 500 rows +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 + Storage: Memory Maximum Storage: NkB + NFA States: 2 peak, 501 total, 0 merged + NFA Contexts: 3 peak, 501 total, 490 pruned + NFA: 5 matched (len 5/5/5.0), 0 mismatched + NFA: 0 absorbed, 5 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=500.00 loops=1) +(9 rows) + +-- ============================================================ +-- 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 +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+# + -> Function Scan on generate_series s +(4 rows) + +-- NEXT only: no backward navigation +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+# + -> Function Scan on generate_series s +(4 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) + +-- PREV(v, 1 + 1): a foldable offset has to arrive as a constant, or the trim +-- bound would print "runtime" instead of 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 v > PREV(v, 1 + 1) +); + 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) + +-- 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; +-- EXPLAIN (GENERIC_PLAN) of an unbound parameter offset must not evaluate the +-- parameter: the offset stays "runtime" instead of failing with "no value +-- found for parameter 1". +EXPLAIN (GENERIC_PLAN, 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, $1) +); + 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) + +-- FIRST(v) with no offset (references the 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 Lookahead: 0 + -> Function Scan on generate_series s +(5 rows) + +-- FIRST(v, 5): forward reach 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 v > FIRST(v, 5) +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookahead: 5 + -> Function Scan on generate_series s +(5 rows) + +-- The same forward reach in the XML and JSON forms. Only the text form is +-- exercised elsewhere, so the tag itself has no coverage. +EXPLAIN (COSTS OFF, FORMAT XML) 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, 5) +); + QUERY PLAN +-------------------------------------------------------------------------------- + + + + + + + WindowAgg + + false + + false + + false + + w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)+ + a+ + + 5 + + + + + + Function Scan + + Outer + + false + + false + + generate_series + + s + + false + + + + + + + + + + +(1 row) + +EXPLAIN (COSTS OFF, FORMAT JSON) 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, 5) +); + QUERY PLAN +---------------------------------------------------------------------------- + [ + + { + + "Plan": { + + "Node Type": "WindowAgg", + + "Parallel Aware": false, + + "Async Capable": false, + + "Disabled": false, + + "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+ + "Pattern": "a+", + + "Nav Mark Lookahead": 5, + + "Plans": [ + + { + + "Node Type": "Function Scan", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Function Name": "generate_series", + + "Alias": "s", + + "Disabled": false + + } + + ] + + } + + } + + ] +(1 row) + +-- 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 Lookahead: -1 + -> Function Scan on generate_series s +(5 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 Lookahead: 3 + -> Function Scan on generate_series s +(5 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 forms with the outer offset left out, which defaults to 1: each of +-- the four arms combines it with the inner offset differently, and the value +-- decides both the trim bound and the row the navigation lands on. +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, 2)) > 0 +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookahead: 1 + -> Function Scan on generate_series s +(5 rows) + +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, 2)) > 0 +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookahead: 3 + -> Function Scan on generate_series s +(5 rows) + +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: 3 + -> Function Scan on generate_series s +(5 rows) + +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, 2)) > 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) + +-- 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 Lookahead: infinite + -> Function Scan on generate_series s +(5 rows) + +-- A navigation with a negative offset cannot run, so it contributes no reach +-- and its dimension reports nothing at 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(FIRST(v, -3), 2) IS NOT NULL +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + -> Function Scan on generate_series s +(4 rows) + +-- The same query errors once it runs, since execution validates the offset. +EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY 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, -3), 2) IS NOT NULL +); +ERROR: row pattern navigation offset must not be negative +-- Same at the int64 limit, where the reach subtraction would otherwise wrap. +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, (-9223372036854775807)::int8), 2) IS NOT NULL +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + -> Function Scan on generate_series s +(4 rows) + +-- The other dimension keeps its own aggregate. +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, 5) IS NOT NULL AND FIRST(v, -1) IS NOT NULL +); + 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) + +-- A null offset cannot run either, so it too contributes no reach. Resolving +-- it to a placeholder 0 would instead let it join the aggregate and displace +-- the offset of the navigation that can run. +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 FIRST(v, 7) IS NOT NULL AND PREV(FIRST(v, NULL::int), 2) IS NOT NULL +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookahead: 7 + -> Function Scan on generate_series s +(5 rows) + +-- And it errors once it runs, for the same reason the negative one does. +EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY 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 FIRST(v, 7) IS NOT NULL AND PREV(FIRST(v, NULL::int), 2) IS NOT NULL +); +ERROR: row pattern navigation offset must not be null +-- Dropping a navigation must not disturb the kind the survivor reports. An +-- overflowing lookback still retains every 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 FIRST(v, -1) IS NOT NULL + AND 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) + +-- And a parameter offset beside a dropped one is still settled per scan. +PREPARE test_dropped_with_runtime(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 AND PREV(v, $1) IS NOT NULL +); +SET plan_cache_mode = force_generic_plan; +EXPLAIN (COSTS OFF) EXECUTE test_dropped_with_runtime(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 test_dropped_with_runtime; +-- NEXT(LAST()) reaches the same subtraction on the lookback side. +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, 2), (-9223372036854775807)::int8) IS NOT NULL +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + -> Function Scan on generate_series s +(4 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) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) + EXECUTE test_overflow_lookback(4611686018427387904, 4611686018427387904)'); + 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: retain all + Storage: Memory Maximum Storage: NkB + 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; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) + EXECUTE test_overflow_lookahead(4611686018427387904, 4611686018427387904)'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=10.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookahead: infinite + Storage: Memory Maximum Storage: NkB + 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_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 Lookahead: runtime + -> Function Scan on generate_series s +(5 rows) + +RESET plan_cache_mode; +DEALLOCATE p_first_runtime; +-- PREV(v) + PREV(v, $1): the implicit lookback of 1 has to count even when the +-- explicit offset resolves to 0, or PREV(v) would fail with "cannot fetch row +-- before mark". A generic plan settles the reach per scan instead of at init. +SET plan_cache_mode = force_generic_plan; +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; +RESET plan_cache_mode; +-- NEEDS_EVAL executor offset paths: a Param nav offset stays non-Const under a +-- generic plan, so build_define_offsets() marks the offset NEEDS_EVAL and +-- resolve_nav_offsets() settles it once per scan. 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) + +-- Observe the arm rather than only run it. At plan time the line reads +-- "runtime"; once resolved it reads inner - outer, so 2 here is the PREV_FIRST +-- subtraction. A bare FIRST would report the inner offset alone. +EXPLAIN (COSTS OFF) EXECUTE test_eval_prevfirst(3, 1); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookahead: runtime + -> Function Scan on generate_series s +(5 rows) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +EXECUTE test_eval_prevfirst(3, 1);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=10.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookahead: 2 + Storage: Memory Maximum Storage: NkB + 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_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; +-- The same at generic-plan resolution time, and for each half of a compound +-- navigation on its own. +PREPARE test_runtime_neg_compound_offset(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; +EXECUTE test_runtime_neg_compound_offset(1, -1); +ERROR: row pattern navigation offset must not be negative +EXECUTE test_runtime_neg_compound_offset(-1, 1); +ERROR: row pattern navigation offset must not be negative +RESET plan_cache_mode; +DEALLOCATE test_runtime_neg_compound_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; +-- A correlated PARAM_EXEC nav offset (reaching the offset via SRF inlining) is +-- resolved per scan by resolve_nav_offsets(); after execution EXPLAIN ANALYZE +-- must display the concrete resolved bound (a number), not "runtime" -- that is, +-- navMaxOffsetKind resolves to FIXED. Plain EXPLAIN of the same query shows +-- "runtime"; only ANALYZE exercises the per-scan clear. +CREATE TABLE rpr_exp_srf (v int); +INSERT INTO rpr_exp_srf SELECT generate_series(1, 10); +CREATE FUNCTION rpr_exp_srf_f(k int) RETURNS SETOF bigint AS $$ + SELECT count(*) OVER w + FROM rpr_exp_srf + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS v > PREV(v, k)) +$$ LANGUAGE sql STABLE; +SELECT t FROM rpr_explain_filter( + 'EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) + SELECT g.n, max(s) FROM (VALUES (2), (2)) g(n), LATERAL rpr_exp_srf_f(g.n) s + GROUP BY g.n') AS t +WHERE t LIKE '%Nav Mark Lookback%'; + t +------------------------------------ + Nav Mark Lookback: 2 +(1 row) + +DROP FUNCTION rpr_exp_srf_f(int); +-- The kind a scan settles on is per scan, not sticky. An outer offset that +-- overflows int64 gives up on the trim for that scan alone, and EXPLAIN +-- ANALYZE reports what the last rescan left behind: the same three offsets +-- in a different order have to read differently, retain all when the +-- overflow runs last and a bound again when a smaller offset follows it. +CREATE FUNCTION rpr_exp_srf_cmp(k int8) RETURNS SETOF bigint AS $$ + SELECT count(*) OVER w + FROM rpr_exp_srf + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) DEFINE B AS v > PREV(LAST(v, 1), k)) +$$ LANGUAGE sql STABLE; +SELECT t FROM rpr_explain_filter( + 'EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) + SELECT g.n, max(s) + FROM (VALUES (1::int8), (3::int8), (9223372036854775807::int8)) g(n), + LATERAL rpr_exp_srf_cmp(g.n) s + GROUP BY g.n') AS t +WHERE t LIKE '%Nav Mark Lookback%'; + t +--------------------------------------------- + Nav Mark Lookback: retain all +(1 row) + +SELECT t FROM rpr_explain_filter( + 'EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) + SELECT g.n, max(s) + FROM (VALUES (1::int8), (9223372036854775807::int8), (3::int8)) g(n), + LATERAL rpr_exp_srf_cmp(g.n) s + GROUP BY g.n') AS t +WHERE t LIKE '%Nav Mark Lookback%'; + t +------------------------------------ + Nav Mark Lookback: 4 +(1 row) + +DROP FUNCTION rpr_exp_srf_cmp(int8); +DROP TABLE rpr_exp_srf; diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out new file mode 100644 index 00000000000..2d5edebb128 --- /dev/null +++ b/src/test/regress/expected/rpr_integration.out @@ -0,0 +1,1917 @@ +-- ============================================================ +-- 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. +-- +-- The file drops what it creates, with one exception: A3 leaves the view +-- rpr_ev_opt_mixed behind, so that pg_upgrade and pg_dump exercise an RPR +-- window serialized together with a non-RPR one. +-- +-- 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 in WHERE +-- B11. RPR + Junk targetlist pruning +-- B12. RPR + Correlated navigation offsets +-- B13. RPR + DEFINE-only parameter caching +-- B14. RPR + Multiple window definitions +-- +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. +-- 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. +-- 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. +-- 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. +-- 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 parser 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) + +-- Result level: if the two windows had been merged, fv_normal and fv_rpr +-- would agree on every row. They do not, so the windows stayed 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) + +-- The two windows above start from the same frame. These two do not: +-- they converge only after frame optimization rewrites the non-RPR one, +-- and the RPR window is preserved, so they still must not be merged. +-- The view is deliberately left undropped: it is the only one in the +-- tree that serializes an RPR window and a non-RPR window together, so +-- pg_upgrade/pg_dump needs it to exercise that round trip. +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+# + -> WindowAgg + Window: w_normal AS (ORDER BY s.v ROWS UNBOUNDED PRECEDING) + -> Sort + Sort Key: s.v + -> Function Scan on generate_series s +(9 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+# + -> Seq Scan on rpr_integ +(5 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+ + -> Sort + Output: rpr_integ.id, rpr_integ.* + Sort Key: rpr_integ.id + -> Seq Scan on public.rpr_integ + Output: rpr_integ.id, rpr_integ.* +(11 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() is inverse-transition eligible in a plain window; over an RPR reduced +-- frame rpr_is_defined() forces peraggstate->restart, so the aggregate is +-- recomputed from scratch. The logging aggregate below asserts the bypass +-- directly. +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) + +-- A lateral outer reference can share varno and varattno with a DEFINE-only +-- column: here o.b and y are both attribute 2 at their own query levels. +-- Only varlevelsup separates them, so the junk targetlist entry for y has to +-- be added even though a Var with the same varno and varattno is present. +CREATE TABLE rpr_lat_o (a int, b int); +CREATE TABLE rpr_lat_i (x int, y int); +INSERT INTO rpr_lat_o VALUES (1, 10); +INSERT INTO rpr_lat_i VALUES (1, 5), (2, 6); +-- The overlapping shape: the outer reference is o.b, attribute 2. +SELECT * +FROM rpr_lat_o o, +LATERAL ( + SELECT o.b AS lat, count(*) OVER w AS c + FROM rpr_lat_i + WINDOW w AS (ORDER BY x + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS y > 0) +) s; + a | b | lat | c +---+----+-----+--- + 1 | 10 | 10 | 2 + 1 | 10 | 10 | 0 +(2 rows) + +-- Control: the outer reference is o.a, attribute 1, which cannot be mistaken +-- for y. The counts must match the query above. +SELECT * +FROM rpr_lat_o o, +LATERAL ( + SELECT o.a AS lat, count(*) OVER w AS c + FROM rpr_lat_i + WINDOW w AS (ORDER BY x + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS y > 0) +) s; + a | b | lat | c +---+----+-----+--- + 1 | 10 | 1 | 2 + 1 | 10 | 1 | 0 +(2 rows) + +DROP TABLE rpr_lat_o, rpr_lat_i; +-- ============================================================ +-- B7. RPR + Recursive CTE +-- ============================================================ +-- RPR is rejected in every leg of a recursive query, including the +-- non-recursive leg (see parse_cte.c for the standard citation). +-- 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: DEFINE clause cannot contain volatile functions +-- ============================================================ +-- 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) + +-- ============================================================ +-- B11. RPR + Junk targetlist pruning +-- ============================================================ +-- Verify that the junk targetlist entry planted for a DEFINE-only +-- column does not keep an unrelated column alive. DEFINE references +-- a (rpr_over1); c (rpr_over2) carries the same attribute number but +-- is unused, so the plan must drop it. +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); +-- Plan: only the DEFINE column survives in the subquery output. +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+# + -> 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 +(15 rows) + +DROP TABLE rpr_over1, rpr_over2; +-- ============================================================ +-- B12. RPR + Correlated navigation offsets +-- ============================================================ +-- A row pattern navigation offset that resolves to a correlated PARAM_EXEC +-- (here through SRF inlining of rpr_srf_prev(g.n)) must be re-resolved on every +-- rescan, not frozen at executor init. The inlined WindowAgg is the inner +-- side of a nestloop and is rescanned once per outer row, so each row sees its +-- own PREV(v, n) offset; a frozen offset would report the same value for all. +CREATE TABLE rpr_srf (v int); +INSERT INTO rpr_srf SELECT generate_series(1, 10); +CREATE FUNCTION rpr_srf_prev(k int) RETURNS SETOF bigint AS $$ + SELECT count(*) OVER w + FROM rpr_srf + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS v > PREV(v, k)) +$$ LANGUAGE sql STABLE; +-- Plan: the offset reads "runtime"; the WindowAgg inlines into the +-- nestloop and is rescanned per outer row. +EXPLAIN (COSTS OFF) +SELECT g.n, max(s) FROM (VALUES (1), (2), (3)) g(n), LATERAL rpr_srf_prev(g.n) s +GROUP BY g.n ORDER BY g.n; + QUERY PLAN +-------------------------------------------------------------------------------------------------------- + Sort + Sort Key: "*VALUES*".column1 + -> HashAggregate + Group Key: "*VALUES*".column1 + -> Nested Loop + -> Values Scan on "*VALUES*" + -> WindowAgg + Window: w AS (ORDER BY rpr_srf.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+# + Nav Mark Lookback: runtime + -> Sort + Sort Key: rpr_srf.v + -> Seq Scan on rpr_srf +(13 rows) + +-- Each outer row yields its own offset (9, 8, 7), not one frozen value. +SELECT g.n, max(s) AS m FROM (VALUES (1), (2), (3)) g(n), LATERAL rpr_srf_prev(g.n) s +GROUP BY g.n ORDER BY g.n; + n | m +---+--- + 1 | 9 + 2 | 8 + 3 | 7 +(3 rows) + +-- A forward FIRST-family offset must likewise be re-resolved per scan +-- (navFirstOffset / navFirstOffsetKind). PATTERN (B A+) anchors the +-- match start at B so A can reference FIRST(v, k) k rows ahead, and +-- each outer k yields its own forward offset. +CREATE FUNCTION rpr_srf_first(k int) RETURNS SETOF bigint AS $$ + SELECT count(*) OVER w + FROM rpr_srf + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (B A+) DEFINE A AS v > FIRST(v, k)) +$$ LANGUAGE sql STABLE; +-- Plan: the forward offset reads "runtime" and the WindowAgg inlines +-- into the nestloop. +EXPLAIN (COSTS OFF) +SELECT g.n, max(s) FROM (VALUES (0), (1), (2)) g(n), LATERAL rpr_srf_first(g.n) s +GROUP BY g.n ORDER BY g.n; + QUERY PLAN +-------------------------------------------------------------------------------------------------------- + Sort + Sort Key: "*VALUES*".column1 + -> HashAggregate + Group Key: "*VALUES*".column1 + -> Nested Loop + -> Values Scan on "*VALUES*" + -> WindowAgg + Window: w AS (ORDER BY rpr_srf.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: b a+ + Nav Mark Lookahead: runtime + -> Sort + Sort Key: rpr_srf.v + -> Seq Scan on rpr_srf +(13 rows) + +-- Result: k=0 matches all ten rows and k>=1 makes the first A fail, +-- so the forward offset is not frozen at ExecInit. +SELECT g.n, max(s) AS m FROM (VALUES (0), (1), (2)) g(n), LATERAL rpr_srf_first(g.n) s +GROUP BY g.n ORDER BY g.n; + n | m +---+---- + 0 | 10 + 1 | 0 + 2 | 0 +(3 rows) + +DROP FUNCTION rpr_srf_first(int); +-- A compound navigation's OUTER offset must be re-resolved per scan +-- as well. The last offset overflows int64, so that scan's navigation +-- has no target row at all. +CREATE FUNCTION rpr_srf_cmp(k int8) RETURNS SETOF bigint AS $$ + SELECT count(*) OVER w + FROM rpr_srf + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) DEFINE B AS v > PREV(LAST(v, 1), k)) +$$ LANGUAGE sql STABLE; +-- Result: k=1 -> 9, k=3 -> 7, overflow -> 0. Three answers from one plan +-- is what says each rescan resolved its own outer offset. The trim kind a +-- scan settles on does not show in a count; rpr_explain reads it out of +-- EXPLAIN ANALYZE instead. +SELECT g.n, max(s) AS m +FROM (VALUES (1::int8), (3::int8), (9223372036854775807::int8)) g(n), + LATERAL rpr_srf_cmp(g.n) s +GROUP BY g.n ORDER BY g.n; + n | m +---------------------+--- + 1 | 9 + 3 | 7 + 9223372036854775807 | 0 +(3 rows) + +DROP FUNCTION rpr_srf_cmp(int8); +DROP FUNCTION rpr_srf_prev(int); +DROP TABLE rpr_srf; +-- ============================================================ +-- B13. RPR + DEFINE-only parameter caching +-- ============================================================ +-- A correlated PARAM_EXEC used only inside DEFINE must reach the WindowAgg's +-- extParam. Otherwise chgParam never gets to the HashAgg that DISTINCT plans +-- above it, and its hash table for the first outer row is re-served. +-- The SRF must be inlined as a lateral subquery for its argument to become a +-- PARAM_EXEC; a FunctionScan plan would pass either way. +CREATE TABLE rpr_hcache_thr (threshold int); +INSERT INTO rpr_hcache_thr VALUES (10), (200); +CREATE TABLE rpr_hcache_stock (price int); +INSERT INTO rpr_hcache_stock SELECT g FROM generate_series(1, 100) g; +CREATE FUNCTION rpr_hcache_fn(th int) RETURNS SETOF bigint LANGUAGE sql STABLE AS $$ + SELECT DISTINCT count(*) OVER w FROM rpr_hcache_stock + WINDOW w AS (ORDER BY price ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL PATTERN (a+) DEFINE a AS price > th) $$; +-- Result: each threshold gets its own answer set (10 -> {0, 90}, +-- 200 -> {0}); a stale cache would add a spurious 200|90. +SELECT o.threshold, f FROM rpr_hcache_thr o, LATERAL rpr_hcache_fn(o.threshold) f +ORDER BY 1, 2; + threshold | f +-----------+---- + 10 | 0 + 10 | 90 + 200 | 0 +(3 rows) + +DROP FUNCTION rpr_hcache_fn(int); +DROP TABLE rpr_hcache_thr, rpr_hcache_stock; +-- ============================================================ +-- B14. RPR + Multiple window definitions +-- ============================================================ +-- A DEFINE-only column and a later window's sort key both become junk +-- targetlist entries. Each draws its resno from p_next_resno, which is what +-- keeps the two distinct: a targetlist that gives one resno to two entries is +-- not a valid Query, and the parser is the only place that can prevent it. +SELECT id, count(*) OVER w1 AS c1, count(*) OVER w2 AS c2 +FROM (VALUES (1,1,10),(2,1,20)) t(id, grp, val) +WINDOW w1 AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (S U+) + DEFINE U AS val > PREV(val)), + w2 AS (PARTITION BY grp ORDER BY id) +ORDER BY id; + id | c1 | c2 +----+----+---- + 1 | 2 | 1 + 2 | 0 | 2 +(2 rows) + +-- The same pair reached through a plain ORDER BY window. +SELECT id, count(*) OVER w1 AS c1, count(*) OVER w2 AS c2 +FROM (VALUES (1,1,10),(2,1,20)) t(id, grp, val) +WINDOW w1 AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (S U+) + DEFINE U AS val > PREV(val)), + w2 AS (ORDER BY grp) +ORDER BY id; + id | c1 | c2 +----+----+---- + 1 | 2 | 2 + 2 | 0 | 2 +(2 rows) + +-- Control: the opposite declaration order draws the sort key first, so the two +-- never compete for a resno. It must return the same rows as the first query +-- above. +SELECT id, count(*) OVER w1 AS c1, count(*) OVER w2 AS c2 +FROM (VALUES (1,1,10),(2,1,20)) t(id, grp, val) +WINDOW w2 AS (PARTITION BY grp ORDER BY id), + w1 AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (S U+) + DEFINE U AS val > PREV(val)) +ORDER BY id; + id | c1 | c2 +----+----+---- + 1 | 2 | 1 + 2 | 0 | 2 +(2 rows) + +-- 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..b133a0c99f6 --- /dev/null +++ b/src/test/regress/expected/rpr_nfa.out @@ -0,0 +1,7009 @@ +-- ============================================================ +-- 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 execRPR.c, driven by +-- update_reduced_frame() in nodeWindowAgg.c. +-- +-- 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 +-- Standard Clause 7: Formal Pattern Matching Rules +-- +-- 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: the +-- inner quantifier decides whether a row is consumed, so every column whose +-- body is reluctant stays at zero, and the two with a greedy body differ by +-- their outer quantifier -- gg takes the longest match, rg one row. +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 | 0 | 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 +-- (Standalone reluctant-plus case; compare with A{1,3}? and A{3,5}? 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. +-- (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) + +-- Optional VAR at the head of the body, re-entered across iterations: +-- (B? A+){2} with no B rows. Both iterations skip B and re-enter A. +WITH test_optvar_quant_reentry AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, 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_optvar_quant_reentry +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((B? A+){2}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {A} | | +(2 rows) + +-- A skip path that lands on the group's END still counts that iteration: +-- (UP DOWN?)+ over a rising run, where DOWN never matches. +WITH test_skip_lands_on_end AS ( + SELECT * FROM (VALUES + (1, 100), + (2, 110), + (3, 120) + ) 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_skip_lands_on_end +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((UP DOWN?)+) + DEFINE + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + id | price | match_start | match_end +----+-------+-------------+----------- + 1 | 100 | | + 2 | 110 | 2 | 3 + 3 | 120 | | +(3 rows) + +-- Deep giveback: A+ swallows every overlapping row, then B{3} fails and +-- A+ must surrender three rows in a row. The failure is discovered five +-- rows away from the branch point, not adjacent to it. +WITH test_quant_deep_giveback AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['A','B']), + (3, ARRAY['A','B']), + (4, ARRAY['A','B']), + (5, ARRAY['A','B']), + (6, ARRAY['A','B']), + (7, 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_quant_deep_giveback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B{3} 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 | 7 + 2 | {A,B} | | + 3 | {A,B} | | + 4 | {A,B} | | + 5 | {A,B} | | + 6 | {A,B} | | + 7 | {C} | | +(7 rows) + +-- A+ B+ A+ over rows that are all A and B: three quantifiers draw from one +-- pool, so shrinking the first changes what the other two can take. They +-- must renegotiate jointly, not one at a time. +WITH test_quant_coupled_spans AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['A','B']), + (3, ARRAY['A','B']), + (4, 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_quant_coupled_spans +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 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 4 + 2 | {A,B} | | + 3 | {A,B} | | + 4 | {A,B} | | +(4 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) + +-- Consecutive groups are merged only when the body has a fixed row count. +-- (A | B B)+ (A | B B)+ keeps both groups: the merged (A | B B){2,} would +-- stop after two rows, because two iterations already meet its lower bound, +-- while the second group here still demands an iteration of its own. +WITH test_group_merge_uneven_alt AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A', 'B']), + (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_group_merge_uneven_alt +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B B)+ (A | B 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 | {B} | | + 4 | {A} | | +(4 rows) + +-- Same guard, reached through a reluctant quantifier in the body: B A+? has no +-- fixed row count either, so (B A+?)+ (B A+?)+ is left alone. +WITH test_group_merge_reluctant_body AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, 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_group_merge_reluctant_body +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((B A+?)+ (B A+?)+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | 1 | 7 + 2 | {A} | | + 3 | {B} | | + 4 | {A} | | + 5 | {A} | | + 6 | {B} | | + 7 | {A} | | +(7 rows) + +-- The trailing copy in (A | B B){1,2} (A | B B) is not folded into the group +-- for the same reason. A leading copy would be, since it is mandatory. +WITH test_suffix_merge_uneven_alt AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A', 'B']), + (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_suffix_merge_uneven_alt +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B B){1,2} (A | B 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 | {B} | | + 4 | {A} | | +(4 rows) + +-- Nested bounded quantifiers ((A{2,3}){1,2}) are not flattened into A{2,6}: +-- the first iteration's count is settled before the group decides whether to +-- iterate again, so with four A rows it takes three and leaves the group +-- rather than taking two and two. A{2,6} would take all four. +WITH test_nested_bounded_quantifier 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_nested_bounded_quantifier +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A{2,3}){1,2}) + DEFINE + A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {A} | | + 3 | {A} | | + 4 | {A} | | + 5 | {_} | | +(5 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) + +-- Branch preference (A B C | D): D completes first at row 1, but the +-- earlier-written A B C branch is preferred and replaces the match 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) + +-- Preference beats length even when the preferred branch matches empty: the +-- A* branch reaches FIN with no rows consumed and nfa_advance_alt stops there, +-- so B is never expanded on a row where it would have matched. +WITH test_alt_empty_pref AS ( + SELECT * FROM (VALUES + (1, ARRAY['A', 'B']), + (2, ARRAY['_', 'B']), + (3, ARRAY['A', 'B']), + (4, ARRAY['_', 'B']) + ) AS t(id, flags) +) +SELECT id, flags, count(*) OVER w AS n +FROM test_alt_empty_pref +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 | n +----+-------+--- + 1 | {A,B} | 1 + 2 | {_,B} | 0 + 3 | {A,B} | 1 + 4 | {_,B} | 0 +(4 rows) + +-- Swapping the branches makes B preferred, so every row matches one row. +WITH test_alt_empty_pref AS ( + SELECT * FROM (VALUES + (1, ARRAY['A', 'B']), + (2, ARRAY['_', 'B']), + (3, ARRAY['A', 'B']), + (4, ARRAY['_', 'B']) + ) AS t(id, flags) +) +SELECT id, flags, count(*) OVER w AS n +FROM test_alt_empty_pref +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (B | A*) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | n +----+-------+--- + 1 | {A,B} | 1 + 2 | {_,B} | 1 + 3 | {A,B} | 1 + 4 | {_,B} | 1 +(4 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. On a B row the A? branch still succeeds with an +-- empty match, which outranks the B branch, so the match is empty. +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} | | + 2 | {B} | | + 3 | {B} | | +(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 | 1 + 2 | {B} | | + 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} | | + 2 | {B} | | + 3 | {B} | | +(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) + +-- (A | (B C)+ (D E)+): the last branch is (B C)+ (D E)+, so with no A/B/C the +-- rows D E D E match nothing -- the trailing (D E)+ is not its own branch. +WITH test_alt_concat_groups AS ( + SELECT * FROM (VALUES + (1, ARRAY['D']), + (2, ARRAY['E']), + (3, ARRAY['D']), + (4, 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_alt_concat_groups +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 | {D} | | + 2 | {E} | | + 3 | {D} | | + 4 | {E} | | +(4 rows) + +-- (A | (B C)*): optional group as the last ALT branch. When (B C)* matches +-- zero the branch ends with an empty match (NULL bounds), consuming no row. +WITH test_alt_tail_optgroup AS ( + SELECT * FROM (VALUES + (1, ARRAY['_']), + (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_tail_optgroup +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 | {_} | | + 2 | {B} | 2 | 3 + 3 | {C} | | + 4 | {_} | | +(4 rows) + +-- ((B C)* | A): optional group as a non-last branch. Where B C does start +-- the group consumes it (row 2); elsewhere the branch still succeeds with a +-- zero match, and that outranks the A branch, so A never fires (row 4). +WITH test_alt_nonlast_optgroup AS ( + SELECT * FROM (VALUES + (1, ARRAY['_']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (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_alt_nonlast_optgroup +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((B C)* | A) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {_} | | + 2 | {B} | 2 | 3 + 3 | {C} | | + 4 | {A} | | +(4 rows) + +-- (((B C)* | A) D): the ALT is followed by D, so a zero-match (B C)* must +-- continue into D. Row 4 (a lone D) exercises that; row 1 is the B C D match. +WITH test_alt_optgroup_then_elem AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['C']), + (3, ARRAY['D']), + (4, ARRAY['D']), + (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_alt_optgroup_then_elem +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((B C)* | A) 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} | | + 3 | {D} | | + 4 | {D} | 4 | 4 + 5 | {_} | | +(5 rows) + +-- ((B C)* D | A): the same group inside a non-last branch, with D following it +-- in that branch. A zero-match (B C)* must continue into D, not fall through +-- into the A branch; row 4 (a lone D) is the row that tells the two apart. +WITH test_alt_nonlast_grp_then_elem AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['C']), + (3, ARRAY['D']), + (4, ARRAY['D']), + (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_alt_nonlast_grp_then_elem +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((B C)* D | A) + 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} | | + 3 | {D} | | + 4 | {D} | 4 | 4 + 5 | {_} | | +(5 rows) + +-- ((B C)*? | A): a reluctant optional group as a branch prefers zero +-- iterations, so the first branch matches empty on every row and the A +-- branch never fires. +WITH test_alt_reluctant_optgroup AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['C']), + (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_alt_reluctant_optgroup +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((B C)*? | A) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | | + 2 | {C} | | + 3 | {A} | | + 4 | {_} | | +(4 rows) + +-- Same variable re-entered across iterations: (A+ | B){2} on an all-A +-- partition. Both iterations take the A+ branch, so the second one re-enters +-- VAR A with a higher iteration count -- a new state, not a cycle. +WITH test_alt_quant_reentry 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_alt_quant_reentry +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((A+ | B){2}) + 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} | | +(3 rows) + +-- Branch order does not change the match: (B | A+){2} is the same. +WITH test_alt_quant_reentry_order 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_alt_quant_reentry_order +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((B | A+){2}) + 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} | | +(3 rows) + +-- Hand-unrolled form. mergeConsecutiveAlts rolls it back up to {2}, so the +-- match must be the same as the two above. +WITH test_alt_quant_unrolled 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_alt_quant_unrolled +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + 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 | 3 + 2 | {A} | | + 3 | {A} | | +(3 rows) + +-- ((A B) | (C D))+ with both branches two rows wide and every row shared: +-- the branch taken at one iteration decides which rows remain for the next. +WITH test_alt_multirow_branches AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','C']), + (2, ARRAY['B','D']), + (3, ARRAY['A','C']), + (4, ARRAY['B','D']), + (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_alt_multirow_branches +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,C} | 1 | 4 + 2 | {B,D} | | + 3 | {A,C} | 3 | 4 + 4 | {B,D} | | + 5 | {_} | | +(5 rows) + +-- Same rows, branches written in the other order. Both branches match the +-- same rows here, so the result must not change: the classification does, +-- but the frame does not. +WITH test_alt_multirow_swapped AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','C']), + (2, ARRAY['B','D']), + (3, ARRAY['A','C']), + (4, ARRAY['B','D']), + (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_alt_multirow_swapped +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((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,C} | 1 | 4 + 2 | {B,D} | | + 3 | {A,C} | 3 | 4 + 4 | {B,D} | | + 5 | {_} | | +(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) + +-- Exact outer quantifier over a variable-length body +-- (X{1,2}){2} is by definition X{1,2} X{1,2}, so the two must prefer the same +-- match. Collapsing to (A | B B){2,4} does not: the branches consume unequal +-- rows, so moving an iteration across the block boundary changes which rows +-- match. The third query below shows the collapsed form's own preference. +WITH test_nested_alt_body AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A','B']), + (3, ARRAY['B']), + (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_nested_alt_body +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A | B B){1,2}){2}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 5 + 2 | {A,B} | | + 3 | {B} | | + 4 | {A} | | + 5 | {A} | | +(5 rows) + +-- The same pattern written out. Must give the same match as the nested form. +WITH test_nested_alt_body AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A','B']), + (3, ARRAY['B']), + (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_nested_alt_body +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B B){1,2} (A | B B){1,2}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 5 + 2 | {A,B} | | + 3 | {B} | | + 4 | {A} | | + 5 | {A} | | +(5 rows) + +-- The collapsed form, written by hand: same iteration counts as the two above, +-- but with no block boundary forcing a second iteration it stops at rows 1-2. +-- That difference is why the two above must not be rewritten into this one. +WITH test_nested_alt_body AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A','B']), + (3, ARRAY['B']), + (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_nested_alt_body +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B B){2,4}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {A,B} | | + 3 | {B} | | + 4 | {A} | 4 | 5 + 5 | {A} | | +(5 rows) + +-- Re-entry into a group whose body always consumes rows +-- A body that cannot match empty consumes a row on every derivation, so a +-- loop-back into it is progress, not a cycle. Both forms must find the same +-- match: (X){3} is by definition X X X. +WITH test_reentry_consuming AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A','B']), + (3, ARRAY['B']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['A','B']), + (7, ARRAY['B']), + (8, 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_reentry_consuming +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A | B B){1,3}){3}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 8 + 2 | {A,B} | | + 3 | {B} | | + 4 | {A} | | + 5 | {A} | | + 6 | {A,B} | | + 7 | {B} | | + 8 | {A} | | +(8 rows) + +-- The same pattern written out. Must give the same match as the nested form. +WITH test_reentry_consuming AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A','B']), + (3, ARRAY['B']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['A','B']), + (7, ARRAY['B']), + (8, 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_reentry_consuming +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B B){1,3} (A | B B){1,3} (A | B 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 | {A,B} | | + 3 | {B} | | + 4 | {A} | | + 5 | {A} | | + 6 | {A,B} | | + 7 | {B} | | + 8 | {A} | | +(8 rows) + +-- Empty iteration followed by a consuming one, below min +-- A? is tried before B, so on row 1 the first two iterations go empty and the +-- third takes B, matching rows 1-2. The longer A B C match ranks lower: it +-- abandons A? in the first iteration (7.2.4 -- length breaks prefix ties only). +WITH test_empty_then_consume 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_empty_then_consume +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A? | B){3} 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 | 3 +(3 rows) + +-- The same pattern written out, with the copies renamed so that no rewrite +-- can fold them back into a loop. Must give the same match. +WITH test_empty_then_consume 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_empty_then_consume +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A? | B) (D? | E) (F? | G) C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'A' = ANY(flags), + E AS 'B' = ANY(flags), + F AS 'A' = ANY(flags), + G AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | 1 | 2 + 2 | {A,C} | | + 3 | {C} | 3 | 3 +(3 rows) + +-- Bound above the empty-then-consume cases above. The rows are the same at +-- every bound from 2 up while the derivation shifts, so a shortcut through +-- the below-min counts first shows here, where they are not adjacent. +WITH test_empty_then_consume_bound AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['A']), + (3, ARRAY['A','C']), + (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_empty_then_consume_bound +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A? | B){4} 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 | 3 + 2 | {A} | | + 3 | {A,C} | | + 4 | {C} | 4 | 4 +(4 rows) + +-- Recursion depth of the same below-min path: the fall-through advances the +-- count by one empty iteration at a time, so the depth follows the bound. +-- Reduced stack so the limit is reached quickly. +SET max_stack_depth = '100kB'; +WITH test_below_min_depth 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_below_min_depth +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A? | B){10000} C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); +ERROR: stack depth limit exceeded +HINT: Increase the configuration parameter "max_stack_depth" (currently 100kB), after ensuring the platform's stack depth limit is adequate. +RESET max_stack_depth; +-- (A+)+ B with an overlap row: the inner A+ must take row 3 as A, leaving +-- row 4 for B. Nested unbounded quantifiers over a row that satisfies both +-- the quantified variable and its successor. +WITH test_nest_unbounded_overlap 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_nest_unbounded_overlap +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,B} | 3 | 4 + 4 | {B} | | +(4 rows) + +-- ((A+ B)+ C)+ D: three unbounded quantifiers at three depths, every row +-- shared with its successor. Iteration boundaries at each level can be +-- drawn several ways over the same rows. +WITH test_nest_three_levels AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A','B']), + (3, ARRAY['B','C']), + (4, ARRAY['C','D']), + (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_nest_three_levels +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 | 5 + 2 | {A,B} | 2 | 5 + 3 | {B,C} | | + 4 | {C,D} | | + 5 | {D} | | +(5 rows) + +-- ((A | B)+)+ C: alternation nested inside two unbounded quantifiers, with +-- rows satisfying both alternatives. +WITH test_nest_alt_unbounded AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['A','B']), + (3, ARRAY['B','C']), + (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_nest_alt_unbounded +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,B} | 1 | 4 + 2 | {A,B} | 2 | 4 + 3 | {B,C} | 3 | 4 + 4 | {C} | | +(4 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) + +-- A* under SKIP PAST LAST ROW: row 1 matches empty, which consumes nothing +-- and so must not move the skip landing. Row 2 still starts its own match. +WITH test_skip_past_after_empty AS ( + SELECT * FROM (VALUES + (1, ARRAY['_']), + (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_skip_past_after_empty +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 | {_} | | + 2 | {A} | 2 | 3 + 3 | {A} | | + 4 | {_} | | +(4 rows) + +-- (A B)* C: row 1 matches C alone with the group taken zero times, so the +-- landing is row 2 and the A B C match at rows 2-4 is still found. +WITH test_skip_past_zero_iterations AS ( + SELECT * FROM (VALUES + (1, ARRAY['C']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (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_skip_past_zero_iterations +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 | {C} | 1 | 1 + 2 | {A} | 2 | 4 + 3 | {B} | | + 4 | {C} | | +(4 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 +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) + +-- (A (B*?)+?)+ : a reluctant unbounded quantifier over a nullable group, +-- inside an outer quantifier. The empty iteration ends the inner loop. +WITH test_cycle_reluctant_nullable AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, 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_cycle_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} | 1 | 2 + 2 | {A} | 2 | 2 +(2 rows) + +-- Same body under a bounded outer quantifier with lower bound zero. +WITH test_cycle_reluctant_bounded AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, 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_cycle_reluctant_bounded +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A (B*?)+?){0,2}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {A} | 2 | 2 +(2 rows) + +-- Greedy inner over the same nullable group: the preferred path consumes a +-- row and parks in the frontier, so it cannot recurse through epsilon. +WITH test_cycle_greedy_nullable AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, 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_cycle_greedy_nullable +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 | 2 + 2 | {A} | | +(2 rows) + +-- Non-nullable inner body: every derivation consumes a row, so no empty +-- iteration exists and the guard has nothing to do. +WITH test_cycle_nonnullable_inner 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_cycle_nonnullable_inner +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 | 4 + 2 | {B} | | + 3 | {A} | | + 4 | {B} | | +(4 rows) + +-- Reluctant unbounded quantifier over a nullable group, inside an outer +-- quantifier whose lower bound is above one. Below the bound the guard +-- must keep looping without unbounded epsilon recursion: each empty inner +-- iteration advances the count until the bound is met. Two iterations +-- (one A each, inner empty) satisfy min=2 and match rows 1-2. +WITH test_cycle_reluctant_below_min AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, 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_cycle_reluctant_below_min +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 | 2 + 2 | {A} | | +(2 rows) + +-- Same shape with a reluctant outer bound: stops at exactly two iterations +-- even though a third A row is available. +WITH test_cycle_reluctant_outer_min 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_cycle_reluctant_outer_min +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 | 2 + 2 | {A} | 2 | 3 + 3 | {A} | | +(3 rows) + +-- ------------------------------------------------------------ +-- Preferment guards: the path explored second must not record +-- over a match the first one already made +-- ------------------------------------------------------------ +-- +-- Where two paths leave the same element the first is the preferred one, so +-- once it has recorded a match the second must not run. Both derivations +-- cover the same rows below, so the output is the same either way; what +-- fails when a guard is removed is an assertion, in a cassert build. +-- Entering a group before skipping it. +WITH test_guard_begin_enter AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, 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_guard_begin_enter +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 | 2 + 2 | {A} | 2 | 2 +(2 rows) + +-- Below the lower bound, looping back before the fast-forward. +WITH test_guard_end_below_min AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (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_guard_end_below_min +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? B?){3,4}) + 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 +(2 rows) + +-- Between the bounds, looping before the exit. Here the second path parks +-- a state instead of reaching FIN. +WITH test_guard_end_loop_exit AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, 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_guard_end_loop_exit +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 | {A} | | +(2 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) + +-- (A | B B | C C C): three alternatives, all viable on every row. +-- Preferment order is A, BB, CCC: the first alternative wins even though +-- the later ones would match longer. +WITH test_alt_three_way AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B','C']), + (2, ARRAY['A','B','C']), + (3, ARRAY['A','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_three_way +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A | B B | C C 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,C} | 1 | 1 + 2 | {A,B,C} | 2 | 2 + 3 | {A,B,C} | 3 | 3 +(3 rows) + +-- ((A | B | C)+): three alternatives ranked on every iteration, not just +-- once. Each row carries all three flags. +WITH test_alt_three_way_loop AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B','C']), + (2, ARRAY['A','B','C']), + (3, ARRAY['A','B','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_three_way_loop +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,B,C} | 1 | 3 + 2 | {A,B,C} | 2 | 3 + 3 | {A,B,C} | 3 | 3 + 4 | {_} | | +(4 rows) + +-- (A B | A C): branches share the prefix A, so the choice is only decided +-- at row 2, which carries both B and C. The first alternative wins there. +WITH test_alt_shared_prefix AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B','C']), + (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_shared_prefix +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A 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 | {A} | 1 | 2 + 2 | {B,C} | | + 3 | {C} | | + 4 | {_} | | +(4 rows) + +-- (A B C | A B): the first alternative is the longer one and it fits, so +-- length and written order agree. Compare with the reverse below. +WITH test_alt_shared_prefix_long_first 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_alt_shared_prefix_long_first +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A B C | A 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} | 1 | 3 + 2 | {B} | | + 3 | {C} | | + 4 | {_} | | +(4 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 +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) + +-- A+? B where every row is both A and B: stopping and continuing are both +-- viable at every step, so reluctance is actually contested. Without the +-- overlap the reluctant path is never tested against a live alternative. +WITH test_quant_reluctant_contested AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['A','B']), + (3, ARRAY['A','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_quant_reluctant_contested +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 | 2 + 2 | {A,B} | | + 3 | {A,B} | | + 4 | {_} | | +(4 rows) + +-- A+ B over the same rows: greedy takes as many as it can while still +-- leaving a B behind. Contrast with the reluctant case above. +WITH test_quant_greedy_contested AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['A','B']), + (3, ARRAY['A','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_quant_greedy_contested +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 | 3 + 2 | {A,B} | | + 3 | {A,B} | | + 4 | {_} | | +(4 rows) + +-- (A+ B)+? C: the outer quantifier is reluctant, so it stops after one +-- iteration and takes the C available at row 3. Row 3 is also an A, which +-- would let a second iteration run; reluctance declines it. +WITH test_quant_reluctant_outer AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A','C']), + (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_quant_reluctant_outer +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 | {A,C} | | + 4 | {B} | | + 5 | {C} | | +(5 rows) + +-- (A+ B)+ C over the same rows: outer greed takes the second iteration and +-- lands on the C at row 5 instead. Contrast with the reluctant outer above. +WITH test_quant_greedy_outer AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A','C']), + (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_quant_greedy_outer +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,C} | | + 4 | {B} | | + 5 | {C} | | +(5 rows) + +-- A{2}? B: an exact bound leaves no choice, so the reluctant mark must not +-- change anything. Overlap on rows 2-3 would expose a bound lowered to +-- {0,2}? or {1,2}?. +WITH test_quant_reluctant_exact AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A','B']), + (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_quant_reluctant_exact +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,B} | | + 3 | {A,B} | | + 4 | {B} | | +(4 rows) + +-- A{2} B: the greedy spelling of the same bound, for contrast. +WITH test_quant_exact AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A','B']), + (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_quant_exact +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,B} | | + 3 | {A,B} | | + 4 | {B} | | +(4 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. +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. +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. +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 +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){3}: an empty iteration below min fills the lower bound (STR06), +-- and it must outrank the later branch. Row 2 is B only, so A? derives empty +-- there; repeating that derivation fills the remaining iterations and the +-- match ends at row 1. Taking branch B instead would consume rows 2-3. +WITH test_728_empty_fills_min 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_728_empty_fills_min +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? | B){3}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 1 + 2 | {B} | | + 3 | {A} | 3 | 3 +(3 rows) + +-- The same pattern unrolled. Consecutive identical alternations are merged +-- into the rolled form above, so the two must agree. +WITH test_728_empty_fills_min_unrolled 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_728_empty_fills_min_unrolled +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 'A' = ANY(flags), + D AS 'B' = ANY(flags), + E AS 'A' = ANY(flags), + F AS 'B' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 1 + 2 | {B} | | + 3 | {A} | 3 | 3 +(3 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. +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). +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 +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) + +-- The stopping rule also governs an alternation body. On A,B the greedy +-- (A? | B)* takes A in iteration 1; in iteration 2 the preferred branch A? +-- matches empty, so the quantifier stops and B never consumes row 2. +WITH test_empty_stop_alt_body AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (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_empty_stop_alt_body +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 | {B} | | +(2 rows) + +-- With min = max the group's own greed says nothing, so the body decides +-- whether the empty match is preferred: {n}? must equal {n}. A? prefers to +-- consume (both forms take two rows); A?? prefers empty (both match empty). +WITH test_fixed_quant_body_greed AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + count(*) OVER g AS greedy_body, -- ((A?){2}) + count(*) OVER gr AS greedy_body_rel, -- ((A?){2}?) + count(*) OVER r AS rel_body, -- ((A??){2}) + count(*) OVER rr AS rel_body_rel -- ((A??){2}?) +FROM test_fixed_quant_body_greed +WINDOW g 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)), + gr 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)), + r 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)), + rr 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)) +ORDER BY id; + id | flags | greedy_body | greedy_body_rel | rel_body | rel_body_rel +----+-------+-------------+-----------------+----------+-------------- + 1 | {A} | 2 | 2 | 0 | 0 + 2 | {A} | 0 | 0 | 0 | 0 +(2 rows) + +-- (A* | B)*: A* is the preferred alternative and matches empty at row 3, +-- which ends the loop by the lower-bound stopping rule. B is never tried, +-- so the match stops short of the B rows even though taking them would be +-- longer. Perl agrees: (a*|b)* against "aabb" matches "aa". +WITH test_728_nullable_alt_first AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (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_728_nullable_alt_first +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 | {A} | 2 | 2 + 3 | {B} | | + 4 | {B} | | +(4 rows) + +-- (B | A*)*: same body, alternatives swapped. B is preferred and consumes, +-- so no empty iteration arises and the loop reaches the B rows. +WITH test_728_nullable_alt_second AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (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_728_nullable_alt_second +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((B | A*)*) + 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 +(4 rows) + +-- (A+ | B)*: the first alternative is not nullable, so it cannot produce an +-- empty iteration and the loop reaches the B rows. Compare with (A* | B)*. +WITH test_728_nonnullable_alt_first AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (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_728_nonnullable_alt_first +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 +(4 rows) + +-- (A? | B){2,3}: the stop rule binds at the lower bound even when the +-- upper bound could still admit more iterations. Iterations one and two +-- go empty via A?, which stops the loop at min; C then fails at row 1 and +-- backtracking REPLACES iteration two with B instead of extending, so the +-- match runs B, A, C = rows 1-3. Perl agrees: (?:a?|b){2,3}c backtracks +-- the same way. An engine that keeps iterating after the empty stop +-- returns rows 1-2 via the shorter empty-empty-B derivation instead. +WITH test_728_stop_binds_at_min 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_728_stop_binds_at_min +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? | B){2,3} 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 | 3 + 2 | {A,C} | 2 | 3 + 3 | {C} | 3 | 3 +(3 rows) + +-- (A? | B){3} C over the same rows: with an exact bound the two empty +-- iterations sit below min, so the loop must continue; the third takes B +-- and the match is rows 1-2. Contrast with the {2,3} case above. +WITH test_728_exact_below_min 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_728_exact_below_min +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? | B){3} 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} | 2 | 3 + 3 | {C} | 3 | 3 +(3 rows) + +-- (A? | B){2,}: the stopping rule applies above the lower bound as well. +-- Two iterations consume the A rows and satisfy min=2; the third matches +-- empty via A? and ends the loop before any B is taken. +WITH test_728_nullable_alt_min2 AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (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_728_nullable_alt_min2 +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 | 2 + 2 | {A} | 2 | 2 + 3 | {B} | | + 4 | {B} | | +(4 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) + -- 2.43.0