From c8d8fc185fb7d1b5eae6824f4bc731336d36bac7 Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii Date: Tue, 20 Oct 2026 17:34:39 +0900 Subject: [PATCH v54 8/9] Row pattern recognition patch (tests: expected). --- src/test/regress/expected/create_view.out | 451 + src/test/regress/expected/rpr.out | 5083 ++++++ src/test/regress/expected/rpr_base.out | 13104 ++++++++++++++++ src/test/regress/expected/rpr_explain.out | 7042 +++++++++ src/test/regress/expected/rpr_integration.out | 2645 ++++ src/test/regress/expected/rpr_nfa.out | 7421 +++++++++ src/test/regress/expected/rules.out | 54 + src/test/regress/expected/window.out | 36 + 8 files changed, 35836 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/create_view.out b/src/test/regress/expected/create_view.out index 11e0460d009..c9340f137fa 100644 --- a/src/test/regress/expected/create_view.out +++ b/src/test/regress/expected/create_view.out @@ -971,6 +971,457 @@ select pg_get_viewdef('view_of_joins_2d', true); JOIN tbl1a USING (a) AS x) y; (1 row) +-- A TABLEFUNC RTE names its columns in the clause that produces them, but it +-- accepts a column alias list like any other RTE, and that list is where a +-- rename of one of its columns has to be printed: the ON clause below refers +-- to the column by the name the query sees, and without the list the two +-- would not agree. The anonymous FULL JOIN is what forces USING names to be +-- unique query-wide, which is what pushes the JSON_TABLE column aside. +create table tblnr (m int); +create table tblnu (x int, m int); +create table tblnl (x int); +create table tblnm (x int); +create table tblnw (a int, c int, d int); +create table tblnv (c int); +create view view_of_unrenamable as +select j.m +from (tblnl full join tblnm using (x)), + (json_table(jsonb '[1,2]', '$[*]' columns (x int path '$')) as jt + join tblnr on jt.x > 0) j; +select pg_get_viewdef('view_of_unrenamable', true); + pg_get_viewdef +---------------------------------------------------------- + SELECT j.m + + FROM tblnl + + FULL JOIN tblnm USING (x), + + (JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0+ + COLUMNS ( + + x integer PATH '$' + + ) + + ) jt(x_1) + + JOIN tblnr ON jt.x_1 > 0) j; +(1 row) + +-- and that text is what has to reparse +select 'create view view_of_unrenamable_2 as ' + || pg_get_viewdef('view_of_unrenamable', true) \gexec +create view view_of_unrenamable_2 as SELECT j.m + FROM tblnl + FULL JOIN tblnm USING (x), + (JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + x integer PATH '$' + ) + ) jt(x_1) + JOIN tblnr ON jt.x_1 > 0) j; +select pg_get_viewdef('view_of_unrenamable', true) + = pg_get_viewdef('view_of_unrenamable_2', true) as round_trips; + round_trips +------------- + t +(1 row) + +-- The name of a merged column is the other place a rename lands, and it lands +-- on both sides at once: whatever is picked, each input has to answer to it, +-- the TABLEFUNC through its alias list like the table through its own. +create view view_of_unrenamable_using as +select j.m +from (tblnl full join tblnm using (x)), + (json_table(jsonb '[1,2]', '$[*]' columns (x int path '$')) as jt + join tblnu using (x)) j; +select pg_get_viewdef('view_of_unrenamable_using', true); + pg_get_viewdef +---------------------------------------------------------- + SELECT j.m + + FROM tblnl + + FULL JOIN tblnm USING (x), + + (JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0+ + COLUMNS ( + + x integer PATH '$' + + ) + + ) jt(x_1) + + JOIN tblnu tblnu(x_1, m) USING (x_1)) j; +(1 row) + +select 'create view view_of_unrenamable_using_2 as ' + || pg_get_viewdef('view_of_unrenamable_using', true) \gexec +create view view_of_unrenamable_using_2 as SELECT j.m + FROM tblnl + FULL JOIN tblnm USING (x), + (JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + x integer PATH '$' + ) + ) jt(x_1) + JOIN tblnu tblnu(x_1, m) USING (x_1)) j; +select pg_get_viewdef('view_of_unrenamable_using', true) + = pg_get_viewdef('view_of_unrenamable_using_2', true) as round_trips; + round_trips +------------- + t +(1 row) + +-- The far side of an INNER or LEFT JOIN is as much a side as the near one: +-- the merged column's expression names only the near input, but the rename +-- reaches both. +create view view_of_unrenamable_right as +select j.m +from (tblnl full join tblnm using (x)), + ((tblnu join json_table(jsonb '[1,2]', '$[*]' columns (x int path '$')) as jt + using (x)) full join tblnm t2 using (x)) j; +select pg_get_viewdef('view_of_unrenamable_right', true); + pg_get_viewdef +---------------------------------------------------------- + SELECT j.m + + FROM tblnl + + FULL JOIN tblnm USING (x), + + (tblnu tblnu(x_1, m) + + JOIN JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0+ + COLUMNS ( + + x integer PATH '$' + + ) + + ) jt(x_1) USING (x_1) + + FULL JOIN tblnm t2(x_1) USING (x_1)) j; +(1 row) + +select 'create view view_of_unrenamable_right_2 as ' + || pg_get_viewdef('view_of_unrenamable_right', true) \gexec +create view view_of_unrenamable_right_2 as SELECT j.m + FROM tblnl + FULL JOIN tblnm USING (x), + (tblnu tblnu(x_1, m) + JOIN JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + x integer PATH '$' + ) + ) jt(x_1) USING (x_1) + FULL JOIN tblnm t2(x_1) USING (x_1)) j; +select pg_get_viewdef('view_of_unrenamable_right', true) + = pg_get_viewdef('view_of_unrenamable_right_2', true) as round_trips; + round_trips +------------- + t +(1 row) + +create view view_of_unrenamable_left as +select j.m +from (tblnl full join tblnm using (x)), + ((tblnu left join json_table(jsonb '[1,2]', '$[*]' columns (x int path '$')) as jt + using (x)) full join tblnm t2 using (x)) j; +select pg_get_viewdef('view_of_unrenamable_left', true); + pg_get_viewdef +---------------------------------------------------------- + SELECT j.m + + FROM tblnl + + FULL JOIN tblnm USING (x), + + (tblnu tblnu(x_1, m) + + LEFT JOIN JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0+ + COLUMNS ( + + x integer PATH '$' + + ) + + ) jt(x_1) USING (x_1) + + FULL JOIN tblnm t2(x_1) USING (x_1)) j; +(1 row) + +select 'create view view_of_unrenamable_left_2 as ' + || pg_get_viewdef('view_of_unrenamable_left', true) \gexec +create view view_of_unrenamable_left_2 as SELECT j.m + FROM tblnl + FULL JOIN tblnm USING (x), + (tblnu tblnu(x_1, m) + LEFT JOIN JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + x integer PATH '$' + ) + ) jt(x_1) USING (x_1) + FULL JOIN tblnm t2(x_1) USING (x_1)) j; +select pg_get_viewdef('view_of_unrenamable_left', true) + = pg_get_viewdef('view_of_unrenamable_left_2', true) as round_trips; + round_trips +------------- + t +(1 row) + +-- An aliased join hides its inputs, and a USING name above it is taken by +-- the join's own column: the join renames it on its alias list, and the +-- TABLEFUNC underneath, being held to the query-wide USING names as well, +-- renames its column on its own list. +create view view_of_unrenamable_hidden as +select j.m +from (tblnl full join tblnm using (x)), + ((json_table(jsonb '[1,2]', '$[*]' columns (x int path '$')) as jt + join tblnr on true) j full join tblnm t2 using (x)); +select pg_get_viewdef('view_of_unrenamable_hidden', true); + pg_get_viewdef +---------------------------------------------------------- + SELECT j.m + + FROM tblnl + + FULL JOIN tblnm USING (x), + + (JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0+ + COLUMNS ( + + x integer PATH '$' + + ) + + ) jt(x_2) + + JOIN tblnr ON true) j(x_1, m) + + FULL JOIN tblnm t2(x_1) USING (x_1); +(1 row) + +select 'create view view_of_unrenamable_hidden_2 as ' + || pg_get_viewdef('view_of_unrenamable_hidden', true) \gexec +create view view_of_unrenamable_hidden_2 as SELECT j.m + FROM tblnl + FULL JOIN tblnm USING (x), + (JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + x integer PATH '$' + ) + ) jt(x_2) + JOIN tblnr ON true) j(x_1, m) + FULL JOIN tblnm t2(x_1) USING (x_1); +select pg_get_viewdef('view_of_unrenamable_hidden', true) + = pg_get_viewdef('view_of_unrenamable_hidden_2', true) as round_trips; + round_trips +------------- + t +(1 row) + +-- A name a parent join pushes down onto a merged column has to be unique in +-- the input it lands in, whatever the USING clause underneath spells. +create view view_of_unrenamable_pushed as +select j2.d +from (tblnv join (tblnw join json_table(jsonb '[1]', '$[*]' columns (a int path '$')) x + using (a)) using (c)) as j2(a); +select pg_get_viewdef('view_of_unrenamable_pushed', true); + pg_get_viewdef +------------------------------------------------------- + SELECT j2.d + + FROM (tblnv tblnv(a) + + JOIN (tblnw tblnw(a_1, a, d) + + JOIN JSON_TABLE( + + '[1]'::jsonb, '$[*]' AS json_table_path_0+ + COLUMNS ( + + a integer PATH '$' + + ) + + ) x(a_1) USING (a_1)) USING (a)) j2; +(1 row) + +select 'create view view_of_unrenamable_pushed_2 as ' + || pg_get_viewdef('view_of_unrenamable_pushed', true) \gexec +create view view_of_unrenamable_pushed_2 as SELECT j2.d + FROM (tblnv tblnv(a) + JOIN (tblnw tblnw(a_1, a, d) + JOIN JSON_TABLE( + '[1]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + a integer PATH '$' + ) + ) x(a_1) USING (a_1)) USING (a)) j2; +select pg_get_viewdef('view_of_unrenamable_pushed', true) + = pg_get_viewdef('view_of_unrenamable_pushed_2', true) as round_trips; + round_trips +------------- + t +(1 row) + +-- Two anonymous FULL JOINs merging the same name through a TABLEFUNC each +-- still get distinct names. +create view view_of_unrenamable_twice as +select count(*) as n +from (tblnl full join json_table(jsonb '[1]', '$[*]' columns (x int path '$')) jt1 + using (x)), + (tblnm full join json_table(jsonb '[1]', '$[*]' columns (x int path '$')) jt2 + using (x)); +select pg_get_viewdef('view_of_unrenamable_twice', true); + pg_get_viewdef +------------------------------------------------------- + SELECT count(*) AS n + + FROM tblnl + + FULL JOIN JSON_TABLE( + + '[1]'::jsonb, '$[*]' AS json_table_path_0+ + COLUMNS ( + + x integer PATH '$' + + ) + + ) jt1 USING (x), + + tblnm tblnm(x_1) + + FULL JOIN JSON_TABLE( + + '[1]'::jsonb, '$[*]' AS json_table_path_0+ + COLUMNS ( + + x integer PATH '$' + + ) + + ) jt2(x_1) USING (x_1); +(1 row) + +select 'create view view_of_unrenamable_twice_2 as ' + || pg_get_viewdef('view_of_unrenamable_twice', true) \gexec +create view view_of_unrenamable_twice_2 as SELECT count(*) AS n + FROM tblnl + FULL JOIN JSON_TABLE( + '[1]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + x integer PATH '$' + ) + ) jt1 USING (x), + tblnm tblnm(x_1) + FULL JOIN JSON_TABLE( + '[1]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + x integer PATH '$' + ) + ) jt2(x_1) USING (x_1); +select pg_get_viewdef('view_of_unrenamable_twice', true) + = pg_get_viewdef('view_of_unrenamable_twice_2', true) as round_trips; + round_trips +------------- + t +(1 row) + +-- and a column alias list the user wrote on a TABLEFUNC is part of the query +create view view_of_tablefunc_alias as +select t.a from json_table(jsonb '[1]', '$[*]' columns (c int path '$')) as t(a); +select pg_get_viewdef('view_of_tablefunc_alias', true); + pg_get_viewdef +------------------------------------------------------- + SELECT a + + FROM JSON_TABLE( + + '[1]'::jsonb, '$[*]' AS json_table_path_0+ + COLUMNS ( + + c integer PATH '$' + + ) + + ) t(a); +(1 row) + +select 'create view view_of_tablefunc_alias_2 as ' + || pg_get_viewdef('view_of_tablefunc_alias', true) \gexec +create view view_of_tablefunc_alias_2 as SELECT a + FROM JSON_TABLE( + '[1]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + c integer PATH '$' + ) + ) t(a); +select pg_get_viewdef('view_of_tablefunc_alias', true) + = pg_get_viewdef('view_of_tablefunc_alias_2', true) as round_trips; + round_trips +------------- + t +(1 row) + +drop view view_of_tablefunc_alias_2, view_of_tablefunc_alias; +drop view view_of_unrenamable_twice_2, view_of_unrenamable_twice; +drop view view_of_unrenamable_pushed_2, view_of_unrenamable_pushed; +drop view view_of_unrenamable_hidden_2, view_of_unrenamable_hidden; +drop view view_of_unrenamable_left_2, view_of_unrenamable_left; +drop view view_of_unrenamable_right_2, view_of_unrenamable_right; +drop view view_of_unrenamable_using_2, view_of_unrenamable_using; +drop view view_of_unrenamable_2, view_of_unrenamable; +drop table tblnr, tblnu, tblnl, tblnm, tblnw, tblnv; +-- The columns a function's result type grows after the view is made are +-- columns the RTE has now, and the alias list being positional they are +-- printed in full: an aliased join above lays its own list over its inputs' +-- lists end to end, so a grown column left off would shift the input after +-- it, and a reference into that input would silently land on another column. +create table tblfc (a int, z int); +insert into tblfc values (1, 5); +create function tblfc_f() returns setof tblfc language sql + as $$ select * from tblfc $$; +create table tblfr (z int, val int); +insert into tblfr values (1, 7); +create view view_of_grown_input as +select j.a, j.val from (tblfc_f() f join tblfr on true) j; +select * from view_of_grown_input; + a | val +---+----- + 1 | 7 +(1 row) + +alter table tblfc add column val int, add column spare int; +select pg_get_viewdef('view_of_grown_input', true); + pg_get_viewdef +----------------------------------------------------------- + SELECT j.a, + + j.val + + FROM (tblfc_f() f(a, z, val, spare) + + JOIN tblfr ON true) j(a, z, val_1, spare, z_1, val); +(1 row) + +select 'create view view_of_grown_input_2 as ' + || pg_get_viewdef('view_of_grown_input', true) \gexec +create view view_of_grown_input_2 as SELECT j.a, + j.val + FROM (tblfc_f() f(a, z, val, spare) + JOIN tblfr ON true) j(a, z, val_1, spare, z_1, val); +select pg_get_viewdef('view_of_grown_input', true) + = pg_get_viewdef('view_of_grown_input_2', true) as round_trips; + round_trips +------------- + t +(1 row) + +select * from view_of_grown_input; + a | val +---+----- + 1 | 7 +(1 row) + +select * from view_of_grown_input_2; + a | val +---+----- + 1 | 7 +(1 row) + +drop view view_of_grown_input_2; +-- A grown column that is dropped again still takes up its place in the +-- positional alias list, as a dropped column; a column grown after it must +-- not slide into that place. +alter table tblfc drop column val; +alter table tblfc add column val int; +select pg_get_viewdef('view_of_grown_input', true); + pg_get_viewdef +----------------------------------------------------------- + SELECT j.a, + + j.val + + FROM (tblfc_f() f(a, z, spare, val) + + JOIN tblfr ON true) j(a, z, spare, val_1, z_1, val); +(1 row) + +select 'create view view_of_grown_input_2 as ' + || pg_get_viewdef('view_of_grown_input', true) \gexec +create view view_of_grown_input_2 as SELECT j.a, + j.val + FROM (tblfc_f() f(a, z, spare, val) + JOIN tblfr ON true) j(a, z, spare, val_1, z_1, val); +select pg_get_viewdef('view_of_grown_input', true) + = pg_get_viewdef('view_of_grown_input_2', true) as round_trips; + round_trips +------------- + t +(1 row) + +select * from view_of_grown_input; + a | val +---+----- + 1 | 7 +(1 row) + +select * from view_of_grown_input_2; + a | val +---+----- + 1 | 7 +(1 row) + +drop view view_of_grown_input_2, view_of_grown_input; +drop function tblfc_f(); +drop table tblfc, tblfr; -- Test view decompilation in the face of column addition/deletion/renaming create table tt2 (a int, b int, c int); create table tt3 (ax int8, b int2, c numeric); diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out new file mode 100644 index 00000000000..1958fc67e32 --- /dev/null +++ b/src/test/regress/expected/rpr.out @@ -0,0 +1,5083 @@ +-- +-- 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 rpr_price (company TEXT, tdate DATE, price INTEGER); +INSERT INTO rpr_price 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 rpr_price; + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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_ab_pairs (id int, val text); +INSERT INTO rpr_ab_pairs 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_ab_pairs +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_ab_pairs; +-- last_value() should remain consistent +SELECT company, tdate, price, last_value(price) OVER w + FROM rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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: volatile function as compound outer offset +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(LAST(price, 1), random()::int) > 0 +); +ERROR: DEFINE clause cannot contain volatile functions +-- Non-constant offset: subquery as offset +SELECT price FROM rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price + 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 rpr_price + 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 rpr_price + 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 + ^ +-- An outer range variable is subject to the same two rules as a local one: a +-- whole-row reference is rejected as one, and a name that does not resolve +-- keeps its own diagnosis rather than being reported as a qualifier problem. +SELECT * FROM (VALUES (95)) AS o(threshold), +LATERAL ( + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS (o.*) IS NOT NULL + ) +) s; +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 9: DEFINE A AS (o.*) IS NOT NULL + ^ +HINT: A DEFINE condition may reference individual columns only. +SELECT * FROM (VALUES (95)) AS o(threshold), +LATERAL ( + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS o.threshhold > 0 + ) +) s; +ERROR: column o.threshhold does not exist +LINE 9: DEFINE A AS o.threshhold > 0 + ^ +HINT: Perhaps you meant to reference the column "o.threshold". +-- A two-part name is not always a range variable qualifier: a SQL function's +-- parameter and a PL/pgSQL variable both resolve through +-- p_post_columnref_hook. The qualifier slot is reserved all the same, so +-- these are rejected for the spelling, not for what they name. +CREATE FUNCTION rpr_sqlfn(threshold int) RETURNS SETOF int +LANGUAGE sql AS $$ + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > rpr_sqlfn.threshold) +$$; +ERROR: qualified expression "rpr_sqlfn.threshold" is not allowed in DEFINE clause +LINE 9: DEFINE A AS price > rpr_sqlfn.threshold) + ^ +HINT: Write the name without its qualifier, or write "(x).field" to select a field of a composite value. +CREATE FUNCTION rpr_plfn(threshold int) RETURNS bigint +LANGUAGE plpgsql AS $$ +DECLARE + n bigint; +BEGIN + SELECT count(*) INTO n FROM ( + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > rpr_plfn.threshold) + ) s; + RETURN n; +END +$$; +SELECT rpr_plfn(0); +ERROR: qualified expression "rpr_plfn.threshold" is not allowed in DEFINE clause +LINE 8: DEFINE A AS price > rpr_plfn.threshold) + ^ +HINT: Write the name without its qualifier, or write "(x).field" to select a field of a composite value. +QUERY: SELECT count(*) FROM ( + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > rpr_plfn.threshold) + ) s +CONTEXT: PL/pgSQL function rpr_plfn(integer) line 5 at SQL statement +DROP FUNCTION rpr_plfn(int); +-- Unqualified, the same parameter is readable. +CREATE FUNCTION rpr_sqlfn(threshold int) RETURNS SETOF int +LANGUAGE sql AS $$ + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > threshold) +$$; +SELECT count(*) FROM rpr_sqlfn(0); + count +------- + 20 +(1 row) + +DROP FUNCTION rpr_sqlfn(int); +-- The qualifier slot is decided on the qualifier alone, before resolution, so +-- a pattern variable takes the slot even from the routine that contains the +-- query. Naming a pattern variable after the function makes rpr_pv.threshold +-- the pattern variable's, and the reservation is reported; that the function +-- has a parameter of that name, and the query has no such column, does not +-- enter into it. +CREATE FUNCTION rpr_pv(threshold int) RETURNS SETOF int +LANGUAGE sql AS $$ + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (rpr_pv) + DEFINE rpr_pv AS price > rpr_pv.threshold) +$$; +ERROR: pattern variable qualified expression "rpr_pv.threshold" is not supported in DEFINE clause +LINE 9: DEFINE rpr_pv AS price > rpr_pv.threshold) + ^ +-- The collision is in the qualifier, not in the DEFINE variable being +-- defined: any pattern variable of that name reserves it. +CREATE FUNCTION rpr_pv(threshold int) RETURNS SETOF int +LANGUAGE sql AS $$ + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (rpr_pv A) + DEFINE A AS price > rpr_pv.threshold) +$$; +ERROR: pattern variable qualified expression "rpr_pv.threshold" is not supported in DEFINE clause +LINE 9: DEFINE A AS price > rpr_pv.threshold) + ^ +-- A field of a composite parameter has no unqualified spelling, so it is +-- reached by parenthesizing the value: "(p).lo" selects a field rather than +-- qualifying a name, and occupies no qualifier slot. +CREATE TYPE rpr_pair AS (lo int, hi int); +CREATE FUNCTION rpr_compfn(p rpr_pair) RETURNS SETOF int +LANGUAGE sql AS $$ + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > p.lo) +$$; +ERROR: qualified expression "p.lo" is not allowed in DEFINE clause +LINE 9: DEFINE A AS price > p.lo) + ^ +HINT: Write the name without its qualifier, or write "(x).field" to select a field of a composite value. +CREATE FUNCTION rpr_compfn(p rpr_pair) RETURNS SETOF int +LANGUAGE sql AS $$ + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > (p).lo) +$$; +SELECT count(*) FROM rpr_compfn(ROW(0, 0)::rpr_pair); + count +------- + 20 +(1 row) + +DROP FUNCTION rpr_compfn(rpr_pair); +DROP TYPE rpr_pair; +-- The DEFINE rules apply to the names the ref hooks leave to the query +-- parser. Under use_variable resolution PL/pgSQL answers first and keeps +-- any name one of its variables owns, so the qualified spelling rejected +-- above is resolved by PL/pgSQL here and never reaches the rule. +CREATE FUNCTION rpr_plfn_var(threshold int) RETURNS bigint +LANGUAGE plpgsql AS $$ +#variable_conflict use_variable +DECLARE + n bigint; +BEGIN + SELECT count(*) INTO n FROM ( + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > rpr_plfn_var.threshold) + ) s; + RETURN n; +END +$$; +SELECT rpr_plfn_var(0); + rpr_plfn_var +-------------- + 20 +(1 row) + +DROP FUNCTION rpr_plfn_var(int); +-- The same applies to a pattern variable's name. Under the default +-- resolution PL/pgSQL declines the name, so the reservation is reached and +-- the collision is reported rather than resolved. +CREATE FUNCTION rpr_conflictfn_err() RETURNS bigint +LANGUAGE plpgsql AS $$ +DECLARE + a rpr_price%ROWTYPE; + n bigint; +BEGIN + a.price := 95; + SELECT count(*) INTO n FROM ( + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > a.price) + ) s; + RETURN n; +END +$$; +SELECT rpr_conflictfn_err(); +ERROR: pattern variable qualified expression "a.price" is not supported in DEFINE clause +LINE 8: DEFINE A AS price > a.price) + ^ +QUERY: SELECT count(*) FROM ( + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > a.price) + ) s +CONTEXT: PL/pgSQL function rpr_conflictfn_err() line 7 at SQL statement +DROP FUNCTION rpr_conflictfn_err(); +CREATE FUNCTION rpr_conflictfn() RETURNS bigint +LANGUAGE plpgsql AS $$ +#variable_conflict use_variable +DECLARE + a rpr_price%ROWTYPE; + n bigint; +BEGIN + a.price := 95; + SELECT count(*) INTO n FROM ( + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > a.price) + ) s; + RETURN n; +END +$$; +SELECT rpr_conflictfn(); + rpr_conflictfn +---------------- + 20 +(1 row) + +DROP FUNCTION rpr_conflictfn(); +-- A star on a name a ref hook owns is not a reference to a FROM-clause +-- relation, so it expands in a DEFINE condition as it does anywhere else. +-- Withholding the expansion would not reject such a name; it would read +-- "rec.*" as the single whole value "rec", which is a different condition, +-- and one that differs silently wherever a row constructor is not counting +-- its entries. Each function below matches every row when the record holds +-- (1,2) and none when it does not, so a reading that is off returns the +-- wrong count rather than an error. +CREATE TYPE rpr_pair AS (a int, b int); +CREATE TEMP TABLE rpr_rec (id int); +INSERT INTO rpr_rec VALUES (1), (2), (3); +-- under the default resolution the post hook answers the name: +CREATE FUNCTION rpr_recstar(x int, y int) RETURNS bigint +LANGUAGE plpgsql AS $$ +DECLARE + rec rpr_pair; + n bigint; +BEGIN + rec := ROW(x, y); + SELECT count(*) OVER w INTO n FROM rpr_rec + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS ROW(rec.*)::text = '(1,2)') + LIMIT 1; + RETURN n; +END +$$; +SELECT rpr_recstar(1, 2); + rpr_recstar +------------- + 3 +(1 row) + +SELECT rpr_recstar(1, 3); + rpr_recstar +------------- + 0 +(1 row) + +DROP FUNCTION rpr_recstar(int, int); +-- under use_variable the pre hook answers it instead: +CREATE FUNCTION rpr_recstar_var(x int, y int) RETURNS bigint +LANGUAGE plpgsql AS $$ +#variable_conflict use_variable +DECLARE + rec rpr_pair; + n bigint; +BEGIN + rec := ROW(x, y); + SELECT count(*) OVER w INTO n FROM rpr_rec + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS ROW(rec.*)::text = '(1,2)') + LIMIT 1; + RETURN n; +END +$$; +SELECT rpr_recstar_var(1, 2); + rpr_recstar_var +----------------- + 3 +(1 row) + +SELECT rpr_recstar_var(1, 3); + rpr_recstar_var +----------------- + 0 +(1 row) + +DROP FUNCTION rpr_recstar_var(int, int); +-- the same spelling outside a DEFINE condition, which is what the two above +-- have to agree with: +CREATE FUNCTION rpr_recstar_plain(x int, y int) RETURNS text +LANGUAGE plpgsql AS $$ +DECLARE + rec rpr_pair; +BEGIN + rec := ROW(x, y); + RETURN (SELECT ROW(rec.*)::text FROM rpr_rec LIMIT 1); +END +$$; +SELECT rpr_recstar_plain(1, 2); + rpr_recstar_plain +------------------- + (1,2) +(1 row) + +DROP FUNCTION rpr_recstar_plain(int, int); +-- A FROM-clause relation is not such a name, inside such a function or out. +CREATE FUNCTION rpr_relstar() RETURNS bigint +LANGUAGE plpgsql AS $$ +#variable_conflict use_variable +DECLARE + rec rpr_pair := ROW(1, 2); + n bigint; +BEGIN + SELECT count(*) OVER w INTO n FROM rpr_rec + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS ROW(rpr_rec.*)::text IS NOT NULL) + LIMIT 1; + RETURN n; +END +$$; +SELECT rpr_relstar(); +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 6: DEFINE A AS ROW(rpr_rec.*)::text IS NOT NULL) + ^ +HINT: A DEFINE condition may reference individual columns only. +QUERY: SELECT count(*) OVER w FROM rpr_rec + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS ROW(rpr_rec.*)::text IS NOT NULL) + LIMIT 1 +CONTEXT: PL/pgSQL function rpr_relstar() line 7 at SQL statement +DROP FUNCTION rpr_relstar(); +DROP TABLE rpr_rec; +DROP TYPE rpr_pair; +-- An outer range variable used as a function-call qualifier reaches DEFINE as +-- a FuncExpr rather than a Var, so the level the qualifier resolved at, not +-- the shape of the resulting node, is what identifies the outer reference. +CREATE TABLE rpr_outer (threshold int); +INSERT INTO rpr_outer VALUES (95); +CREATE FUNCTION rpr_rowfn(rpr_outer) RETURNS int LANGUAGE sql AS 'SELECT 1'; +SELECT * FROM rpr_outer AS o, +LATERAL ( + SELECT price FROM rpr_price + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS o.rpr_rowfn > 0 + ) +) s; +ERROR: cannot use outer query column in DEFINE clause +LINE 9: DEFINE A AS o.rpr_rowfn > 0 + ^ +DROP FUNCTION rpr_rowfn(rpr_outer); +DROP TABLE rpr_outer; +-- DEFINE rejects a schema-qualified column reference (three or more name +-- parts) once it resolves; the qualified form itself is not allowed. +-- (rpr_price is a temp table, so it is qualified with pg_temp here.) +-- 3-part (schema.table.column): +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS pg_temp.rpr_price.price > 0 +); +ERROR: qualified expression "pg_temp.rpr_price.price" is not allowed in DEFINE clause +LINE 7: DEFINE A AS pg_temp.rpr_price.price > 0 + ^ +-- whole-row variant (schema.table.*): +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS (pg_temp.rpr_price.*) IS NOT NULL +); +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 7: DEFINE A AS (pg_temp.rpr_price.*) IS NOT NULL + ^ +HINT: A DEFINE condition may reference individual columns only. +-- A two-part table-qualified whole-row reference is rejected as well, and by +-- the whole-row check rather than by a qualifier rule: the error names the +-- whole-row reference, not the qualifier. +-- 2-part (table.*): +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS (rpr_price.*) IS NOT NULL +); +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 7: DEFINE A AS (rpr_price.*) IS NOT NULL + ^ +HINT: A DEFINE condition may reference individual columns only. +-- The form decides before the qualifier is looked up, so a misspelled table +-- name is reported as the whole-row reference it is written as, not as a +-- missing FROM-clause entry: +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS (stok.*) IS NOT NULL +); +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 7: DEFINE A AS (stok.*) IS NOT NULL + ^ +HINT: A DEFINE condition may reference individual columns only. +-- and the same through a row constructor: +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS ROW(stok.*) IS NOT NULL +); +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 7: DEFINE A AS ROW(stok.*) IS NOT NULL + ^ +HINT: A DEFINE condition may reference individual columns only. +-- A row constructor reaches the same references through +-- transformExpressionList(), whose star expansion binds them by RTE into +-- individual column Vars, past every check. DEFINE skips it. +-- ROW(schema.table.*): +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS ROW(pg_temp.rpr_price.*) IS NOT NULL +); +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 7: DEFINE A AS ROW(pg_temp.rpr_price.*) IS NOT NULL + ^ +HINT: A DEFINE condition may reference individual columns only. +-- ROW(table.*): +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS ROW(rpr_price.*) IS NOT NULL +); +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 7: DEFINE A AS ROW(rpr_price.*) IS NOT NULL + ^ +HINT: A DEFINE condition may reference individual columns only. +-- the ROW keyword is optional, so the bare constructor needs the same +-- treatment: +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS (rpr_price.*, 1) IS NOT NULL +); +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 7: DEFINE A AS (rpr_price.*, 1) IS NOT NULL + ^ +HINT: A DEFINE condition may reference individual columns only. +-- redundant parentheses are not a way around it: +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS ROW((rpr_price.*)) IS NOT NULL +); +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 7: DEFINE A AS ROW((rpr_price.*)) IS NOT NULL + ^ +HINT: A DEFINE condition may reference individual columns only. +-- a pattern variable qualifier is a separate class of rejection: +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS ROW(A.*) IS NOT NULL +); +ERROR: pattern variable qualified expression "a.*" is not supported in DEFINE clause +LINE 7: DEFINE A AS ROW(A.*) IS NOT NULL + ^ +-- The plain two-part form is the one the standard writes its DEFINE examples +-- with, and it is decided on the qualifier alone, before resolution. +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS A.price > 100 +); +ERROR: pattern variable qualified expression "a.price" is not supported in DEFINE clause +LINE 7: DEFINE A AS A.price > 100 + ^ +-- Deciding on the qualifier alone means a pattern variable takes a name a +-- range variable would otherwise answer to: the rejection names the pattern +-- variable, not the alias, even though "a" is a live alias here. +SELECT price FROM rpr_price AS a +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS a.price > 100 +); +ERROR: pattern variable qualified expression "a.price" is not supported in DEFINE clause +LINE 7: DEFINE A AS a.price > 100 + ^ +-- Unlike the pattern variable and whole-row rejections, the range variable +-- and schema-qualified rejections classify the reference only after it +-- resolves, so a misspelled column keeps the diagnosis and the suggestion it +-- gets anywhere else. Firing on the qualifier alone would report a range +-- variable problem before the rest of the name was looked at. +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS rpr_price.pric > 0 +); +ERROR: column rpr_price.pric does not exist +LINE 7: DEFINE A AS rpr_price.pric > 0 + ^ +HINT: Perhaps you meant to reference the column "rpr_price.price". +SELECT price FROM rpr_price +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS pg_temp.rpr_price.pric > 0 +); +ERROR: column rpr_price.pric does not exist +LINE 7: DEFINE A AS pg_temp.rpr_price.pric > 0 + ^ +HINT: Perhaps you meant to reference the column "rpr_price.price". +-- the same typo outside a DEFINE clause, for comparison: +SELECT price FROM rpr_price WHERE rpr_price.pric > 0; +ERROR: column rpr_price.pric does not exist +LINE 1: SELECT price FROM rpr_price WHERE rpr_price.pric > 0; + ^ +HINT: Perhaps you meant to reference the column "rpr_price.price". +-- Retrying an unresolved column as a function call on the whole row builds a +-- whole-row reference the query does not contain. That must not be reported +-- as one, and must not let the reference through either: rpr_tag(rpr_stock) +-- below resolves, so the retry succeeds and the result is rejected by the +-- qualifier rules rather than by the whole-row check. +CREATE FUNCTION rpr_tag(rpr_stock) RETURNS int + LANGUAGE sql IMMUTABLE AS $$SELECT 1$$; +SELECT price FROM rpr_stock +WINDOW w AS ( + PARTITION BY part_id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS rpr_stock.rpr_tag > 0 +); +ERROR: range variable qualified expression "rpr_stock.rpr_tag" is not allowed in DEFINE clause +LINE 7: DEFINE A AS rpr_stock.rpr_tag > 0 + ^ +SELECT price FROM rpr_stock +WINDOW w AS ( + PARTITION BY part_id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS public.rpr_stock.rpr_tag > 0 +); +ERROR: qualified expression "public.rpr_stock.rpr_tag" is not allowed in DEFINE clause +LINE 7: DEFINE A AS public.rpr_stock.rpr_tag > 0 + ^ +DROP FUNCTION rpr_tag(rpr_stock); +-- A JOIN USING alias has no whole-row Var of its own, so the same retry +-- expands it to a row constructor instead. The retry carries no star, so +-- DEFINE lets it through to that arm. +CREATE TEMP TABLE rpr_j_l (x int, y int); +CREATE TEMP TABLE rpr_j_r (x int, z int); +SELECT count(*) OVER w FROM (rpr_j_l JOIN rpr_j_r USING (x)) j +WINDOW w AS ( + ORDER BY x + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS j.yy > 0 +); +ERROR: column j.yy does not exist +LINE 6: DEFINE A AS j.yy > 0 + ^ +SELECT count(*) OVER w FROM (rpr_j_l JOIN rpr_j_r USING (x)) j +WINDOW w AS ( + ORDER BY x + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS j.y > 0 +); +ERROR: range variable qualified expression "j.y" is not allowed in DEFINE clause +LINE 6: DEFINE A AS j.y > 0 + ^ +SELECT count(*) OVER w FROM (rpr_j_l JOIN rpr_j_r USING (x)) j +WINDOW w AS ( + ORDER BY x + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS (j.*) IS NOT NULL +); +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 6: DEFINE A AS (j.*) IS NOT NULL + ^ +HINT: A DEFINE condition may reference individual columns only. +DROP TABLE rpr_j_l, rpr_j_r; +-- A row constructor over plain columns is unaffected. +SELECT company, tdate, count(*) OVER w AS cnt +FROM rpr_price +WHERE company = 'company2' AND tdate <= '2023-07-03' +WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A+) + DEFINE A AS ROW(price, price) IS NOT NULL +); + company | tdate | cnt +----------+------------+----- + company2 | 07-01-2023 | 3 + company2 | 07-02-2023 | 0 + company2 | 07-03-2023 | 0 +(3 rows) + +-- A restriction on a DEFINE condition covers the whole condition, including a +-- clause nested inside it that carries an expression kind of its own. FILTER +-- and an aggregate's ORDER BY are the two such clauses a condition can reach, +-- and each case below is preceded by the same reference written directly in +-- the condition, which is the rejection the nested one has to keep. +CREATE TEMP TABLE rpr_nest_i (i int, v int); +CREATE TEMP TABLE rpr_nest_o (v int); +-- an outer query column: +SELECT o.v, (SELECT count(*) OVER w FROM rpr_nest_i inn + WINDOW w AS ( + ORDER BY inn.i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS o.v > 0) + LIMIT 1) +FROM rpr_nest_o o GROUP BY o.v; +ERROR: cannot use outer query column in DEFINE clause +LINE 6: DEFINE A AS o.v > 0) + ^ +SELECT o.v, (SELECT count(*) OVER w FROM rpr_nest_i inn + WINDOW w AS ( + ORDER BY inn.i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS count(*) FILTER (WHERE o.v > 0) > 0) + LIMIT 1) +FROM rpr_nest_o o GROUP BY o.v; +ERROR: cannot use outer query column in DEFINE clause +LINE 6: ... DEFINE A AS count(*) FILTER (WHERE o.v > 0) >... + ^ +SELECT o.v, (SELECT count(*) OVER w FROM rpr_nest_i inn + WINDOW w AS ( + ORDER BY inn.i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > count(1 ORDER BY o.v)) + LIMIT 1) +FROM rpr_nest_o o GROUP BY o.v; +ERROR: cannot use outer query column in DEFINE clause +LINE 6: DEFINE A AS v > count(1 ORDER BY o.v)) + ^ +-- a pattern variable qualifier, where an outer alias answers to the same +-- name, so letting it through would silently read the outer column instead: +SELECT count(*) OVER w FROM rpr_nest_i A +WINDOW w AS ( + ORDER BY A.i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS A.v > 0 +); +ERROR: pattern variable qualified expression "a.v" is not supported in DEFINE clause +LINE 6: DEFINE A AS A.v > 0 + ^ +SELECT A.v, (SELECT count(*) OVER w FROM rpr_nest_i inn + WINDOW w AS ( + ORDER BY inn.i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS percentile_disc(0.5) + WITHIN GROUP (ORDER BY A.v) > 0) + LIMIT 1) +FROM rpr_nest_i A GROUP BY A.v; +ERROR: pattern variable qualified expression "a.v" is not supported in DEFINE clause +LINE 7: ... WITHIN GROUP (ORDER BY A.v) > 0) + ^ +-- a whole-row reference, which a row constructor would expand by RTE: +SELECT (SELECT count(*) OVER w FROM rpr_nest_i inn + WINDOW w AS ( + ORDER BY inn.i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS ROW(o.*)::text IS NOT NULL) + LIMIT 1) +FROM rpr_nest_o o; +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 6: DEFINE A AS ROW(o.*)::text IS NOT NULL) + ^ +HINT: A DEFINE condition may reference individual columns only. +SELECT (SELECT count(*) OVER w FROM rpr_nest_i inn + WINDOW w AS ( + ORDER BY inn.i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS percentile_disc(0.5) + WITHIN GROUP (ORDER BY ROW(o.*)::text) IS NOT NULL) + LIMIT 1) +FROM rpr_nest_o o GROUP BY o.v; +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 7: ... WITHIN GROUP (ORDER BY ROW(o.*)::text... + ^ +HINT: A DEFINE condition may reference individual columns only. +-- a subquery: +SELECT count(*) OVER w FROM rpr_nest_i inn +WINDOW w AS ( + ORDER BY inn.i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > (SELECT 1) +); +ERROR: cannot use subquery in DEFINE expression +LINE 6: DEFINE A AS v > (SELECT 1) + ^ +SELECT count(*) OVER w FROM rpr_nest_i inn +WINDOW w AS ( + ORDER BY inn.i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS count(*) FILTER (WHERE (SELECT 1) = 1) > 0 +); +ERROR: cannot use subquery in DEFINE expression +LINE 6: DEFINE A AS count(*) FILTER (WHERE (SELECT 1) = 1) > 0 + ^ +-- The nesting itself is not what is rejected: with nothing forbidden inside +-- it, the aggregate carrying the FILTER is what the condition trips over. +SELECT count(*) OVER w FROM rpr_nest_i inn +WINDOW w AS ( + ORDER BY inn.i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS count(*) FILTER (WHERE v > 0) > 0 +); +ERROR: aggregate functions are not allowed in DEFINE +LINE 6: DEFINE A AS count(*) FILTER (WHERE v > 0) > 0 + ^ +DROP TABLE rpr_nest_i, rpr_nest_o; +-- +-- 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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_price +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) + +-- Bare pass-by-reference column rather than a cast: the two navigations land +-- on different rows, so the second fetch frees the tuple the first result +-- points into. The casts above allocate a fresh datum and never reach that; +-- only EEOP_RPR_NAV_RESTORE's datumCopy keeps this one alive. +CREATE TEMP TABLE rpr_byref (id int, s text); +INSERT INTO rpr_byref VALUES + (1, 'aaa'), (2, 'bbb'), (3, 'ccc'), (4, 'bbb'), (5, 'ddd'), (6, 'aaa'); +SELECT id, s, first_value(s) OVER w AS fs, last_value(s) OVER w AS ls, + count(*) OVER w AS cnt +FROM rpr_byref +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(s, 1) > PREV(s, 2) +); + id | s | fs | ls | cnt +----+-----+-----+-----+----- + 1 | aaa | | | 0 + 2 | bbb | bbb | bbb | 3 + 3 | ccc | | | 0 + 4 | bbb | | | 0 + 5 | ddd | ddd | aaa | 2 + 6 | aaa | | | 0 +(6 rows) + +DROP TABLE rpr_byref; +-- 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_cycle (id int, val int); +INSERT INTO rpr_nav_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle: (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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle +(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_cycle 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_cycle 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_cycle 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) + +-- Inner offset overflows int64. The cases above all overflow while applying +-- the outer offset; these two overflow while computing the inner position, +-- before any outer offset is applied. A is false at the first row, so no +-- match starts there and match_start is at least 1 wherever B is evaluated; +-- match_start + INT64_MAX then overflows. With match_start 0 the sum still +-- fits and the clamp below it answers instead. +SELECT id, val, count(*) OVER w FROM rpr_nav_cycle WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS val > 10, B AS FIRST(val, 9223372036854775807) IS NULL +); + id | val | count +----+-----+------- + 1 | 10 | 0 + 2 | 20 | 5 + 3 | 30 | 0 + 4 | 10 | 0 + 5 | 50 | 0 + 6 | 10 | 0 +(6 rows) + +-- The same overflow reached through a compound navigation, where it happens +-- before the outer offset is applied +SELECT id, val, count(*) OVER w FROM rpr_nav_cycle WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS val > 10, B AS NEXT(FIRST(val, 9223372036854775807), 1) IS NULL +); + id | val | count +----+-----+------- + 1 | 10 | 0 + 2 | 20 | 5 + 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle 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_cycle; +-- +-- SKIP TO / Backtracking / Frame boundary +-- +-- match everything +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w + FROM rpr_price + 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 rpr_price +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 rpr_price + 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 rpr_price + 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 rpr_price + 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 rpr_price +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 rpr_price +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 rpr_price +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 rpr_join_left (i int, v1 int); +CREATE TEMP TABLE rpr_join_right (j int, v2 int); +INSERT INTO rpr_join_left VALUES(1,10); +INSERT INTO rpr_join_left VALUES(1,11); +INSERT INTO rpr_join_left VALUES(1,12); +INSERT INTO rpr_join_right VALUES(2,10); +INSERT INTO rpr_join_right VALUES(2,11); +INSERT INTO rpr_join_right VALUES(2,12); +SELECT * FROM rpr_join_left, rpr_join_right WHERE rpr_join_left.v1 <= 11 AND rpr_join_right.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 rpr_join_left, rpr_join_right +WINDOW w AS ( + PARTITION BY rpr_join_left.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 rpr_price 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 rpr_prev_multicol (id INTEGER, i SERIAL, j INTEGER); +INSERT INTO rpr_prev_multicol(id, j) SELECT 1, g*2 FROM generate_series(1, 10) AS g; +SELECT id, i, j, count(*) OVER w + FROM rpr_prev_multicol + 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 rpr_price + 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 rpr_price +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 must start at the end of the reduced frame, +-- not the full frame, so the later non-NULL row 4 is not returned. +-- +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 rpr_stock_null (company TEXT, tdate DATE, price INTEGER); +INSERT INTO rpr_stock_null VALUES ('c1', '2023-07-01', 100); +INSERT INTO rpr_stock_null VALUES ('c1', '2023-07-02', NULL); -- NULL in middle +INSERT INTO rpr_stock_null VALUES ('c1', '2023-07-03', 200); +INSERT INTO rpr_stock_null VALUES ('c1', '2023-07-04', 150); +SELECT company, tdate, price, count(*) OVER w AS match_count +FROM rpr_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..560e89ac611 --- /dev/null +++ b/src/test/regress/expected/rpr_base.out @@ -0,0 +1,13104 @@ +-- ============================================================ +-- 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; + 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 rpr_stock_price ( + dt DATE, + symbol TEXT, + price NUMERIC, + volume INT +); +INSERT INTO rpr_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 rpr_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 +); + 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 rpr_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 +); + 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 rpr_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 +); + 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 rpr_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 +); + 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 rpr_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 +); + 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 +); + 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 +); + 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 +); + 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 +); + id | cnt +----+----- + 1 | 0 + 2 | 0 +(2 rows) + +-- Implicit cast to boolean via custom type +CREATE TYPE rpr_truthyint AS (v int); +CREATE FUNCTION rpr_truthyint_to_bool(rpr_truthyint) RETURNS boolean AS $$ + SELECT ($1).v <> 0; +$$ LANGUAGE SQL IMMUTABLE STRICT; +CREATE CAST (rpr_truthyint AS boolean) + WITH FUNCTION rpr_truthyint_to_bool(rpr_truthyint) + AS ASSIGNMENT; +CREATE TABLE rpr_coerce (id int, val rpr_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 (rpr_truthyint AS boolean); +DROP FUNCTION rpr_truthyint_to_bool(rpr_truthyint); +DROP TYPE rpr_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 rpr_boolish AS boolean; +CREATE TABLE rpr_domain (id int, flag rpr_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 +); + id | cnt +----+----- + 1 | 1 + 2 | 0 + 3 | 1 +(3 rows) + +DROP TABLE rpr_domain; +DROP DOMAIN rpr_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) +); + 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 +); + 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 +); +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 +); + id | v | cnt +----+---+----- + 1 | 1 | 0 + 2 | 2 | 0 + 3 | 3 | 0 +(3 rows) + +DROP TABLE rpr_lazy; +-- A navigation result carrying a collation. Both forms make the parser ask +-- for the collation of the navigation node itself rather than of its argument. +CREATE TABLE rpr_navcoll (id INT, s TEXT); +INSERT INTO rpr_navcoll VALUES (1, 'a'), (2, 'B'), (3, 'c'); +-- COLLATE applied to the navigation result +SELECT id, s, count(*) OVER w AS cnt +FROM rpr_navcoll +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(s) COLLATE "C" < s +); + id | s | cnt +----+---+----- + 1 | a | 0 + 2 | B | 0 + 3 | c | 1 +(3 rows) + +-- Simple CASE over the navigation result: the placeholder takes its collation +-- from the tested expression +SELECT id, s, count(*) OVER w AS cnt +FROM rpr_navcoll +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS CASE PREV(s) WHEN 'a' THEN true ELSE false END +); + id | s | cnt +----+---+----- + 1 | a | 0 + 2 | B | 1 + 3 | c | 0 +(3 rows) + +DROP TABLE rpr_navcoll; +-- A system column in a DEFINE expression. Only the scan reads it as a system +-- attribute; it reaches the expression as an outer Var, and navigation still +-- applies to it: PREV(ctid) is the previous row of the match, not this row. +CREATE TABLE rpr_navsys (i INT); +INSERT INTO rpr_navsys SELECT generate_series(1, 5); +SELECT i, count(*) OVER w AS cnt +FROM rpr_navsys +WINDOW w AS ( + ORDER BY i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(ctid) IS DISTINCT FROM ctid +); + i | cnt +---+----- + 1 | 5 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +DROP TABLE rpr_navsys; +-- ============================================================ +-- 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) + +-- frame must start at CURRENT ROW, not UNBOUNDED PRECEDING +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: unsupported frame for row pattern recognition +LINE 5: ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + ^ +DETAIL: The frame must be "ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING" or "ROWS BETWEEN CURRENT ROW AND offset FOLLOWING". +-- 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 with row pattern recognition +LINE 6: EXCLUDE CURRENT ROW + ^ +-- 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 with row pattern recognition +LINE 6: EXCLUDE GROUP + ^ +-- 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 with row pattern recognition +LINE 6: EXCLUDE TIES + ^ +-- Both rules broken at once. The frame shape is settled first, so the +-- report names the shape; the EXCLUDE clause may not survive the rewrite. +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + EXCLUDE TIES + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: unsupported frame for row pattern recognition +LINE 5: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ^ +DETAIL: The frame must be "ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING" or "ROWS BETWEEN CURRENT ROW AND offset FOLLOWING". +-- 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: unsupported frame for row pattern recognition +LINE 5: RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWIN... + ^ +DETAIL: The frame must be "ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING" or "ROWS BETWEEN CURRENT ROW AND offset FOLLOWING". +-- 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: unsupported frame for row pattern recognition +LINE 5: GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI... + ^ +DETAIL: The frame must be "ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING" or "ROWS BETWEEN CURRENT ROW AND offset FOLLOWING". +-- omitting the frame clause leaves the standard default, RANGE BETWEEN +-- UNBOUNDED PRECEDING AND CURRENT ROW, which breaks three of the rules at +-- once. One report, stating what the frame has to be. +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: unsupported frame for row pattern recognition +LINE 3: WINDOW w AS ( + ^ +DETAIL: The frame must be "ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING" or "ROWS BETWEEN CURRENT ROW AND offset FOLLOWING". +-- frame must start at CURRENT ROW, not offset PRECEDING +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: unsupported frame for row pattern recognition +LINE 5: ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING + ^ +DETAIL: The frame must be "ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING" or "ROWS BETWEEN CURRENT ROW AND offset FOLLOWING". +-- frame must start at CURRENT ROW, not offset FOLLOWING +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); +ERROR: unsupported frame for row pattern recognition +LINE 5: ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING + ^ +DETAIL: The frame must be "ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING" or "ROWS BETWEEN CURRENT ROW AND offset FOLLOWING". +-- 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 +); +ERROR: unsupported frame for row pattern recognition +LINE 5: ROWS BETWEEN CURRENT ROW AND CURRENT ROW + ^ +DETAIL: The frame must be "ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING" or "ROWS BETWEEN CURRENT ROW AND 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 +); +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 +); +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 +); + 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 +); + 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 +); + 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 +); +ERROR: unsupported frame for row pattern recognition +LINE 5: RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING + ^ +DETAIL: The frame must be "ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING" or "ROWS BETWEEN CURRENT ROW AND offset FOLLOWING". +-- 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 +); +ERROR: unsupported frame for row pattern recognition +LINE 5: GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING + ^ +DETAIL: The frame must be "ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING" or "ROWS BETWEEN CURRENT ROW AND offset FOLLOWING". +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 +); + 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 +); +ERROR: unsupported frame for row pattern recognition +LINE 6: RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING + ^ +DETAIL: The frame must be "ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING" or "ROWS BETWEEN CURRENT ROW AND offset FOLLOWING". +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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); +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 +); +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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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}. A fixed count has no shorter match, +-- so the result is the same either way. +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. +-- A first token ending in "|" is a quantifier plus the alternation operator, +-- so what follows it has to be a pattern and an Op never is one. The report +-- names the alternation rather than the pair: "A* ?" is accepted, so naming +-- that pair would describe something 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: alternation operator "|" requires a pattern on both sides +LINE 6: PATTERN (A *| ?) + ^ +-- the same for a glued two-character quantifier, where naming the pair would +-- have to invent "*?" out of "*?|" +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: alternation operator "|" requires a pattern on both sides +LINE 6: PATTERN (A *?| ??) + ^ +-- 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: invalid 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 +-- A constant subexpression of the argument is folded away, so the pattern +-- does not recompute it per row. +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(val + 2 * 3) > 0); + id | cnt +----+----- + 1 | 0 + 2 | 4 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +-- Folding a constant subexpression can raise where the whole argument would +-- not have: unlike val / 0 above, 1 / 0 does not depend on the row, so it is +-- reached at plan time even on the row PREV misses on. +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 + 1 / 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) + +-- Pulling up the VALUES substitutes 10 for v, which is not what the column +-- stood for under a navigation: the argument reads the row the navigation +-- lands on, not this one. The replacement is wrapped in a PlaceHolderVar +-- rather than folded through. +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) + +-- That wrapping is what keeps this one from raising: the divisor is constant +-- but the dividend is not folded through, so the division stands until +-- execution, where PREV has no row to navigate to and never reaches it. +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); + id | cnt +----+----- + 1 | 0 +(1 row) + +-- A pulled-up subquery and a function RTE that folded to a constant reach a +-- navigation argument the same way, so both are wrapped as well. +SELECT id, count(*) OVER w AS cnt +FROM (SELECT 1 AS id, 10 AS v) t +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A) DEFINE A AS PREV(v / 0) > 0); + id | cnt +----+----- + 1 | 0 +(1 row) + +SELECT count(*) OVER w AS cnt +FROM abs(-10) AS v +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A) DEFINE A AS PREV(v / 0) > 0); + cnt +----- + 0 +(1 row) + +-- Only the argument is protected. One level outside the navigation the same +-- column is replaced and folded as it is anywhere else, and the division +-- raises at plan time -- as it does for the same WHERE clause over the same +-- one-row VALUES, with no pattern in sight. +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 v / 0 > 0); +ERROR: division by zero +-- A replacement that still depends on the row is left unwrapped, because it +-- is what the column meant at whichever row the navigation lands on. +SELECT id, count(*) OVER w AS cnt +FROM (SELECT id, val + 1 AS v FROM rpr_nav) t +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v) > 0); + id | cnt +----+----- + 1 | 0 + 2 | 4 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +-- Nesting: the inner navigation's argument is below the outer one's, so the +-- column there is wrapped too. +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(LAST(v / 0, 1), 2) > 0); + id | cnt +----+----- + 1 | 0 +(1 row) + +-- eval_const_expressions() must perform a few rewrites on every expression it +-- is handed -- a CollateExpr becomes a RelabelType, named arguments become +-- positional, omitted defaults are inserted -- and the executor depends on +-- all three, so they are mandatory, not optimizations. Each of the three +-- below reaches the executor only if those rewrites reach inside a navigation +-- argument, and each returns what the same expression one level outside the +-- navigation returns. +CREATE TABLE rpr_nav_txt (id int, s text); +INSERT INTO rpr_nav_txt VALUES (1, 'b'), (2, 'c'), (3, 'a'); +CREATE FUNCTION rpr_nav_named(a int, b int) RETURNS int + LANGUAGE sql IMMUTABLE AS 'SELECT $1 * 10 + $2'; +CREATE FUNCTION rpr_nav_dflt(a int, b int DEFAULT 100) RETURNS int + LANGUAGE sql IMMUTABLE AS 'SELECT $2'; +-- COLLATE under a navigation: the executor has no CollateExpr step, so the +-- RelabelType rewrite has to reach here. +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav_txt +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(s COLLATE "C") > 'a'); + id | cnt +----+----- + 1 | 0 + 2 | 2 + 3 | 0 +(3 rows) + +-- Named arguments under a navigation: the executor has no NamedArgExpr step. +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(rpr_nav_named(b => 7, a => val)) > 0); + id | cnt +----+----- + 1 | 0 + 2 | 4 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +-- An omitted default under a navigation: without the insertion the call is +-- initialized with one fewer argument than the callee reads. +SELECT id, count(*) OVER w AS cnt +FROM rpr_nav +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(rpr_nav_dflt(val)) = 100); + id | cnt +----+----- + 1 | 0 + 2 | 4 + 3 | 0 + 4 | 0 + 5 | 0 +(5 rows) + +DROP FUNCTION rpr_nav_dflt(int, int); +DROP FUNCTION rpr_nav_named(int, int); +DROP TABLE rpr_nav_txt; +-- A navigation offset is resolved once at the top of the scan, before any +-- input row has been read, so it must not be matched to the window input the +-- way the navigated argument is. These two spell the offset the same as a +-- window ORDER BY key and as a GROUP BY expression, which is what makes the +-- match available. +CREATE TABLE rpr_navoff (id int, val int); +INSERT INTO rpr_navoff VALUES (1, 10), (2, 20), (3, 15), (4, 30), (5, 5); +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_navoff +WINDOW w AS (ORDER BY (extract(hour from localtimestamp)::int * 0 + 1), id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val, (extract(hour from localtimestamp)::int * 0 + 1))); + id | val | cnt +----+-----+----- + 1 | 10 | 2 + 2 | 20 | 0 + 3 | 15 | 2 + 4 | 30 | 0 + 5 | 5 | 0 +(5 rows) + +-- Control: an offset that matches nothing in the window input. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_navoff +WINDOW w AS (ORDER BY (extract(hour from localtimestamp)::int * 0 + 1), id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val, (extract(hour from localtimestamp)::int * 0 + 2))); + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 3 + 3 | 15 | 0 + 4 | 30 | 0 + 5 | 5 | 0 +(5 rows) + +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_navoff +GROUP BY GROUPING SETS ((id, val, ((random() * 0)::bigint + 1)), + (id, ((random() * 0)::bigint + 1))) +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val, (random() * 0)::bigint + 1)) +ORDER BY id, val; + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 1 | | 0 + 2 | 20 | 0 + 2 | | 0 + 3 | 15 | 0 + 3 | | 0 + 4 | 30 | 0 + 4 | | 0 + 5 | 5 | 0 + 5 | | 0 +(10 rows) + +DROP TABLE rpr_navoff; +-- 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) +); + 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 +); + 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) +); + 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) +); +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) +); +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) +); + 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) +); + 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) +); + 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 rpr_nav_rows (g text, id int, val int); +INSERT INTO rpr_nav_rows 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 rpr_nav_rows; +ERROR: function prev(integer) does not exist +LINE 1: SELECT prev(val) FROM rpr_nav_rows; + ^ +DETAIL: There is no function of that name. +SELECT next(val) FROM rpr_nav_rows; +ERROR: function next(integer) does not exist +LINE 1: SELECT next(val) FROM rpr_nav_rows; + ^ +DETAIL: There is no function of that name. +SELECT prev(val, 2) FROM rpr_nav_rows; +ERROR: function prev(integer, integer) does not exist +LINE 1: SELECT prev(val, 2) FROM rpr_nav_rows; + ^ +DETAIL: There is no function of that name. +SELECT next(val, 2) FROM rpr_nav_rows; +ERROR: function next(integer, integer) does not exist +LINE 1: SELECT next(val, 2) FROM rpr_nav_rows; + ^ +DETAIL: There is no function of that name. +SELECT first(val) FROM rpr_nav_rows; +ERROR: function first(integer) does not exist +LINE 1: SELECT first(val) FROM rpr_nav_rows; + ^ +DETAIL: There is no function of that name. +SELECT last(val) FROM rpr_nav_rows; +ERROR: function last(integer) does not exist +LINE 1: SELECT last(val) FROM rpr_nav_rows; + ^ +DETAIL: There is no function of that name. +SELECT first(val, 1) FROM rpr_nav_rows; +ERROR: function first(integer, integer) does not exist +LINE 1: SELECT first(val, 1) FROM rpr_nav_rows; + ^ +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 rpr_nav_rows; +ERROR: function pg_catalog.prev(integer) does not exist +LINE 1: SELECT pg_catalog.prev(val) FROM rpr_nav_rows; + ^ +-- 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 the subquery is planned and the +-- check reaches its DEFINE before anything settles that no OVER references +-- the window, just as for an unreferenced window at the top level +SELECT id FROM ( + SELECT id FROM rpr_nav_rows + 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 +-- ERROR: an OVER referencing the window keeps the subquery without OFFSET 0, +-- and its DEFINE is checked the same way +SELECT id, c FROM ( + SELECT id, count(*) OVER w AS c FROM rpr_nav_rows + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS random() > 0.5)) sub; +ERROR: DEFINE clause cannot contain volatile functions +-- The same query with WHERE false makes the subquery rel dummy, so the +-- planner never plans it and nothing looks at its DEFINE +SELECT id, c FROM ( + SELECT id, count(*) OVER w AS c FROM rpr_nav_rows + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS random() > 0.5)) sub +WHERE false; + id | c +----+--- +(0 rows) + +-- The window runs, but the volatile is in a dead CASE arm that folds away +-- before the check, so nothing volatile is left for the check to find +SELECT id, count(*) OVER w AS c FROM rpr_nav_rows + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS CASE WHEN false THEN random()::int > 0 + ELSE val > 5 END) +ORDER BY id; + id | c +----+--- + 1 | 5 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 +(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 rpr_nav_rows + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS random() > 0.5) + UNION ALL + SELECT id FROM rpr_nav_rows) 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_nav_rows + 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 rpr_navns_nav AS + SELECT id, count(*) OVER w AS cnt FROM rpr_nav_rows + 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 rpr_navns_fn AS + SELECT id, count(*) OVER w AS cnt FROM rpr_nav_rows + 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('rpr_navns_nav'); + pg_get_viewdef +-------------------------------------------------------------------------------------------- + SELECT id, + + count(*) OVER w AS cnt + + FROM rpr_nav_rows + + 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('rpr_navns_fn'); + pg_get_viewdef +-------------------------------------------------------------------------------------------- + SELECT id, + + count(*) OVER w AS cnt + + FROM rpr_nav_rows + + 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 rpr_navns_nav, rpr_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 rpr_navns_fn_last AS + SELECT id, count(*) OVER w AS cnt FROM rpr_nav_rows + 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('rpr_navns_fn_last'); + pg_get_viewdef +-------------------------------------------------------------------------------------------- + SELECT id, + + count(*) OVER w AS cnt + + FROM rpr_nav_rows + + 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 rpr_navns_fn_last; +DROP FUNCTION rpr_navns.last(integer); +-- Attribute notation is never a navigation call; it resolves to a field or +-- to an ordinary function +CREATE TYPE rpr_navns_pair AS (first int, last int); +CREATE TABLE rpr_composite_rows (id int, p rpr_navns_pair); +INSERT INTO rpr_composite_rows VALUES (1, (10, 20)), (2, (30, 40)); +SELECT (p).last FROM rpr_composite_rows ORDER BY id; + last +------ + 20 + 40 +(2 rows) + +SELECT count(*) OVER w FROM rpr_composite_rows + 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 rpr_composite_rows + 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 rpr_nav_rows + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 in PATTERN 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 that are not dropped explicitly are +-- intentionally left in place 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 names 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 rpr_serial views above, 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) + +-- A DEFINE clause can only name a column without a qualifier, so the name has +-- to resolve exactly as printed. When another relation of the query acquires +-- a column of that name, the deparser pushes the newcomer aside with a column +-- alias list, the same way it protects a column merged by USING. +CREATE TABLE rpr_pin (id INT, val INT); +CREATE TABLE rpr_pin_other (id INT); +INSERT INTO rpr_pin VALUES (1, 10), (2, 20), (3, 15); +INSERT INTO rpr_pin_other VALUES (1), (2), (3); +CREATE VIEW rpr_pin_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_pin, rpr_pin_other +WHERE rpr_pin.id = rpr_pin_other.id +WINDOW w AS (ORDER BY rpr_pin.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0); +-- names reached through a navigation operation are pinned too +CREATE VIEW rpr_pin_nav_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_pin, rpr_pin_other +WHERE rpr_pin.id = rpr_pin_other.id +WINDOW w AS (ORDER BY rpr_pin.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(val) < val); +-- no collision yet, so no column alias list +SELECT pg_get_viewdef('rpr_pin_v'::regclass, true); + pg_get_viewdef +------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_pin, + + rpr_pin_other + + WHERE rpr_pin.id = rpr_pin_other.id + + WINDOW w AS (ORDER BY rpr_pin.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS val > 0); +(1 row) + +ALTER TABLE rpr_pin_other ADD COLUMN val INT; +SELECT pg_get_viewdef('rpr_pin_v'::regclass, true); + pg_get_viewdef +------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_pin, + + rpr_pin_other rpr_pin_other(id, val_1) + + WHERE rpr_pin.id = rpr_pin_other.id + + WINDOW w AS (ORDER BY rpr_pin.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS val > 0); +(1 row) + +SELECT pg_get_viewdef('rpr_pin_nav_v'::regclass, true); + pg_get_viewdef +------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_pin, + + rpr_pin_other rpr_pin_other(id, val_1) + + WHERE rpr_pin.id = rpr_pin_other.id + + WINDOW w AS (ORDER BY rpr_pin.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS PREV(val) < val); +(1 row) + +-- and the deparsed text builds an identical view +CREATE VIEW rpr_pin_v2 AS + SELECT count(*) OVER w AS cnt + FROM rpr_pin, + rpr_pin_other rpr_pin_other(id, val_1) + WHERE rpr_pin.id = rpr_pin_other.id + WINDOW w AS (ORDER BY rpr_pin.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS val > 0); +SELECT pg_get_viewdef('rpr_pin_v'::regclass, true) + = pg_get_viewdef('rpr_pin_v2'::regclass, true) AS identical; + identical +----------- + t +(1 row) + +-- The hazard this section guards against cannot be written in the first +-- place: a whole-row reference through a row constructor is rejected in +-- DEFINE, so no view can carry one as far as the deparser. +CREATE VIEW rpr_pin_row_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_pin, rpr_pin_other +WHERE rpr_pin.id = rpr_pin_other.id +WINDOW w AS (ORDER BY rpr_pin.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS ROW(rpr_pin.*) IS NOT NULL); +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 8: DEFINE A AS ROW(rpr_pin.*) IS NOT NULL); + ^ +HINT: A DEFINE condition may reference individual columns only. +-- a column merged by USING is pinned the same way +CREATE TABLE rpr_pin_l (x INT, y INT); +CREATE TABLE rpr_pin_r (x INT, z INT); +CREATE TABLE rpr_pin_x (id INT); +CREATE VIEW rpr_pin_using_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_pin_l JOIN rpr_pin_r USING (x), rpr_pin_x +WINDOW w AS (ORDER BY rpr_pin_l.y + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS x > 0); +ALTER TABLE rpr_pin_x ADD COLUMN x INT; +SELECT pg_get_viewdef('rpr_pin_using_v'::regclass, true); + pg_get_viewdef +-------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_pin_l + + JOIN rpr_pin_r USING (x), + + rpr_pin_x rpr_pin_x(id, x_1) + + WINDOW w AS (ORDER BY rpr_pin_l.y ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS x > 0); +(1 row) + +-- a JOIN ... ON behaves the same, and the view keeps returning its rows +CREATE TABLE rpr_pin_j1 (id INT, price INT); +CREATE TABLE rpr_pin_j2 (id INT, qty INT); +INSERT INTO rpr_pin_j1 VALUES (1, 10), (2, 20); +INSERT INTO rpr_pin_j2 VALUES (1, 5), (2, 7); +CREATE VIEW rpr_pin_on_v AS +SELECT j1.id, count(*) OVER w AS cnt +FROM rpr_pin_j1 j1 JOIN rpr_pin_j2 j2 ON j1.id = j2.id +WINDOW w AS (ORDER BY j1.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS price > 0); +ALTER TABLE rpr_pin_j2 ADD COLUMN price INT; +SELECT pg_get_viewdef('rpr_pin_on_v'::regclass, true); + pg_get_viewdef +-------------------------------------------------------------------------------- + SELECT j1.id, + + count(*) OVER w AS cnt + + FROM rpr_pin_j1 j1 + + JOIN rpr_pin_j2 j2(id, qty, price_1) ON j1.id = j2.id + + WINDOW w AS (ORDER BY j1.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS price > 0); +(1 row) + +SELECT * FROM rpr_pin_on_v ORDER BY id; + id | cnt +----+----- + 1 | 2 + 2 | 0 +(2 rows) + +-- The same query written fresh is rejected, since nothing pins the name for +-- it. Pinning is what lets the stored rpr_pin_on_v definition still reparse. +SELECT j1.id, count(*) OVER w AS cnt +FROM rpr_pin_j1 j1 JOIN rpr_pin_j2 j2 ON j1.id = j2.id +WINDOW w AS (ORDER BY j1.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS price > 0); +ERROR: column reference "price" is ambiguous +LINE 6: DEFINE A AS price > 0); + ^ +-- An aliased join hides its inputs, so the name that gets printed is +-- the join's own, taken from varnosyn, not the child column the Var +-- carries in varno. +-- Pinning the child instead would reserve a name that never reaches the output +-- and leave the printed one free for a later column to collide with. +CREATE TABLE rpr_pin_a (i INT, x INT); +CREATE TABLE rpr_pin_b (k INT, y INT); +CREATE TABLE rpr_pin_c (m INT); +INSERT INTO rpr_pin_a VALUES (1, 10), (2, 20); +INSERT INTO rpr_pin_b VALUES (1, 5), (2, 7); +INSERT INTO rpr_pin_c VALUES (100), (200); +CREATE VIEW rpr_pin_alias_v AS +SELECT count(*) OVER w AS cnt +FROM (rpr_pin_a JOIN rpr_pin_b ON rpr_pin_a.i = rpr_pin_b.k) j(p, q, r, s), + rpr_pin_c +WINDOW w AS (ORDER BY rpr_pin_c.m + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS q > 0); +ALTER TABLE rpr_pin_c ADD COLUMN q INT; +SELECT pg_get_viewdef('rpr_pin_alias_v'::regclass, true); + pg_get_viewdef +-------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM (rpr_pin_a + + JOIN rpr_pin_b ON rpr_pin_a.i = rpr_pin_b.k) j(p, q, r, s), + + rpr_pin_c rpr_pin_c(m, q_1) + + WINDOW w AS (ORDER BY rpr_pin_c.m ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a) + + DEFINE + + a AS q > 0); +(1 row) + +SELECT * FROM rpr_pin_alias_v; + cnt +----- + 1 + 1 + 1 + 1 +(4 rows) + +-- and the deparsed text builds a view that returns the same rows +CREATE VIEW rpr_pin_alias_v2 AS +SELECT count(*) OVER w AS cnt +FROM (rpr_pin_a JOIN rpr_pin_b ON rpr_pin_a.i = rpr_pin_b.k) j(p, q, r, s), + rpr_pin_c rpr_pin_c(m, q_1) +WINDOW w AS (ORDER BY rpr_pin_c.m + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a) + DEFINE a AS q > 0); +SELECT * FROM rpr_pin_alias_v2; + cnt +----- + 1 + 1 + 1 + 1 +(4 rows) + +DROP VIEW rpr_pin_alias_v2; +-- Without a user column alias list the join still keeps the printed name, and +-- the input relation is the one that moves aside. +CREATE VIEW rpr_pin_alias_v3 AS +SELECT count(*) OVER w AS cnt +FROM (rpr_pin_a JOIN rpr_pin_b ON rpr_pin_a.i = rpr_pin_b.k) j, rpr_pin_c +WINDOW w AS (ORDER BY rpr_pin_c.m + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS x > 0); +SELECT pg_get_viewdef('rpr_pin_alias_v3'::regclass, true); + pg_get_viewdef +-------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM (rpr_pin_a rpr_pin_a(i, x_1) + + JOIN rpr_pin_b ON rpr_pin_a.i = rpr_pin_b.k) j(i, x, k, y), + + rpr_pin_c + + WINDOW w AS (ORDER BY rpr_pin_c.m ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a) + + DEFINE + + a AS x > 0); +(1 row) + +DROP VIEW rpr_pin_alias_v3; +DROP VIEW rpr_pin_alias_v; +DROP TABLE rpr_pin_a, rpr_pin_b, rpr_pin_c; +-- A relation RTE prints the column alias the user wrote, not the catalog name, +-- so the alias is the name to reserve. Pinning the catalog name would replace +-- the alias in the printed text and push aside an unrelated column that +-- collides only with a name nobody prints. +CREATE TABLE rpr_pin_d (i INT, k INT); +CREATE TABLE rpr_pin_e (m INT); +INSERT INTO rpr_pin_d VALUES (1, 10), (2, 20); +INSERT INTO rpr_pin_e VALUES (100), (200); +CREATE VIEW rpr_pin_rel_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_pin_d d(p, q), rpr_pin_e +WINDOW w AS (ORDER BY rpr_pin_e.m + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS q > 0); +-- a column named after the catalog name is no collision: q is what is printed +ALTER TABLE rpr_pin_e ADD COLUMN k INT; +SELECT pg_get_viewdef('rpr_pin_rel_v'::regclass, true); + pg_get_viewdef +-------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_pin_d d(p, q), + + rpr_pin_e + + WINDOW w AS (ORDER BY rpr_pin_e.m ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a) + + DEFINE + + a AS q > 0); +(1 row) + +-- one named after the alias is, and moves aside +ALTER TABLE rpr_pin_e ADD COLUMN q INT; +SELECT pg_get_viewdef('rpr_pin_rel_v'::regclass, true); + pg_get_viewdef +-------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_pin_d d(p, q), + + rpr_pin_e rpr_pin_e(m, k, q_1) + + WINDOW w AS (ORDER BY rpr_pin_e.m ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a) + + DEFINE + + a AS q > 0); +(1 row) + +SELECT * FROM rpr_pin_rel_v; + cnt +----- + 1 + 1 + 1 + 1 +(4 rows) + +DROP VIEW rpr_pin_rel_v; +DROP TABLE rpr_pin_d, rpr_pin_e; +-- A name a DEFINE clause reads is the one name in the query that cannot be +-- spelled any other way, the qualifier slot being reserved for a pattern +-- variable. set_using_names() picks the name of every column merged by USING +-- before it, and is free to pick any name at all, renaming the merged inputs +-- to match. So the DEFINE names that are settled already are reserved first +-- and the merged names are chosen around them. Here the second USING would +-- otherwise reach for x_1, the very column the DEFINE clause reads: the +-- anonymous FULL JOIN forces USING names to be unique query-wide, which takes +-- plain x, and x_1 is what the next one counts up to. +CREATE TABLE rpr_res_t (x_1 INT, id INT); +CREATE TABLE rpr_res_l1 (x INT); +CREATE TABLE rpr_res_r1 (x INT); +CREATE TABLE rpr_res_l2 (x INT); +CREATE TABLE rpr_res_r2 (x INT); +INSERT INTO rpr_res_t VALUES (1, 1), (2, 2); +INSERT INTO rpr_res_l1 VALUES (1); +INSERT INTO rpr_res_r1 VALUES (1); +INSERT INTO rpr_res_l2 VALUES (1); +INSERT INTO rpr_res_r2 VALUES (1); +CREATE VIEW rpr_res_using_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_t, + (rpr_res_l1 FULL JOIN rpr_res_r1 USING (x)), + (rpr_res_l2 JOIN rpr_res_r2 USING (x)) +WINDOW w AS (ORDER BY rpr_res_t.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS x_1 > 0); +SELECT pg_get_viewdef('rpr_res_using_v'::regclass, true); + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_t, + + rpr_res_l1 + + FULL JOIN rpr_res_r1 USING (x), + + rpr_res_l2 rpr_res_l2(x_2) + + JOIN rpr_res_r2 rpr_res_r2(x_2) USING (x_2) + + WINDOW w AS (ORDER BY rpr_res_t.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS x_1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_using_rt AS ' + || pg_get_viewdef('rpr_res_using_v'::regclass, true) \gexec +CREATE VIEW rpr_res_using_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_t, + rpr_res_l1 + FULL JOIN rpr_res_r1 USING (x), + rpr_res_l2 rpr_res_l2(x_2) + JOIN rpr_res_r2 rpr_res_r2(x_2) USING (x_2) + WINDOW w AS (ORDER BY rpr_res_t.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS x_1 > 0); +SELECT pg_get_viewdef('rpr_res_using_v'::regclass, true) + = pg_get_viewdef('rpr_res_using_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_using_v; + cnt +----- + 2 + 0 +(2 rows) + +SELECT * FROM rpr_res_using_rt; + cnt +----- + 2 + 0 +(2 rows) + +DROP VIEW rpr_res_using_rt, rpr_res_using_v; +DROP TABLE rpr_res_t, rpr_res_l1, rpr_res_r1, rpr_res_l2, rpr_res_r2; +-- A merged column keeps its natural name and collides all the same. This one +-- is the column set_relation_column_names() cannot push aside afterwards: its +-- name was settled and handed to both inputs before that function ran, so the +-- loop there passes over it. An ordinary column in its place does move aside, +-- which is what the rpr_pin views above cover. +CREATE TABLE rpr_res_a (j INT, p INT); +CREATE TABLE rpr_res_b (j INT, q INT); +CREATE TABLE rpr_res_c (r INT, s INT); +INSERT INTO rpr_res_a VALUES (1, 10); +INSERT INTO rpr_res_b VALUES (1, 30); +INSERT INTO rpr_res_c VALUES (1, 10), (2, 20), (3, 15); +CREATE VIEW rpr_res_merged_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_a JOIN rpr_res_b USING (j) CROSS JOIN rpr_res_c +WINDOW w AS (ORDER BY rpr_res_c.s + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL PATTERN (X Y+) + DEFINE X AS true, Y AS s > PREV(s)); +-- the collision arrives only now: the merged column has been named j all along +ALTER TABLE rpr_res_c RENAME COLUMN s TO j; +SELECT pg_get_viewdef('rpr_res_merged_v'::regclass, true); + pg_get_viewdef +-------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_a rpr_res_a(j_1, p) + + JOIN rpr_res_b rpr_res_b(j_1, q) USING (j_1) + + CROSS JOIN rpr_res_c + + WINDOW w AS (ORDER BY rpr_res_c.j ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (x y+) + + DEFINE + + x AS true, + + y AS j > PREV(j)); +(1 row) + +SELECT 'CREATE VIEW rpr_res_merged_rt AS ' + || pg_get_viewdef('rpr_res_merged_v'::regclass, true) \gexec +CREATE VIEW rpr_res_merged_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_a rpr_res_a(j_1, p) + JOIN rpr_res_b rpr_res_b(j_1, q) USING (j_1) + CROSS JOIN rpr_res_c + WINDOW w AS (ORDER BY rpr_res_c.j ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (x y+) + DEFINE + x AS true, + y AS j > PREV(j)); +SELECT pg_get_viewdef('rpr_res_merged_v'::regclass, true) + = pg_get_viewdef('rpr_res_merged_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_merged_v; + cnt +----- + 3 + 0 + 0 +(3 rows) + +SELECT * FROM rpr_res_merged_rt; + cnt +----- + 3 + 0 + 0 +(3 rows) + +DROP VIEW rpr_res_merged_rt, rpr_res_merged_v; +DROP TABLE rpr_res_a, rpr_res_b, rpr_res_c; +-- Naming a merged column renames the columns it merges, and the name can land +-- in an RTE that has a real column of that name already. The column the +-- DEFINE clause reads is the one that cannot move, so the merge counts past it +-- instead and the RTE prints two distinct aliases. +CREATE TABLE rpr_res_fa (k INT); +CREATE TABLE rpr_res_fb (k INT); +CREATE TABLE rpr_res_ga (k INT, k_1 INT); +CREATE TABLE rpr_res_gb (k INT); +CREATE TABLE rpr_res_ord (id INT); +INSERT INTO rpr_res_fa VALUES (1); +INSERT INTO rpr_res_fb VALUES (1); +INSERT INTO rpr_res_ga VALUES (1, 5); +INSERT INTO rpr_res_gb VALUES (1); +INSERT INTO rpr_res_ord VALUES (1), (2); +CREATE VIEW rpr_res_dup_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_fa FULL JOIN rpr_res_fb USING (k), + rpr_res_ga JOIN rpr_res_gb USING (k), + rpr_res_ord +WINDOW w AS (ORDER BY rpr_res_ord.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS k_1 > 0); +SELECT pg_get_viewdef('rpr_res_dup_v'::regclass, true); + pg_get_viewdef +----------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_fa + + FULL JOIN rpr_res_fb USING (k), + + rpr_res_ga rpr_res_ga(k_2, k_1) + + JOIN rpr_res_gb rpr_res_gb(k_2) USING (k_2), + + rpr_res_ord + + WINDOW w AS (ORDER BY rpr_res_ord.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS k_1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_dup_rt AS ' + || pg_get_viewdef('rpr_res_dup_v'::regclass, true) \gexec +CREATE VIEW rpr_res_dup_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_fa + FULL JOIN rpr_res_fb USING (k), + rpr_res_ga rpr_res_ga(k_2, k_1) + JOIN rpr_res_gb rpr_res_gb(k_2) USING (k_2), + rpr_res_ord + WINDOW w AS (ORDER BY rpr_res_ord.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS k_1 > 0); +SELECT pg_get_viewdef('rpr_res_dup_v'::regclass, true) + = pg_get_viewdef('rpr_res_dup_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_dup_v; + cnt +----- + 2 + 0 +(2 rows) + +SELECT * FROM rpr_res_dup_rt; + cnt +----- + 2 + 0 +(2 rows) + +DROP VIEW rpr_res_dup_rt, rpr_res_dup_v; +DROP TABLE rpr_res_fa, rpr_res_fb, rpr_res_ga, rpr_res_gb, rpr_res_ord; +-- A merged column a DEFINE clause reads is named by the DEFINE clause: its +-- name is settled before set_using_names() runs, and the USING clause adopts +-- it rather than inventing one. The merged name therefore stays id here, +-- and the newcomer is the one that moves aside. +CREATE TABLE rpr_res_p (id INT, v INT); +CREATE TABLE rpr_res_q (id INT, w INT); +CREATE TABLE rpr_res_s (n INT); +INSERT INTO rpr_res_p VALUES (1, 10), (2, 20); +INSERT INTO rpr_res_q VALUES (1, 30), (3, 40); +INSERT INTO rpr_res_s VALUES (7); +CREATE VIEW rpr_res_full_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_p FULL JOIN rpr_res_q USING (id), rpr_res_s +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id > 0); +ALTER TABLE rpr_res_s ADD COLUMN id INT; +SELECT pg_get_viewdef('rpr_res_full_v'::regclass, true); + pg_get_viewdef +----------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_p + + FULL JOIN rpr_res_q USING (id), + + rpr_res_s rpr_res_s(n, id_1) + + 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 id > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_full_rt AS ' + || pg_get_viewdef('rpr_res_full_v'::regclass, true) \gexec +CREATE VIEW rpr_res_full_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_p + FULL JOIN rpr_res_q USING (id), + rpr_res_s rpr_res_s(n, id_1) + 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 id > 0); +SELECT pg_get_viewdef('rpr_res_full_v'::regclass, true) + = pg_get_viewdef('rpr_res_full_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_full_v; + cnt +----- + 3 + 0 + 0 +(3 rows) + +SELECT * FROM rpr_res_full_rt; + cnt +----- + 3 + 0 + 0 +(3 rows) + +DROP VIEW rpr_res_full_rt, rpr_res_full_v; +DROP TABLE rpr_res_p, rpr_res_q, rpr_res_s; +-- An aliased join answers for its inputs. The parser gives every column of +-- such a join the join's own varnosyn, so a DEFINE clause reading a column the +-- inputs brought in names the join and not the relation it came from. The +-- name is settled all the same, the join's USING clause not being the thing +-- that produced it, and so it is reserved: a column of a join that its own +-- USING clause does not name is no more merged than a relation's would be. +-- Left unreserved, the merged name counts up onto it and the join prints the +-- one alias twice, which reparses as an ambiguous column. +CREATE TABLE rpr_res_ja (x INT, x_1 INT); +CREATE TABLE rpr_res_jb (x INT, z INT); +CREATE TABLE rpr_res_m1 (x INT); +CREATE TABLE rpr_res_m2 (x INT); +CREATE TABLE rpr_res_ordj (id INT); +INSERT INTO rpr_res_ja VALUES (1, 7); +INSERT INTO rpr_res_jb VALUES (1, 9); +INSERT INTO rpr_res_m1 VALUES (1); +INSERT INTO rpr_res_m2 VALUES (1); +INSERT INTO rpr_res_ordj VALUES (1), (2); +CREATE VIEW rpr_res_alias_v AS +SELECT count(*) OVER w AS cnt +FROM (rpr_res_m1 FULL JOIN rpr_res_m2 USING (x)), + (rpr_res_ja JOIN rpr_res_jb USING (x)) AS jx, + rpr_res_ordj +WINDOW w AS (ORDER BY rpr_res_ordj.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS x_1 > 0); +SELECT pg_get_viewdef('rpr_res_alias_v'::regclass, true); + pg_get_viewdef +------------------------------------------------------------------------------------------ + SELECT count(*) OVER w AS cnt + + FROM rpr_res_m1 + + FULL JOIN rpr_res_m2 USING (x), + + (rpr_res_ja rpr_res_ja(x_2, x_1_1) + + JOIN rpr_res_jb rpr_res_jb(x_2, z) USING (x_2)) jx(x_2, x_1, z), + + rpr_res_ordj + + WINDOW w AS (ORDER BY rpr_res_ordj.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS x_1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_alias_rt AS ' + || pg_get_viewdef('rpr_res_alias_v'::regclass, true) \gexec +CREATE VIEW rpr_res_alias_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_m1 + FULL JOIN rpr_res_m2 USING (x), + (rpr_res_ja rpr_res_ja(x_2, x_1_1) + JOIN rpr_res_jb rpr_res_jb(x_2, z) USING (x_2)) jx(x_2, x_1, z), + rpr_res_ordj + WINDOW w AS (ORDER BY rpr_res_ordj.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS x_1 > 0); +SELECT pg_get_viewdef('rpr_res_alias_v'::regclass, true) + = pg_get_viewdef('rpr_res_alias_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_alias_v; + cnt +----- + 2 + 0 +(2 rows) + +SELECT * FROM rpr_res_alias_rt; + cnt +----- + 2 + 0 +(2 rows) + +-- The same through NATURAL JOIN, which names no column in the query text. The +-- test above reads a join's USING clause to tell a merged column from one that +-- only passes through, and the analyzed tree is where it reads it: the parser +-- works out which columns NATURAL merges and files them there. +CREATE VIEW rpr_res_nat_v AS +SELECT count(*) OVER w AS cnt +FROM (rpr_res_m1 FULL JOIN rpr_res_m2 USING (x)), + (rpr_res_ja NATURAL JOIN rpr_res_jb) AS jx, + rpr_res_ordj +WINDOW w AS (ORDER BY rpr_res_ordj.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS x_1 > 0); +SELECT pg_get_viewdef('rpr_res_nat_v'::regclass, true); + pg_get_viewdef +------------------------------------------------------------------------------------------ + SELECT count(*) OVER w AS cnt + + FROM rpr_res_m1 + + FULL JOIN rpr_res_m2 USING (x), + + (rpr_res_ja rpr_res_ja(x_2, x_1_1) + + JOIN rpr_res_jb rpr_res_jb(x_2, z) USING (x_2)) jx(x_2, x_1, z), + + rpr_res_ordj + + WINDOW w AS (ORDER BY rpr_res_ordj.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS x_1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_nat_rt AS ' + || pg_get_viewdef('rpr_res_nat_v'::regclass, true) \gexec +CREATE VIEW rpr_res_nat_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_m1 + FULL JOIN rpr_res_m2 USING (x), + (rpr_res_ja rpr_res_ja(x_2, x_1_1) + JOIN rpr_res_jb rpr_res_jb(x_2, z) USING (x_2)) jx(x_2, x_1, z), + rpr_res_ordj + WINDOW w AS (ORDER BY rpr_res_ordj.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS x_1 > 0); +SELECT pg_get_viewdef('rpr_res_nat_v'::regclass, true) + = pg_get_viewdef('rpr_res_nat_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +DROP VIEW rpr_res_nat_rt, rpr_res_nat_v; +DROP VIEW rpr_res_alias_rt, rpr_res_alias_v; +DROP TABLE rpr_res_ja, rpr_res_jb, rpr_res_m1, rpr_res_m2, rpr_res_ordj; +-- A column a function's result type grows after the view is made is one the +-- deparser does not see at all: expandRTE() stops at the column count the +-- query was parsed with. A column it cannot see is one it cannot rename out +-- of the way, and the name it collides with here is the one a DEFINE clause +-- has to resolve to as printed. So the grown columns are looked up and named +-- too, and printed in full, the alias list being positional. +CREATE TABLE rpr_res_fn (id INT, val INT); +INSERT INTO rpr_res_fn VALUES (1, 1), (2, 2), (3, 3); +CREATE TABLE rpr_res_cfg (a INT); +INSERT INTO rpr_res_cfg VALUES (1); +CREATE FUNCTION rpr_res_fcfg() RETURNS SETOF rpr_res_cfg LANGUAGE sql + AS $$ SELECT * FROM rpr_res_cfg $$; +CREATE VIEW rpr_res_fn_v AS +SELECT rpr_res_fn.id, count(*) OVER w AS cnt +FROM rpr_res_fn, rpr_res_fcfg() f +WINDOW w AS (ORDER BY rpr_res_fn.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0); +-- nothing to keep off yet +SELECT pg_get_viewdef('rpr_res_fn_v'::regclass, true); + pg_get_viewdef +---------------------------------------------------------------------------------------- + SELECT rpr_res_fn.id, + + count(*) OVER w AS cnt + + FROM rpr_res_fn, + + rpr_res_fcfg() f(a) + + WINDOW w AS (ORDER BY rpr_res_fn.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS val > 0); +(1 row) + +ALTER TABLE rpr_res_cfg ADD COLUMN val INT; +SELECT pg_get_viewdef('rpr_res_fn_v'::regclass, true); + pg_get_viewdef +---------------------------------------------------------------------------------------- + SELECT rpr_res_fn.id, + + count(*) OVER w AS cnt + + FROM rpr_res_fn, + + rpr_res_fcfg() f(a, val_1) + + WINDOW w AS (ORDER BY rpr_res_fn.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS val > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_fn_rt AS ' + || pg_get_viewdef('rpr_res_fn_v'::regclass, true) \gexec +CREATE VIEW rpr_res_fn_rt AS SELECT rpr_res_fn.id, + count(*) OVER w AS cnt + FROM rpr_res_fn, + rpr_res_fcfg() f(a, val_1) + WINDOW w AS (ORDER BY rpr_res_fn.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS val > 0); +SELECT pg_get_viewdef('rpr_res_fn_v'::regclass, true) + = pg_get_viewdef('rpr_res_fn_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_fn_v; + id | cnt +----+----- + 1 | 3 + 2 | 0 + 3 | 0 +(3 rows) + +SELECT * FROM rpr_res_fn_rt; + id | cnt +----+----- + 1 | 3 + 2 | 0 + 3 | 0 +(3 rows) + +-- a grown column that collides with nothing is still on the list +ALTER TABLE rpr_res_cfg ADD COLUMN spare INT; +SELECT pg_get_viewdef('rpr_res_fn_v'::regclass, true); + pg_get_viewdef +---------------------------------------------------------------------------------------- + SELECT rpr_res_fn.id, + + count(*) OVER w AS cnt + + FROM rpr_res_fn, + + rpr_res_fcfg() f(a, val_1, spare) + + WINDOW w AS (ORDER BY rpr_res_fn.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS val > 0); +(1 row) + +DROP VIEW rpr_res_fn_rt, rpr_res_fn_v; +DROP FUNCTION rpr_res_fcfg(); +DROP TABLE rpr_res_fn, rpr_res_cfg; +-- A system column is named from the catalog, not from the deparser's own +-- choice, so there is no alias to pick for it and nothing to exempt from +-- renaming. Its name still has to be held against the rest of the query, or a +-- column that turns up later answers to it as well. The function builds its +-- row from the type as it stands when called, so it still returns one after +-- the type grows, with NULL in the grown column; a DEFINE clause that read +-- that column instead of rpr_res_sys.ctid would match no row. +CREATE TABLE rpr_res_sys (id INT, v INT); +INSERT INTO rpr_res_sys VALUES (1, 1), (2, 2); +CREATE TYPE rpr_res_ct AS (a INT); +CREATE FUNCTION rpr_res_fct() RETURNS SETOF rpr_res_ct LANGUAGE sql + AS $$ SELECT * FROM json_populate_record(NULL::rpr_res_ct, '{"a": 1}') $$; +CREATE VIEW rpr_res_sys_v AS +SELECT rpr_res_sys.id, count(*) OVER w AS cnt +FROM rpr_res_sys, rpr_res_fct() f +WINDOW w AS (ORDER BY rpr_res_sys.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS ctid IS NOT NULL); +ALTER TYPE rpr_res_ct ADD ATTRIBUTE ctid INT; +SELECT pg_get_viewdef('rpr_res_sys_v'::regclass, true); + pg_get_viewdef +----------------------------------------------------------------------------------------- + SELECT rpr_res_sys.id, + + count(*) OVER w AS cnt + + FROM rpr_res_sys, + + rpr_res_fct() f(a, ctid_1) + + WINDOW w AS (ORDER BY rpr_res_sys.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS ctid IS NOT NULL); +(1 row) + +SELECT 'CREATE VIEW rpr_res_sys_rt AS ' + || pg_get_viewdef('rpr_res_sys_v'::regclass, true) \gexec +CREATE VIEW rpr_res_sys_rt AS SELECT rpr_res_sys.id, + count(*) OVER w AS cnt + FROM rpr_res_sys, + rpr_res_fct() f(a, ctid_1) + WINDOW w AS (ORDER BY rpr_res_sys.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS ctid IS NOT NULL); +SELECT pg_get_viewdef('rpr_res_sys_v'::regclass, true) + = pg_get_viewdef('rpr_res_sys_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_sys_v; + id | cnt +----+----- + 1 | 2 + 2 | 0 +(2 rows) + +SELECT * FROM rpr_res_sys_rt; + id | cnt +----+----- + 1 | 2 + 2 | 0 +(2 rows) + +DROP VIEW rpr_res_sys_rt, rpr_res_sys_v; +DROP FUNCTION rpr_res_fct(); +DROP TYPE rpr_res_ct; +DROP TABLE rpr_res_sys; +-- Four more corners of the same deparse handling. +-- +-- An INNER JOIN USING merges to a plain Var of the left input, not to a +-- COALESCE, so there is no merge expression for the DEFINE clause to be +-- collapsed onto; the grouping still makes the deparser look for one. +CREATE TABLE rpr_cov_l (id INT, v INT); +CREATE TABLE rpr_cov_r (id INT, w INT); +INSERT INTO rpr_cov_l VALUES (1, 1), (2, 2), (3, 3); +INSERT INTO rpr_cov_r VALUES (1, 1), (2, 2), (3, 3); +CREATE VIEW rpr_cov_inner_v AS +SELECT id + 1 AS idp1, count(*) OVER w AS cnt +FROM rpr_cov_l JOIN rpr_cov_r USING (id) +GROUP BY id + 1 +WINDOW w AS (ORDER BY id + 1 + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id + 1 > 0); +SELECT pg_get_viewdef('rpr_cov_inner_v'::regclass, true); + pg_get_viewdef +--------------------------------------------------------------------------------------------- + SELECT rpr_cov_l.id + 1 AS idp1, + + count(*) OVER w AS cnt + + FROM rpr_cov_l + + JOIN rpr_cov_r USING (id) + + GROUP BY (rpr_cov_l.id + 1) + + WINDOW w AS (ORDER BY (rpr_cov_l.id + 1) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS (id + 1) > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_cov_inner_rt AS ' + || pg_get_viewdef('rpr_cov_inner_v'::regclass, true) \gexec +CREATE VIEW rpr_cov_inner_rt AS SELECT rpr_cov_l.id + 1 AS idp1, + count(*) OVER w AS cnt + FROM rpr_cov_l + JOIN rpr_cov_r USING (id) + GROUP BY (rpr_cov_l.id + 1) + WINDOW w AS (ORDER BY (rpr_cov_l.id + 1) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS (id + 1) > 0); +SELECT pg_get_viewdef('rpr_cov_inner_v'::regclass, true) + = pg_get_viewdef('rpr_cov_inner_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_cov_inner_v ORDER BY idp1; + idp1 | cnt +------+----- + 2 | 3 + 3 | 0 + 4 | 0 +(3 rows) + +SELECT * FROM rpr_cov_inner_rt ORDER BY idp1; + idp1 | cnt +------+----- + 2 | 3 + 3 | 0 + 4 | 0 +(3 rows) + +DROP VIEW rpr_cov_inner_rt, rpr_cov_inner_v; +-- A DEFINE clause that reads the same system column in two variables holds its +-- name once; the second reference finds it held already. +CREATE VIEW rpr_cov_sys2_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_cov_l +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B) + DEFINE A AS tableoid > 0, B AS tableoid > 0); +SELECT pg_get_viewdef('rpr_cov_sys2_v'::regclass, true); + pg_get_viewdef +----------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_cov_l + + 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 tableoid > 0::oid, + + b AS tableoid > 0::oid); +(1 row) + +SELECT 'CREATE VIEW rpr_cov_sys2_rt AS ' + || pg_get_viewdef('rpr_cov_sys2_v'::regclass, true) \gexec +CREATE VIEW rpr_cov_sys2_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_cov_l + 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 tableoid > 0::oid, + b AS tableoid > 0::oid); +SELECT pg_get_viewdef('rpr_cov_sys2_v'::regclass, true) + = pg_get_viewdef('rpr_cov_sys2_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +DROP VIEW rpr_cov_sys2_rt, rpr_cov_sys2_v; +-- A function with a column definition list has its column set fixed by the +-- list, so no column can have grown since the view was made. +CREATE VIEW rpr_cov_coldef_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_cov_l, json_to_record('{"a": 1}') AS j(a int) +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS a > 0); +SELECT pg_get_viewdef('rpr_cov_coldef_v'::regclass, true); + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_cov_l, + + json_to_record('{"a": 1}'::json) j(a integer) + + WINDOW w AS (ORDER BY rpr_cov_l.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS a > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_cov_coldef_rt AS ' + || pg_get_viewdef('rpr_cov_coldef_v'::regclass, true) \gexec +CREATE VIEW rpr_cov_coldef_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_cov_l, + json_to_record('{"a": 1}'::json) j(a integer) + WINDOW w AS (ORDER BY rpr_cov_l.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS a > 0); +SELECT pg_get_viewdef('rpr_cov_coldef_v'::regclass, true) + = pg_get_viewdef('rpr_cov_coldef_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_cov_coldef_v; + cnt +----- + 3 + 0 + 0 +(3 rows) + +SELECT * FROM rpr_cov_coldef_rt; + cnt +----- + 3 + 0 + 0 +(3 rows) + +DROP VIEW rpr_cov_coldef_rt, rpr_cov_coldef_v; +-- Once a FULL JOIN USING has a merge expression to collapse, every node of the +-- DEFINE clause is looked at, whatever it is. A function call that is no +-- merge is left as it is, and a navigation without an offset has an empty +-- offset argument to pass over. +CREATE VIEW rpr_cov_merge_v AS +SELECT id, count(*) OVER w AS cnt +FROM rpr_cov_l FULL JOIN rpr_cov_r USING (id) +GROUP BY id +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS abs(id) > 0 AND PREV(id) IS NULL OR id > 1); +SELECT pg_get_viewdef('rpr_cov_merge_v'::regclass, true); + pg_get_viewdef +----------------------------------------------------------------------------- + SELECT id, + + count(*) OVER w AS cnt + + FROM rpr_cov_l + + FULL JOIN rpr_cov_r USING (id) + + GROUP BY id + + 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 abs(id) > 0 AND PREV(id) IS NULL OR id > 1); +(1 row) + +SELECT 'CREATE VIEW rpr_cov_merge_rt AS ' + || pg_get_viewdef('rpr_cov_merge_v'::regclass, true) \gexec +CREATE VIEW rpr_cov_merge_rt AS SELECT id, + count(*) OVER w AS cnt + FROM rpr_cov_l + FULL JOIN rpr_cov_r USING (id) + GROUP BY id + 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 abs(id) > 0 AND PREV(id) IS NULL OR id > 1); +SELECT pg_get_viewdef('rpr_cov_merge_v'::regclass, true) + = pg_get_viewdef('rpr_cov_merge_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_cov_merge_v ORDER BY id; + id | cnt +----+----- + 1 | 3 + 2 | 0 + 3 | 0 +(3 rows) + +SELECT * FROM rpr_cov_merge_rt ORDER BY id; + id | cnt +----+----- + 1 | 3 + 2 | 0 + 3 | 0 +(3 rows) + +DROP VIEW rpr_cov_merge_rt, rpr_cov_merge_v; +DROP TABLE rpr_cov_l, rpr_cov_r; +-- A TABLEFUNC RTE writes its column names into the clause that produces them, +-- but it accepts a column alias list like any other RTE, and a rename of one +-- of its columns is printed there. So a TABLEFUNC column that comes to +-- answer to the name a DEFINE clause reads is renamed like any other column +-- would be, and the DEFINE clause keeps its spelling. +CREATE TABLE rpr_res_tf (id INT, s INT); +INSERT INTO rpr_res_tf VALUES (1, 1), (2, 2); +CREATE VIEW rpr_res_tf_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_tf, + JSON_TABLE(jsonb '[1,2]', '$[*]' COLUMNS (c1 int PATH '$')) AS jx +WINDOW w AS (ORDER BY rpr_res_tf.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS s > 0); +-- the collision arrives only now +ALTER TABLE rpr_res_tf RENAME COLUMN s TO c1; +SELECT pg_get_viewdef('rpr_res_tf_v'::regclass, true); + pg_get_viewdef +---------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_tf, + + JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + + COLUMNS ( + + c1 integer PATH '$' + + ) + + ) jx(c1_1) + + WINDOW w AS (ORDER BY rpr_res_tf.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS c1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_tf_rt AS ' + || pg_get_viewdef('rpr_res_tf_v'::regclass, true) \gexec +CREATE VIEW rpr_res_tf_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_tf, + JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + c1 integer PATH '$' + ) + ) jx(c1_1) + WINDOW w AS (ORDER BY rpr_res_tf.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS c1 > 0); +SELECT pg_get_viewdef('rpr_res_tf_v'::regclass, true) + = pg_get_viewdef('rpr_res_tf_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_tf_v; + cnt +----- + 4 + 0 + 0 + 0 +(4 rows) + +SELECT * FROM rpr_res_tf_rt; + cnt +----- + 4 + 0 + 0 + 0 +(4 rows) + +DROP VIEW rpr_res_tf_rt, rpr_res_tf_v; +DROP TABLE rpr_res_tf; +-- An aliased join answers for its inputs and hides them, so the DEFINE +-- clause here reads the join's own column x. That name is reserved across +-- the query level, which reaches the TABLEFUNC column underneath as well: +-- it is renamed on its own alias list, and the join prints x on its list to +-- keep the name the query sees. Out of reach of an unqualified reference, +-- the TABLEFUNC column would not have collided, but a reserved name is kept +-- off every RTE of the level, as a globally unique USING name is. +CREATE TABLE rpr_res_hid (id INT, v INT); +CREATE TABLE rpr_res_hu (m INT); +INSERT INTO rpr_res_hid VALUES (1, 1), (2, 2), (3, 3); +INSERT INTO rpr_res_hu VALUES (9); +CREATE VIEW rpr_res_hid_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_hid, + (JSON_TABLE(jsonb '[1,2]', '$[*]' COLUMNS (x int PATH '$')) AS jt + JOIN rpr_res_hu ON jt.x > 0) j +WINDOW w AS (ORDER BY rpr_res_hid.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS x > 0); +SELECT pg_get_viewdef('rpr_res_hid_v'::regclass, true); + pg_get_viewdef +----------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_hid, + + (JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + + COLUMNS ( + + x integer PATH '$' + + ) + + ) jt(x_1) + + JOIN rpr_res_hu ON jt.x_1 > 0) j(x, m) + + WINDOW w AS (ORDER BY rpr_res_hid.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS x > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_hid_rt AS ' + || pg_get_viewdef('rpr_res_hid_v'::regclass, true) \gexec +CREATE VIEW rpr_res_hid_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_hid, + (JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + x integer PATH '$' + ) + ) jt(x_1) + JOIN rpr_res_hu ON jt.x_1 > 0) j(x, m) + WINDOW w AS (ORDER BY rpr_res_hid.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS x > 0); +SELECT pg_get_viewdef('rpr_res_hid_v'::regclass, true) + = pg_get_viewdef('rpr_res_hid_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_hid_v; + cnt +----- + 6 + 0 + 0 + 0 + 0 + 0 +(6 rows) + +SELECT * FROM rpr_res_hid_rt; + cnt +----- + 6 + 0 + 0 + 0 + 0 + 0 +(6 rows) + +DROP VIEW rpr_res_hid_rt, rpr_res_hid_v; +DROP TABLE rpr_res_hid, rpr_res_hu; +-- A column can come to carry the name a DEFINE clause reads only after the +-- view is made, by being renamed, and from then on the two have to be told +-- apart in the printed text. The DEFINE column keeps its spelling, being +-- settled first, and the newcomer gets name_N -- whether it sits in the same +-- RTE or in another one where a DEFINE clause reads it too, is the one a +-- USING clause merges, or is not merged but carries the USING clause's +-- spelling, which is no business of the deparser's to guess from. A USING +-- clause elsewhere that spells the newcomer's new name moves out of the way +-- as well. +CREATE TABLE rpr_res_ren (c INT, d INT); +INSERT INTO rpr_res_ren VALUES (1, 1), (2, 2); +CREATE TABLE rpr_res_ren2 (a INT, e INT); +INSERT INTO rpr_res_ren2 VALUES (1, 0), (2, 0); +CREATE TABLE rpr_res_renl (a_1 INT); +CREATE TABLE rpr_res_renr (a_1 INT); +INSERT INTO rpr_res_renl VALUES (1); +INSERT INTO rpr_res_renr VALUES (1); +CREATE TABLE rpr_res_rs (x INT); +CREATE TABLE rpr_res_rr (x INT, y INT); +INSERT INTO rpr_res_rs VALUES (1), (2); +INSERT INTO rpr_res_rr VALUES (1, 5), (2, 6); +CREATE TABLE rpr_res_rena (id INT, a INT); +CREATE TABLE rpr_res_renb (id INT, b INT); +INSERT INTO rpr_res_rena VALUES (1, 1), (2, 2); +INSERT INTO rpr_res_renb VALUES (1, 5), (2, 0); +-- the same RTE, its alias list shorter than the table +CREATE VIEW rpr_res_ren_v AS +SELECT x.a, x.d, count(*) OVER w AS cnt +FROM rpr_res_ren AS x(a) +WINDOW w AS (ORDER BY x.a + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (P Q*) + DEFINE P AS a > 0, Q AS d > 0); +-- the column a USING clause merges +CREATE VIEW rpr_res_renu_v AS +SELECT a, x.d, count(*) OVER w AS cnt +FROM rpr_res_ren AS x(a) JOIN rpr_res_ren2 USING (a) +WINDOW w AS (ORDER BY a + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (P Q*) + DEFINE P AS d > 0); +-- a USING clause elsewhere that spells the name the newcomer is going to get +CREATE VIEW rpr_res_renx_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_ren AS x(a), rpr_res_renl JOIN rpr_res_renr USING (a_1) +WINDOW w AS (ORDER BY x.a + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (P Q*) + DEFINE P AS a > 0, Q AS d > 0); +-- a merged column that is renamed away, and a DEFINE column renamed onto +-- the USING clause's spelling +CREATE VIEW rpr_res_renm_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_rs JOIN rpr_res_rr USING (x) +WINDOW w AS (ORDER BY y + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS y > 0); +-- another RTE, both of its columns read by the DEFINE clause +CREATE VIEW rpr_res_reno_v AS +SELECT rpr_res_rena.id, count(*) OVER w AS cnt +FROM rpr_res_rena JOIN rpr_res_renb ON rpr_res_rena.id = rpr_res_renb.id +WINDOW w AS (ORDER BY rpr_res_rena.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (P Q*) + DEFINE P AS a > 0, Q AS b > 0); +-- the collisions arrive only now +ALTER TABLE rpr_res_ren RENAME d TO a; +ALTER TABLE rpr_res_rr RENAME x TO z; +ALTER TABLE rpr_res_rr RENAME y TO x; +ALTER TABLE rpr_res_renb RENAME b TO a; +SELECT pg_get_viewdef('rpr_res_ren_v'::regclass, true); + pg_get_viewdef +---------------------------------------------------------------------------- + SELECT a, + + a_1 AS d, + + count(*) OVER w AS cnt + + FROM rpr_res_ren x(a, a_1) + + WINDOW w AS (ORDER BY a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (p q*) + + DEFINE + + p AS a > 0, + + q AS a_1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_ren_rt AS ' + || pg_get_viewdef('rpr_res_ren_v'::regclass, true) \gexec +CREATE VIEW rpr_res_ren_rt AS SELECT a, + a_1 AS d, + count(*) OVER w AS cnt + FROM rpr_res_ren x(a, a_1) + WINDOW w AS (ORDER BY a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (p q*) + DEFINE + p AS a > 0, + q AS a_1 > 0); +SELECT pg_get_viewdef('rpr_res_ren_v'::regclass, true) + = pg_get_viewdef('rpr_res_ren_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_ren_v; + a | d | cnt +---+---+----- + 1 | 1 | 2 + 2 | 2 | 0 +(2 rows) + +SELECT * FROM rpr_res_ren_rt; + a | d | cnt +---+---+----- + 1 | 1 | 2 + 2 | 2 | 0 +(2 rows) + +SELECT pg_get_viewdef('rpr_res_renu_v'::regclass, true); + pg_get_viewdef +-------------------------------------------------------------------------------- + SELECT x.a_1 AS a, + + x.a AS d, + + count(*) OVER w AS cnt + + FROM rpr_res_ren x(a_1, a) + + JOIN rpr_res_ren2 rpr_res_ren2(a_1, e) USING (a_1) + + WINDOW w AS (ORDER BY x.a_1 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (p q*) + + DEFINE + + p AS a > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_renu_rt AS ' + || pg_get_viewdef('rpr_res_renu_v'::regclass, true) \gexec +CREATE VIEW rpr_res_renu_rt AS SELECT x.a_1 AS a, + x.a AS d, + count(*) OVER w AS cnt + FROM rpr_res_ren x(a_1, a) + JOIN rpr_res_ren2 rpr_res_ren2(a_1, e) USING (a_1) + WINDOW w AS (ORDER BY x.a_1 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (p q*) + DEFINE + p AS a > 0); +SELECT pg_get_viewdef('rpr_res_renu_v'::regclass, true) + = pg_get_viewdef('rpr_res_renu_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_renu_v; + a | d | cnt +---+---+----- + 1 | 1 | 2 + 2 | 2 | 0 +(2 rows) + +SELECT * FROM rpr_res_renu_rt; + a | d | cnt +---+---+----- + 1 | 1 | 2 + 2 | 2 | 0 +(2 rows) + +SELECT pg_get_viewdef('rpr_res_renx_v'::regclass, true); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT count(*) OVER w AS cnt + + FROM rpr_res_ren x(a, a_1), + + rpr_res_renl rpr_res_renl(a_1_1) + + JOIN rpr_res_renr rpr_res_renr(a_1_1) USING (a_1_1) + + WINDOW w AS (ORDER BY x.a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (p q*) + + DEFINE + + p AS a > 0, + + q AS a_1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_renx_rt AS ' + || pg_get_viewdef('rpr_res_renx_v'::regclass, true) \gexec +CREATE VIEW rpr_res_renx_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_ren x(a, a_1), + rpr_res_renl rpr_res_renl(a_1_1) + JOIN rpr_res_renr rpr_res_renr(a_1_1) USING (a_1_1) + WINDOW w AS (ORDER BY x.a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (p q*) + DEFINE + p AS a > 0, + q AS a_1 > 0); +SELECT pg_get_viewdef('rpr_res_renx_v'::regclass, true) + = pg_get_viewdef('rpr_res_renx_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_renx_v; + cnt +----- + 2 + 0 +(2 rows) + +SELECT * FROM rpr_res_renx_rt; + cnt +----- + 2 + 0 +(2 rows) + +SELECT pg_get_viewdef('rpr_res_renm_v'::regclass, true); + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_rs rpr_res_rs(x_1) + + JOIN rpr_res_rr rpr_res_rr(x_1, x) USING (x_1) + + WINDOW w AS (ORDER BY rpr_res_rr.x ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS x > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_renm_rt AS ' + || pg_get_viewdef('rpr_res_renm_v'::regclass, true) \gexec +CREATE VIEW rpr_res_renm_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_rs rpr_res_rs(x_1) + JOIN rpr_res_rr rpr_res_rr(x_1, x) USING (x_1) + WINDOW w AS (ORDER BY rpr_res_rr.x ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS x > 0); +SELECT pg_get_viewdef('rpr_res_renm_v'::regclass, true) + = pg_get_viewdef('rpr_res_renm_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_renm_v; + cnt +----- + 2 + 0 +(2 rows) + +SELECT * FROM rpr_res_renm_rt; + cnt +----- + 2 + 0 +(2 rows) + +SELECT pg_get_viewdef('rpr_res_reno_v'::regclass, true); + pg_get_viewdef +------------------------------------------------------------------------------------------ + SELECT rpr_res_rena.id, + + count(*) OVER w AS cnt + + FROM rpr_res_rena + + JOIN rpr_res_renb rpr_res_renb(id, a_1) ON rpr_res_rena.id = rpr_res_renb.id + + WINDOW w AS (ORDER BY rpr_res_rena.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (p q*) + + DEFINE + + p AS a > 0, + + q AS a_1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_reno_rt AS ' + || pg_get_viewdef('rpr_res_reno_v'::regclass, true) \gexec +CREATE VIEW rpr_res_reno_rt AS SELECT rpr_res_rena.id, + count(*) OVER w AS cnt + FROM rpr_res_rena + JOIN rpr_res_renb rpr_res_renb(id, a_1) ON rpr_res_rena.id = rpr_res_renb.id + WINDOW w AS (ORDER BY rpr_res_rena.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (p q*) + DEFINE + p AS a > 0, + q AS a_1 > 0); +SELECT pg_get_viewdef('rpr_res_reno_v'::regclass, true) + = pg_get_viewdef('rpr_res_reno_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_reno_v; + id | cnt +----+----- + 1 | 1 + 2 | 1 +(2 rows) + +SELECT * FROM rpr_res_reno_rt; + id | cnt +----+----- + 1 | 1 + 2 | 1 +(2 rows) + +DROP VIEW rpr_res_reno_rt, rpr_res_reno_v; +DROP VIEW rpr_res_renm_rt, rpr_res_renm_v; +DROP VIEW rpr_res_renx_rt, rpr_res_renx_v; +DROP VIEW rpr_res_renu_rt, rpr_res_renu_v; +DROP VIEW rpr_res_ren_rt, rpr_res_ren_v; +DROP TABLE rpr_res_ren, rpr_res_ren2, rpr_res_renl, rpr_res_renr; +DROP TABLE rpr_res_rs, rpr_res_rr; +DROP TABLE rpr_res_rena, rpr_res_renb; +-- A merged column an aliased join carries can spell the DEFINE name from the +-- start, and a TABLEFUNC among the join's inputs changes nothing about that: +-- the join's own alias list names the merged column, the inputs answer to +-- it, and the DEFINE column is left alone. Nor does the order of the FROM +-- list matter. +CREATE TABLE rpr_res_tj (id INT, c1 INT); +INSERT INTO rpr_res_tj VALUES (1, 1), (2, 2); +CREATE TABLE rpr_res_tb (c1 INT, y INT); +INSERT INTO rpr_res_tb VALUES (1, 10), (2, 20); +CREATE TABLE rpr_res_ti (id INT); +INSERT INTO rpr_res_ti VALUES (1), (2); +CREATE VIEW rpr_res_tj_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_tj, + (JSON_TABLE(jsonb '[1,2]', '$[*]' COLUMNS (c1 int PATH '$')) AS jt + JOIN rpr_res_tb USING (c1)) AS j(k, m) +WINDOW w AS (ORDER BY rpr_res_tj.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS c1 > 0); +CREATE VIEW rpr_res_tjr_v AS +SELECT count(*) OVER w AS cnt +FROM (JSON_TABLE(jsonb '[1,2]', '$[*]' COLUMNS (c1 int PATH '$')) AS jt + JOIN rpr_res_tb USING (c1)) AS j(k, m), + rpr_res_tj +WINDOW w AS (ORDER BY rpr_res_tj.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS c1 > 0); +-- the join's alias list puts the USING spelling on a column it does not +-- merge, and that column is the one the DEFINE clause reads +CREATE VIEW rpr_res_tja_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_ti, + (JSON_TABLE(jsonb '[1,2]', '$[*]' COLUMNS (c1 int PATH '$')) AS jt + JOIN rpr_res_tb USING (c1)) AS j(k, c1) +WINDOW w AS (ORDER BY rpr_res_ti.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS c1 > 0); +-- the same with the TABLEFUNC on the right +CREATE VIEW rpr_res_tjb_v AS +SELECT count(*) OVER w AS cnt +FROM (rpr_res_tb JOIN JSON_TABLE(jsonb '[1,2]', '$[*]' COLUMNS (c1 int PATH '$')) AS jt + USING (c1)) AS j(a, c1) +WINDOW w AS (ORDER BY j.a + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS c1 > 0); +SELECT pg_get_viewdef('rpr_res_tj_v'::regclass, true); + pg_get_viewdef +---------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_tj, + + (JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + + COLUMNS ( + + c1 integer PATH '$' + + ) + + ) jt(k) + + JOIN rpr_res_tb rpr_res_tb(k, y) USING (k)) j(k, m) + + WINDOW w AS (ORDER BY rpr_res_tj.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS c1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_tj_rt AS ' + || pg_get_viewdef('rpr_res_tj_v'::regclass, true) \gexec +CREATE VIEW rpr_res_tj_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_tj, + (JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + c1 integer PATH '$' + ) + ) jt(k) + JOIN rpr_res_tb rpr_res_tb(k, y) USING (k)) j(k, m) + WINDOW w AS (ORDER BY rpr_res_tj.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS c1 > 0); +SELECT pg_get_viewdef('rpr_res_tj_v'::regclass, true) + = pg_get_viewdef('rpr_res_tj_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_tj_v; + cnt +----- + 4 + 0 + 0 + 0 +(4 rows) + +SELECT * FROM rpr_res_tj_rt; + cnt +----- + 4 + 0 + 0 + 0 +(4 rows) + +SELECT pg_get_viewdef('rpr_res_tjr_v'::regclass, true); + pg_get_viewdef +---------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM (JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + + COLUMNS ( + + c1 integer PATH '$' + + ) + + ) jt(k) + + JOIN rpr_res_tb rpr_res_tb(k, y) USING (k)) j(k, m), + + rpr_res_tj + + WINDOW w AS (ORDER BY rpr_res_tj.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS c1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_tjr_rt AS ' + || pg_get_viewdef('rpr_res_tjr_v'::regclass, true) \gexec +CREATE VIEW rpr_res_tjr_rt AS SELECT count(*) OVER w AS cnt + FROM (JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + c1 integer PATH '$' + ) + ) jt(k) + JOIN rpr_res_tb rpr_res_tb(k, y) USING (k)) j(k, m), + rpr_res_tj + WINDOW w AS (ORDER BY rpr_res_tj.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS c1 > 0); +SELECT pg_get_viewdef('rpr_res_tjr_v'::regclass, true) + = pg_get_viewdef('rpr_res_tjr_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT pg_get_viewdef('rpr_res_tja_v'::regclass, true); + pg_get_viewdef +---------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_ti, + + (JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + + COLUMNS ( + + c1 integer PATH '$' + + ) + + ) jt(k) + + JOIN rpr_res_tb rpr_res_tb(k, y) USING (k)) j(k, c1) + + WINDOW w AS (ORDER BY rpr_res_ti.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS c1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_tja_rt AS ' + || pg_get_viewdef('rpr_res_tja_v'::regclass, true) \gexec +CREATE VIEW rpr_res_tja_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_ti, + (JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + c1 integer PATH '$' + ) + ) jt(k) + JOIN rpr_res_tb rpr_res_tb(k, y) USING (k)) j(k, c1) + WINDOW w AS (ORDER BY rpr_res_ti.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS c1 > 0); +SELECT pg_get_viewdef('rpr_res_tja_v'::regclass, true) + = pg_get_viewdef('rpr_res_tja_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_tja_v; + cnt +----- + 4 + 0 + 0 + 0 +(4 rows) + +SELECT * FROM rpr_res_tja_rt; + cnt +----- + 4 + 0 + 0 + 0 +(4 rows) + +SELECT pg_get_viewdef('rpr_res_tjb_v'::regclass, true); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT count(*) OVER w AS cnt + + FROM (rpr_res_tb rpr_res_tb(a, y) + + JOIN JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + + COLUMNS ( + + c1 integer PATH '$' + + ) + + ) jt(a) USING (a)) j(a, c1) + + WINDOW w AS (ORDER BY j.a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS c1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_tjb_rt AS ' + || pg_get_viewdef('rpr_res_tjb_v'::regclass, true) \gexec +CREATE VIEW rpr_res_tjb_rt AS SELECT count(*) OVER w AS cnt + FROM (rpr_res_tb rpr_res_tb(a, y) + JOIN JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + c1 integer PATH '$' + ) + ) jt(a) USING (a)) j(a, c1) + WINDOW w AS (ORDER BY j.a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS c1 > 0); +SELECT pg_get_viewdef('rpr_res_tjb_v'::regclass, true) + = pg_get_viewdef('rpr_res_tjb_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_tjb_v; + cnt +----- + 2 + 0 +(2 rows) + +SELECT * FROM rpr_res_tjb_rt; + cnt +----- + 2 + 0 +(2 rows) + +DROP VIEW rpr_res_tjb_rt, rpr_res_tjb_v; +DROP VIEW rpr_res_tja_rt, rpr_res_tja_v; +DROP VIEW rpr_res_tjr_rt, rpr_res_tjr_v; +DROP VIEW rpr_res_tj_rt, rpr_res_tj_v; +DROP TABLE rpr_res_tj, rpr_res_tb, rpr_res_ti; +-- The name a USING clause is given for an aliased join has to stay clear of +-- the names the join's alias list already carries, and of what a DEFINE +-- clause reads among them. The anonymous FULL JOIN takes the plain spelling +-- first, so the second USING has to move past both. +CREATE TABLE rpr_res_fa (x INT); +CREATE TABLE rpr_res_fb (x INT); +CREATE TABLE rpr_res_fc (x INT, y INT); +INSERT INTO rpr_res_fa VALUES (1); +INSERT INTO rpr_res_fb VALUES (1); +INSERT INTO rpr_res_fc VALUES (1, 3); +CREATE VIEW rpr_res_fa_v AS +SELECT count(*) OVER w AS cnt +FROM (rpr_res_fa FULL JOIN rpr_res_fb USING (x)), + ((rpr_res_fc JOIN rpr_res_fa t4 USING (x)) AS j(x, x_1) + JOIN rpr_res_fb t5 USING (x)) +WINDOW w AS (ORDER BY j.x + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS x_1 > 0); +SELECT pg_get_viewdef('rpr_res_fa_v'::regclass, true); + pg_get_viewdef +-------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_fa + + FULL JOIN rpr_res_fb USING (x), + + (rpr_res_fc rpr_res_fc(x_2, y) + + JOIN rpr_res_fa t4(x_2) USING (x_2)) j(x_2, x_1) + + JOIN rpr_res_fb t5(x_2) USING (x_2) + + WINDOW w AS (ORDER BY j.x_2 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS x_1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_fa_rt AS ' + || pg_get_viewdef('rpr_res_fa_v'::regclass, true) \gexec +CREATE VIEW rpr_res_fa_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_fa + FULL JOIN rpr_res_fb USING (x), + (rpr_res_fc rpr_res_fc(x_2, y) + JOIN rpr_res_fa t4(x_2) USING (x_2)) j(x_2, x_1) + JOIN rpr_res_fb t5(x_2) USING (x_2) + WINDOW w AS (ORDER BY j.x_2 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS x_1 > 0); +SELECT pg_get_viewdef('rpr_res_fa_v'::regclass, true) + = pg_get_viewdef('rpr_res_fa_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_fa_v; + cnt +----- + 1 +(1 row) + +SELECT * FROM rpr_res_fa_rt; + cnt +----- + 1 +(1 row) + +DROP VIEW rpr_res_fa_rt, rpr_res_fa_v; +DROP TABLE rpr_res_fa, rpr_res_fb, rpr_res_fc; +-- A TABLEFUNC that merges through an aliased join, or that carries a column +-- alias list of its own, is no different: when a relation column is renamed +-- onto the TABLEFUNC's name, it is the TABLEFUNC column that moves, and a +-- third RTE that already spells the name it moves to is left alone, that +-- name being read nowhere unqualified. +CREATE TABLE rpr_res_tk (id INT, y INT); +INSERT INTO rpr_res_tk VALUES (1, 1), (2, 2); +CREATE TABLE rpr_res_th (x INT, z INT); +INSERT INTO rpr_res_th VALUES (1, 1), (2, 2); +CREATE TABLE rpr_res_ta (id INT, c1 INT); +INSERT INTO rpr_res_ta VALUES (1, 1), (2, 2); +CREATE TABLE rpr_res_ts (id INT, s INT); +INSERT INTO rpr_res_ts VALUES (1, 1), (2, 2); +CREATE TABLE rpr_res_to (c1_1 INT); +INSERT INTO rpr_res_to VALUES (9); +CREATE VIEW rpr_res_tk_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_tk, + (JSON_TABLE(jsonb '[1,2]', '$[*]' COLUMNS (x int PATH '$')) AS jt + JOIN rpr_res_th USING (x)) j +WINDOW w AS (ORDER BY rpr_res_tk.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS y > 0); +CREATE VIEW rpr_res_ta_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_ta, + JSON_TABLE(jsonb '[1,2]', '$[*]' COLUMNS (c1 int PATH '$')) AS jx(a) +WINDOW w AS (ORDER BY rpr_res_ta.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS c1 > 0); +CREATE VIEW rpr_res_ts_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_ts, rpr_res_to, + JSON_TABLE(jsonb '[1,2]', '$[*]' COLUMNS (c1 int PATH '$')) AS jx +WINDOW w AS (ORDER BY rpr_res_ts.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS s > 0); +ALTER TABLE rpr_res_tk RENAME y TO x; +ALTER TABLE rpr_res_ts RENAME s TO c1; +SELECT pg_get_viewdef('rpr_res_tk_v'::regclass, true); + pg_get_viewdef +---------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_tk, + + (JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + + COLUMNS ( + + x integer PATH '$' + + ) + + ) jt(x_1) + + JOIN rpr_res_th rpr_res_th(x_1, z) USING (x_1)) j + + WINDOW w AS (ORDER BY rpr_res_tk.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS x > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_tk_rt AS ' + || pg_get_viewdef('rpr_res_tk_v'::regclass, true) \gexec +CREATE VIEW rpr_res_tk_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_tk, + (JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + x integer PATH '$' + ) + ) jt(x_1) + JOIN rpr_res_th rpr_res_th(x_1, z) USING (x_1)) j + WINDOW w AS (ORDER BY rpr_res_tk.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS x > 0); +SELECT pg_get_viewdef('rpr_res_tk_v'::regclass, true) + = pg_get_viewdef('rpr_res_tk_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_tk_v; + cnt +----- + 4 + 0 + 0 + 0 +(4 rows) + +SELECT * FROM rpr_res_tk_rt; + cnt +----- + 4 + 0 + 0 + 0 +(4 rows) + +SELECT pg_get_viewdef('rpr_res_ta_v'::regclass, true); + pg_get_viewdef +---------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_ta, + + JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + + COLUMNS ( + + c1 integer PATH '$' + + ) + + ) jx(a) + + WINDOW w AS (ORDER BY rpr_res_ta.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS c1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_ta_rt AS ' + || pg_get_viewdef('rpr_res_ta_v'::regclass, true) \gexec +CREATE VIEW rpr_res_ta_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_ta, + JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + c1 integer PATH '$' + ) + ) jx(a) + WINDOW w AS (ORDER BY rpr_res_ta.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS c1 > 0); +SELECT pg_get_viewdef('rpr_res_ta_v'::regclass, true) + = pg_get_viewdef('rpr_res_ta_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_ta_v; + cnt +----- + 4 + 0 + 0 + 0 +(4 rows) + +SELECT * FROM rpr_res_ta_rt; + cnt +----- + 4 + 0 + 0 + 0 +(4 rows) + +SELECT pg_get_viewdef('rpr_res_ts_v'::regclass, true); + pg_get_viewdef +---------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_ts, + + rpr_res_to, + + JSON_TABLE( + + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + + COLUMNS ( + + c1 integer PATH '$' + + ) + + ) jx(c1_1) + + WINDOW w AS (ORDER BY rpr_res_ts.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS c1 > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_ts_rt AS ' + || pg_get_viewdef('rpr_res_ts_v'::regclass, true) \gexec +CREATE VIEW rpr_res_ts_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_ts, + rpr_res_to, + JSON_TABLE( + '[1, 2]'::jsonb, '$[*]' AS json_table_path_0 + COLUMNS ( + c1 integer PATH '$' + ) + ) jx(c1_1) + WINDOW w AS (ORDER BY rpr_res_ts.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS c1 > 0); +SELECT pg_get_viewdef('rpr_res_ts_v'::regclass, true) + = pg_get_viewdef('rpr_res_ts_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_ts_v; + cnt +----- + 4 + 0 + 0 + 0 +(4 rows) + +SELECT * FROM rpr_res_ts_rt; + cnt +----- + 4 + 0 + 0 + 0 +(4 rows) + +DROP VIEW rpr_res_ts_rt, rpr_res_ts_v; +DROP VIEW rpr_res_ta_rt, rpr_res_ta_v; +DROP VIEW rpr_res_tk_rt, rpr_res_tk_v; +DROP TABLE rpr_res_tk, rpr_res_th, rpr_res_ta, rpr_res_ts, rpr_res_to; +-- A merged column the DEFINE clause reads is named by the DEFINE clause, not +-- by the USING clause: the name settled for it is the one the merge adopts. +-- So a join whose alias list renames the merged column prints the same text +-- with the DEFINE clause as without it, and a merge two joins deep is reached +-- through the input the reference resolves to. A column that turns up later +-- under the same name elsewhere is the one that moves. +CREATE TABLE rpr_res_ma (x INT, y INT); +CREATE TABLE rpr_res_mb (x INT, z INT); +CREATE TABLE rpr_res_mc (x INT, r INT); +CREATE TABLE rpr_res_mo (xx INT); +INSERT INTO rpr_res_ma VALUES (1, 1), (2, 2); +INSERT INTO rpr_res_mb VALUES (1, 1), (2, 2); +INSERT INTO rpr_res_mc VALUES (1, 1), (2, 2); +INSERT INTO rpr_res_mo VALUES (1); +CREATE VIEW rpr_res_ma_v AS +SELECT count(*) OVER w AS cnt +FROM (rpr_res_ma JOIN rpr_res_mb USING (x)) AS j(x1, y, z) +WINDOW w AS (ORDER BY j.y + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS x1 > 0); +CREATE VIEW rpr_res_ma_nodef AS +SELECT count(*) OVER w AS cnt +FROM (rpr_res_ma JOIN rpr_res_mb USING (x)) AS j(x1, y, z) +WINDOW w AS (ORDER BY j.y + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING); +CREATE VIEW rpr_res_mm_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_res_mo, (rpr_res_ma JOIN rpr_res_mb USING (x)) JOIN rpr_res_mc USING (x) +WINDOW w AS (ORDER BY rpr_res_ma.y + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS x > 0); +SELECT pg_get_viewdef('rpr_res_ma_v'::regclass, true); + pg_get_viewdef +------------------------------------------------------------------------------ + SELECT count(*) OVER w AS cnt + + FROM (rpr_res_ma rpr_res_ma(x1, y) + + JOIN rpr_res_mb rpr_res_mb(x1, z) USING (x1)) j + + WINDOW w AS (ORDER BY j.y ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS x1 > 0); +(1 row) + +SELECT pg_get_viewdef('rpr_res_ma_nodef'::regclass, true); + pg_get_viewdef +-------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM (rpr_res_ma rpr_res_ma(x1, y) + + JOIN rpr_res_mb rpr_res_mb(x1, z) USING (x1)) j + + WINDOW w AS (ORDER BY j.y ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING); +(1 row) + +SELECT 'CREATE VIEW rpr_res_ma_rt AS ' + || pg_get_viewdef('rpr_res_ma_v'::regclass, true) \gexec +CREATE VIEW rpr_res_ma_rt AS SELECT count(*) OVER w AS cnt + FROM (rpr_res_ma rpr_res_ma(x1, y) + JOIN rpr_res_mb rpr_res_mb(x1, z) USING (x1)) j + WINDOW w AS (ORDER BY j.y ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS x1 > 0); +SELECT pg_get_viewdef('rpr_res_ma_v'::regclass, true) + = pg_get_viewdef('rpr_res_ma_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_ma_v; + cnt +----- + 2 + 0 +(2 rows) + +SELECT * FROM rpr_res_ma_rt; + cnt +----- + 2 + 0 +(2 rows) + +SELECT pg_get_viewdef('rpr_res_mm_v'::regclass, true); + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_mo, + + rpr_res_ma + + JOIN rpr_res_mb USING (x) + + JOIN rpr_res_mc USING (x) + + WINDOW w AS (ORDER BY rpr_res_ma.y ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS x > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_mm_rt AS ' + || pg_get_viewdef('rpr_res_mm_v'::regclass, true) \gexec +CREATE VIEW rpr_res_mm_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_mo, + rpr_res_ma + JOIN rpr_res_mb USING (x) + JOIN rpr_res_mc USING (x) + WINDOW w AS (ORDER BY rpr_res_ma.y ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS x > 0); +SELECT pg_get_viewdef('rpr_res_mm_v'::regclass, true) + = pg_get_viewdef('rpr_res_mm_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_mm_v; + cnt +----- + 2 + 0 +(2 rows) + +SELECT * FROM rpr_res_mm_rt; + cnt +----- + 2 + 0 +(2 rows) + +-- the name turns up elsewhere only now +DROP VIEW rpr_res_mm_rt; +ALTER TABLE rpr_res_mo RENAME xx TO x; +SELECT pg_get_viewdef('rpr_res_mm_v'::regclass, true); + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_res_mo rpr_res_mo(x_1), + + rpr_res_ma + + JOIN rpr_res_mb USING (x) + + JOIN rpr_res_mc USING (x) + + WINDOW w AS (ORDER BY rpr_res_ma.y ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS x > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_res_mm_rt AS ' + || pg_get_viewdef('rpr_res_mm_v'::regclass, true) \gexec +CREATE VIEW rpr_res_mm_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_res_mo rpr_res_mo(x_1), + rpr_res_ma + JOIN rpr_res_mb USING (x) + JOIN rpr_res_mc USING (x) + WINDOW w AS (ORDER BY rpr_res_ma.y ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS x > 0); +SELECT pg_get_viewdef('rpr_res_mm_v'::regclass, true) + = pg_get_viewdef('rpr_res_mm_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_res_mm_v; + cnt +----- + 2 + 0 +(2 rows) + +SELECT * FROM rpr_res_mm_rt; + cnt +----- + 2 + 0 +(2 rows) + +DROP VIEW rpr_res_mm_rt, rpr_res_mm_v; +DROP VIEW rpr_res_ma_rt, rpr_res_ma_nodef, rpr_res_ma_v; +DROP TABLE rpr_res_ma, rpr_res_mb, rpr_res_mc, rpr_res_mo; +-- Deparsing a query whose DEFINE clause reads the grouping step expands the +-- clause's GROUP Vars into the grouping expressions, and over a column merged +-- by a FULL JOIN USING that expands to the COALESCE the parser built for the +-- merge. Printed, both arms come out spelled the same -- a DEFINE clause +-- carries no qualifier -- so COALESCE(id, id) says nothing about where either +-- came from, and re-parsing nests one merged column inside another. The join +-- RTE still holds what it built, so the expansion is folded back into the +-- merged column itself. +CREATE TABLE rpr_cds_l (id INT PRIMARY KEY, val INT); +CREATE TABLE rpr_cds_r (id INT, val INT); +CREATE TABLE rpr_cds_o (k INT); +INSERT INTO rpr_cds_l VALUES (1, 1), (2, 2); +INSERT INTO rpr_cds_r VALUES (1, 1), (3, 3); +INSERT INTO rpr_cds_o VALUES (1), (2); +CREATE VIEW rpr_cds_v AS +SELECT COALESCE(rpr_cds_l.id, rpr_cds_r.id) + 1 AS idp1, count(*) OVER w AS cnt +FROM rpr_cds_l FULL JOIN rpr_cds_r USING (id) +GROUP BY COALESCE(rpr_cds_l.id, rpr_cds_r.id) + 1 +WINDOW w AS (ORDER BY COALESCE(rpr_cds_l.id, rpr_cds_r.id) + 1 + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id + 1 > 0); +SELECT pg_get_viewdef('rpr_cds_v'::regclass, true); + pg_get_viewdef +--------------------------------------------------------------------------------------------------------------------- + SELECT COALESCE(rpr_cds_l.id, rpr_cds_r.id) + 1 AS idp1, + + count(*) OVER w AS cnt + + FROM rpr_cds_l + + FULL JOIN rpr_cds_r USING (id) + + GROUP BY (COALESCE(rpr_cds_l.id, rpr_cds_r.id) + 1) + + WINDOW w AS (ORDER BY (COALESCE(rpr_cds_l.id, rpr_cds_r.id) + 1) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS (id + 1) > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_cds_rt AS ' + || pg_get_viewdef('rpr_cds_v'::regclass, true) \gexec +CREATE VIEW rpr_cds_rt AS SELECT COALESCE(rpr_cds_l.id, rpr_cds_r.id) + 1 AS idp1, + count(*) OVER w AS cnt + FROM rpr_cds_l + FULL JOIN rpr_cds_r USING (id) + GROUP BY (COALESCE(rpr_cds_l.id, rpr_cds_r.id) + 1) + WINDOW w AS (ORDER BY (COALESCE(rpr_cds_l.id, rpr_cds_r.id) + 1) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS (id + 1) > 0); +SELECT pg_get_viewdef('rpr_cds_v'::regclass, true) + = pg_get_viewdef('rpr_cds_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_cds_v ORDER BY idp1; + idp1 | cnt +------+----- + 2 | 3 + 3 | 0 + 4 | 0 +(3 rows) + +SELECT * FROM rpr_cds_rt ORDER BY idp1; + idp1 | cnt +------+----- + 2 | 3 + 3 | 0 + 4 | 0 +(3 rows) + +-- An outer join above the merge marks the copy the grouping expression +-- carries and not the copy the join RTE keeps. Neither mark reaches the +-- printed text, so the two still name one column. +CREATE VIEW rpr_cds_null_v AS +SELECT COALESCE(l.id, r.id) + 1 AS idp1, count(*) OVER w AS cnt +FROM rpr_cds_o LEFT JOIN (rpr_cds_l l FULL JOIN rpr_cds_r r USING (id)) ON true +GROUP BY COALESCE(l.id, r.id) + 1 +WINDOW w AS (ORDER BY COALESCE(l.id, r.id) + 1 + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id + 1 > 0); +SELECT pg_get_viewdef('rpr_cds_null_v'::regclass, true); + pg_get_viewdef +----------------------------------------------------------------------------------------------------- + SELECT COALESCE(l.id, r.id) + 1 AS idp1, + + count(*) OVER w AS cnt + + FROM rpr_cds_o + + LEFT JOIN (rpr_cds_l l + + FULL JOIN rpr_cds_r r USING (id)) ON true + + GROUP BY (COALESCE(l.id, r.id) + 1) + + WINDOW w AS (ORDER BY (COALESCE(l.id, r.id) + 1) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS (id + 1) > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_cds_null_rt AS ' + || pg_get_viewdef('rpr_cds_null_v'::regclass, true) \gexec +CREATE VIEW rpr_cds_null_rt AS SELECT COALESCE(l.id, r.id) + 1 AS idp1, + count(*) OVER w AS cnt + FROM rpr_cds_o + LEFT JOIN (rpr_cds_l l + FULL JOIN rpr_cds_r r USING (id)) ON true + GROUP BY (COALESCE(l.id, r.id) + 1) + WINDOW w AS (ORDER BY (COALESCE(l.id, r.id) + 1) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS (id + 1) > 0); +SELECT pg_get_viewdef('rpr_cds_null_v'::regclass, true) + = pg_get_viewdef('rpr_cds_null_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_cds_null_v ORDER BY idp1; + idp1 | cnt +------+----- + 2 | 3 + 3 | 0 + 4 | 0 +(3 rows) + +SELECT * FROM rpr_cds_null_rt ORDER BY idp1; + idp1 | cnt +------+----- + 2 | 3 + 3 | 0 + 4 | 0 +(3 rows) + +-- The same nesting under a join that nulls nothing leaves both copies +-- unmarked, and they still name one column. +CREATE VIEW rpr_cds_inner_v AS +SELECT COALESCE(l.id, r.id) + 1 AS idp1, count(*) OVER w AS cnt +FROM (rpr_cds_l l FULL JOIN rpr_cds_r r USING (id)) JOIN rpr_cds_o ON true +GROUP BY COALESCE(l.id, r.id) + 1 +WINDOW w AS (ORDER BY COALESCE(l.id, r.id) + 1 + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id + 1 > 0); +SELECT pg_get_viewdef('rpr_cds_inner_v'::regclass, true); + pg_get_viewdef +----------------------------------------------------------------------------------------------------- + SELECT COALESCE(l.id, r.id) + 1 AS idp1, + + count(*) OVER w AS cnt + + FROM rpr_cds_l l + + FULL JOIN rpr_cds_r r USING (id) + + JOIN rpr_cds_o ON true + + GROUP BY (COALESCE(l.id, r.id) + 1) + + WINDOW w AS (ORDER BY (COALESCE(l.id, r.id) + 1) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS (id + 1) > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_cds_inner_rt AS ' + || pg_get_viewdef('rpr_cds_inner_v'::regclass, true) \gexec +CREATE VIEW rpr_cds_inner_rt AS SELECT COALESCE(l.id, r.id) + 1 AS idp1, + count(*) OVER w AS cnt + FROM rpr_cds_l l + FULL JOIN rpr_cds_r r USING (id) + JOIN rpr_cds_o ON true + GROUP BY (COALESCE(l.id, r.id) + 1) + WINDOW w AS (ORDER BY (COALESCE(l.id, r.id) + 1) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS (id + 1) > 0); +SELECT pg_get_viewdef('rpr_cds_inner_v'::regclass, true) + = pg_get_viewdef('rpr_cds_inner_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_cds_inner_v ORDER BY idp1; + idp1 | cnt +------+----- + 2 | 3 + 3 | 0 + 4 | 0 +(3 rows) + +SELECT * FROM rpr_cds_inner_rt ORDER BY idp1; + idp1 | cnt +------+----- + 2 | 3 + 3 | 0 + 4 | 0 +(3 rows) + +-- A merge built over another merge -- a FULL JOIN USING above a FULL JOIN +-- USING -- expands to a COALESCE over a COALESCE. The inner one is folded +-- first, so that the outer one is seen whole and folds in turn; folded +-- inside out, it re-parses to itself rather than to twice the nesting. +CREATE VIEW rpr_cds_nest_v AS +SELECT count(*) OVER w AS cnt +FROM (rpr_cds_l FULL JOIN rpr_cds_r USING (id)) FULL JOIN rpr_cds_r t USING (id) +GROUP BY rpr_cds_l.id, rpr_cds_r.id, t.id +WINDOW w AS (ORDER BY rpr_cds_l.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id > 0); +SELECT pg_get_viewdef('rpr_cds_nest_v'::regclass, true); + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_cds_l + + FULL JOIN rpr_cds_r USING (id) + + FULL JOIN rpr_cds_r t USING (id) + + GROUP BY rpr_cds_l.id, rpr_cds_r.id, t.id + + WINDOW w AS (ORDER BY rpr_cds_l.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS id > 0); +(1 row) + +SELECT 'CREATE VIEW rpr_cds_nest_rt AS ' + || pg_get_viewdef('rpr_cds_nest_v'::regclass, true) \gexec +CREATE VIEW rpr_cds_nest_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_cds_l + FULL JOIN rpr_cds_r USING (id) + FULL JOIN rpr_cds_r t USING (id) + GROUP BY rpr_cds_l.id, rpr_cds_r.id, t.id + WINDOW w AS (ORDER BY rpr_cds_l.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS id > 0); +SELECT pg_get_viewdef('rpr_cds_nest_v'::regclass, true) + = pg_get_viewdef('rpr_cds_nest_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_cds_nest_v; + cnt +----- + 3 + 0 + 0 +(3 rows) + +SELECT * FROM rpr_cds_nest_rt; + cnt +----- + 3 + 0 + 0 +(3 rows) + +DROP VIEW rpr_cds_nest_rt, rpr_cds_nest_v; +DROP VIEW rpr_cds_inner_rt, rpr_cds_inner_v; +DROP VIEW rpr_cds_null_rt, rpr_cds_null_v; +DROP VIEW rpr_cds_rt, rpr_cds_v; +DROP TABLE rpr_cds_l, rpr_cds_r, rpr_cds_o; +-- A rule deparses with varprefix on no matter what its action query looks +-- like, the range table always holding *OLD* and *NEW*, so a DEFINE clause in +-- one would be printed with qualifiers that the parser rejects outright. +-- get_rule_define() turns the prefix off for the clause, and without that a +-- rule holding a row pattern query could not be restored at all -- not even a +-- single-table one, which is what makes this its own case and not the view's. +CREATE TABLE rpr_rule_t (id INT, val INT); +CREATE TABLE rpr_rule_log (id INT, cnt BIGINT); +CREATE RULE rpr_rule_r AS ON INSERT TO rpr_rule_t DO ALSO + INSERT INTO rpr_rule_log + SELECT id, count(*) OVER w FROM rpr_rule_t + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0); +SELECT pg_get_ruledef(oid, true) FROM pg_rewrite WHERE rulename = 'rpr_rule_r'; + pg_get_ruledef +------------------------------------------------------------------------------------------------ + CREATE RULE rpr_rule_r AS + + ON INSERT TO rpr_rule_t DO INSERT INTO rpr_rule_log (id, cnt) SELECT rpr_rule_t.id, + + count(*) OVER w AS count + + FROM rpr_rule_t + + WINDOW w AS (ORDER BY rpr_rule_t.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS val > 0); +(1 row) + +-- and that text is what has to reparse +CREATE TABLE rpr_rule_saved AS + SELECT pg_get_ruledef(oid, true) AS def + FROM pg_rewrite WHERE rulename = 'rpr_rule_r'; +DROP RULE rpr_rule_r ON rpr_rule_t; +SELECT def FROM rpr_rule_saved \gexec +CREATE RULE rpr_rule_r AS + ON INSERT TO rpr_rule_t DO INSERT INTO rpr_rule_log (id, cnt) SELECT rpr_rule_t.id, + count(*) OVER w AS count + FROM rpr_rule_t + WINDOW w AS (ORDER BY rpr_rule_t.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS val > 0); +SELECT (SELECT def FROM rpr_rule_saved) = pg_get_ruledef(oid, true) AS round_trips + FROM pg_rewrite WHERE rulename = 'rpr_rule_r'; + round_trips +------------- + t +(1 row) + +-- the restored rule still fires. A rule action is run against the rows the +-- statement supplies as well as the table, so a two-row INSERT gives the +-- window four rows to order by two distinct ids; sort the result on both +-- columns, the pairs within an id being interchangeable. +INSERT INTO rpr_rule_t VALUES (1, 1), (2, 2); +SELECT * FROM rpr_rule_log ORDER BY id, cnt; + id | cnt +----+----- + 1 | 0 + 1 | 4 + 2 | 0 + 2 | 0 +(4 rows) + +DROP TABLE rpr_rule_saved; +DROP TABLE rpr_rule_t, rpr_rule_log; +-- A relation alias that happens to spell a pattern variable is the other way +-- the prefix goes wrong. Printed as up.price it does not come back as a +-- range variable qualifier, which is merely rejected, but as a pattern +-- variable one, which is rejected by a different rule and with a different +-- message. Two RTEs are what turns the prefix on. +CREATE TABLE rpr_pvar_a (id INT, price INT); +CREATE TABLE rpr_pvar_b (id INT); +INSERT INTO rpr_pvar_a VALUES (1, 10), (2, 20), (3, 5); +INSERT INTO rpr_pvar_b VALUES (1), (2), (3); +CREATE VIEW rpr_pvar_v AS +SELECT count(*) OVER w AS cnt +FROM rpr_pvar_a up, rpr_pvar_b +WHERE up.id = rpr_pvar_b.id +WINDOW w AS (ORDER BY up.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (up+) + DEFINE up AS price > 0); +SELECT pg_get_viewdef('rpr_pvar_v'::regclass, true); + pg_get_viewdef +-------------------------------------------------------------------------------- + SELECT count(*) OVER w AS cnt + + FROM rpr_pvar_a up, + + rpr_pvar_b + + WHERE up.id = rpr_pvar_b.id + + WINDOW w AS (ORDER BY up.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 'CREATE VIEW rpr_pvar_rt AS ' + || pg_get_viewdef('rpr_pvar_v'::regclass, true) \gexec +CREATE VIEW rpr_pvar_rt AS SELECT count(*) OVER w AS cnt + FROM rpr_pvar_a up, + rpr_pvar_b + WHERE up.id = rpr_pvar_b.id + WINDOW w AS (ORDER BY up.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (up+) + DEFINE + up AS price > 0); +SELECT pg_get_viewdef('rpr_pvar_v'::regclass, true) + = pg_get_viewdef('rpr_pvar_rt'::regclass, true) AS round_trips; + round_trips +------------- + t +(1 row) + +SELECT * FROM rpr_pvar_v; + cnt +----- + 3 + 0 + 0 +(3 rows) + +SELECT * FROM rpr_pvar_rt; + cnt +----- + 3 + 0 + 0 +(3 rows) + +DROP VIEW rpr_pvar_rt, rpr_pvar_v; +DROP TABLE rpr_pvar_a, rpr_pvar_b; +-- 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 copyfuncs.c via the plan cache) +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 +); +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 +); +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 +); +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 (not inlined; planned as a CTE scan) +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; + 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; + 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 -- A rows 1-3 and 5, B rows 4 and 6 -- 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); + 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: invalid 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: invalid 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: invalid 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: invalid 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: invalid 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: invalid 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: invalid 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: invalid 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): rejected like any +-- other qualified name, not reported as a missing FROM-clause entry, since +-- adding one would only lead to the range variable error above +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: qualified expression "nosuch.val" is not allowed in DEFINE clause +LINE 7: DEFINE A AS nosuch.val > 0 + ^ +-- A three-part name is schema-qualified even when its first part spells a +-- pattern variable, and is rejected as any other qualified name +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (public+) + DEFINE public AS public.rpr_err.val > 0 +); +ERROR: qualified expression "public.rpr_err.val" is not allowed in DEFINE clause +LINE 7: DEFINE public AS public.rpr_err.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 + ^ +-- A trailing star on a composite column is a different thing from a trailing +-- star on a relation: it names no relation, so the row constructor keeps +-- expanding it and the DEFINE restrictions do not apply. +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 ROW((items).*) IS NOT NULL +); + count +------- + 3 + 0 + 0 +(3 rows) + +DROP TABLE rpr_composite; +DROP TYPE rpr_item; +-- A composite value that reaches DEFINE by way of a subquery Var only takes +-- its ROW(...) shape after pullup, and the ORDER BY copy's sortgroupref +-- keeps it from being flattened. make_window_input_target() adds the fields +-- the split leaves behind. +CREATE TABLE rpr_ordrow (a int, b int); +INSERT INTO rpr_ordrow SELECT g, g % 4 FROM generate_series(1, 10) g; +SELECT count(*) OVER w AS c +FROM (SELECT ROW(a, b) AS x FROM rpr_ordrow) s +WINDOW w AS (ORDER BY x + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL PATTERN (P Q+) DEFINE P AS TRUE, Q AS x IS NOT NULL); + c +---- + 10 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +(10 rows) + +-- Control: without ORDER BY, x is flattened normally and this succeeds too. +SELECT count(*) OVER w AS c +FROM (SELECT ROW(a, b) AS x FROM rpr_ordrow) s +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL PATTERN (P Q+) DEFINE P AS TRUE, Q AS x IS NOT NULL); + c +---- + 10 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +(10 rows) + +DROP TABLE rpr_ordrow; +-- The same split by way of a pulled-up composite target, both as a plain +-- subquery and as a view. Rows with equal a share a partition, so q is +-- tested and DEFINE really reads k. +CREATE TABLE rpr_partrow (a int, b int); +INSERT INTO rpr_partrow VALUES (1, 1), (1, 2), (1, 3), (2, 4); +SELECT count(*) OVER w +FROM (SELECT b, row(a, 1) AS k FROM rpr_partrow) s +WINDOW w AS (PARTITION BY k ORDER BY b + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (p q+) DEFINE q AS k IS NOT NULL); + count +------- + 3 + 0 + 0 + 0 +(4 rows) + +CREATE TYPE rpr_partrow_t AS (x int, y int); +CREATE VIEW rpr_partrow_v AS SELECT b, row(a, 1)::rpr_partrow_t AS k FROM rpr_partrow; +SELECT count(*) OVER w FROM rpr_partrow_v +WINDOW w AS (PARTITION BY k ORDER BY b + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (p q+) DEFINE q AS k IS NOT NULL); + count +------- + 3 + 0 + 0 + 0 +(4 rows) + +-- Control: PATTERN/DEFINE aside, the same window clause runs fine. +SELECT count(*) OVER w +FROM (SELECT b, row(a, 1) AS k FROM rpr_partrow) s +WINDOW w AS (PARTITION BY k ORDER BY b + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING); + count +------- + 3 + 2 + 1 + 1 +(4 rows) + +DROP VIEW rpr_partrow_v; +DROP TYPE rpr_partrow_t; +DROP TABLE rpr_partrow; +-- 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: grouping operation in DEFINE is not supported. This shares the +-- EXPR_KIND_RPR_DEFINE arm with the aggregate case above, but takes the +-- GroupingFunc half of it. parseCheckAggregates() relies on this rejection +-- to leave DEFINE out of finalize_grouping_exprs(). +SELECT COUNT(*) OVER w +FROM rpr_err +GROUP BY id +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS GROUPING(id) = 0 +); +ERROR: grouping operations are not allowed in DEFINE +LINE 8: DEFINE A AS GROUPING(id) = 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 +); +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 +); + 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 +); + 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 +); + 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 like (A{2,3}){2,3} above, 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) + +-- Unwrapped GROUPs then VAR merge: (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) + +-- Quantifier multiply 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: (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: SUFFIX merge after GROUP{1,1} unwrap +-- (A B)+? (A B) stays as (a b)+? a b (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 quantifier multiply folds the 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 | 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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) + +-- Nested Unbounded: only the inner GROUP is absorbable +-- Pattern: ((A B)+ C)+ - inner (A B)+ absorbable on the first iteration +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 +); + 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 +); + 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 +); + 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 +); + 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) + +-- Measuring a group body for a fixed row count. The suffix merge measures +-- the body of a group followed by more of the sequence, so each pattern below +-- puts the shape under test inside such a group. Each measurement must +-- report "not a fixed length": the first because its repetition count is a +-- range, the other two instead of overflowing. Only the first pattern can +-- match real rows. +-- A nested group whose repetition count is a range has no fixed length +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){1,2} C)+ D) + DEFINE A AS val <= 30, B AS val > 30, C AS val > 0, D AS val > 0 +); + id | val | cnt +----+-----+----- + 1 | 10 | 0 + 2 | 20 | 0 + 3 | 30 | 4 + 4 | 40 | 0 + 5 | 50 | 0 + 6 | 60 | 0 + 7 | 70 | 0 + 8 | 80 | 0 + 9 | 90 | 0 + 10 | 100 | 0 +(10 rows) + +-- A fixed repetition whose body length times its count reaches the ceiling +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){1000000000})+ D) + DEFINE A AS val <= 30, B AS val > 30, C AS val > 0, D AS val > 0 +); + 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) + +-- A body whose members sum past the ceiling +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{2000000000} B{2000000000})+ C) + DEFINE A AS val <= 30, B AS val > 30, C AS val > 0 +); + 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 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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; + 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; + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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; +-- A mismatched-type USING merge column used to let the other side's +-- pulled-up constant fold into a navigation argument at plan time. +CREATE TABLE rpr_join4 (k bigint); +INSERT INTO rpr_join4 VALUES (10); +SELECT count(*) OVER w AS cnt +FROM (SELECT 10 AS k) a LEFT JOIN rpr_join4 USING (k) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS PREV(k / 0) > 0 +); + cnt +----- + 0 +(1 row) + +-- Same, but RIGHT JOIN with the constant subquery on its preserved +-- (right) side. +SELECT count(*) OVER w AS cnt +FROM rpr_join4 RIGHT JOIN (SELECT 10 AS k) a USING (k) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS PREV(k / 0) > 0 +); + cnt +----- + 0 +(1 row) + +DROP TABLE rpr_join4; +-- Same shape with real per-row data, confirming navigation still sees +-- each row's own value. +CREATE TABLE rpr_join5 (k int, v int); +INSERT INTO rpr_join5 VALUES (1, 10), (2, 0), (3, 5); +CREATE TABLE rpr_join6 (k bigint, w int); +INSERT INTO rpr_join6 VALUES (1, 1), (2, 1), (3, 1); +SELECT k, v, cnt +FROM (SELECT k, v, count(*) OVER win AS cnt + FROM rpr_join5 LEFT JOIN rpr_join6 USING (k) + WINDOW win AS ( + ORDER BY k + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(v) IS NOT NULL + )) s +ORDER BY k; + k | v | cnt +---+----+----- + 1 | 10 | 0 + 2 | 0 | 2 + 3 | 5 | 0 +(3 rows) + +DROP TABLE rpr_join5, rpr_join6; +-- A DEFINE clause reading a USING column whose two sides differ in typmod. +-- The merged column stays a join alias Var, pullup leaves its joinaliasvars +-- entry a non-trivial expression, and the outer join's nullingrels wrap that +-- in a PlaceHolderVar. The target list copy and the DEFINE copy are wrapped +-- by separate calls, so their phids differ and equal() does not match them -- +-- the window input has to carry the DEFINE clause's own PlaceHolderVar. +CREATE TABLE rpr_phv_src (n int); +CREATE TABLE rpr_phv_dim (c varchar(10), tdate date); +CREATE TABLE rpr_phv_out (k varchar); +INSERT INTO rpr_phv_src VALUES (2), (4); +INSERT INTO rpr_phv_dim VALUES ('zz', '2024-01-01'), ('zzzz', '2024-01-02'); +INSERT INTO rpr_phv_out VALUES ('zz'), ('zzzz'); +SELECT j.c, j.tdate, count(*) OVER w AS cnt +FROM rpr_phv_out o1 + LEFT JOIN ( (SELECT n, repeat('z', n)::varchar(5) AS c FROM rpr_phv_src) s + JOIN rpr_phv_dim USING (c) ) j + ON o1.k = j.c +WINDOW w AS (ORDER BY j.tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL PATTERN (p q+) + DEFINE p AS TRUE, q AS c > ''); + c | tdate | cnt +------+------------+----- + zz | 01-01-2024 | 2 + zzzz | 01-02-2024 | 0 +(2 rows) + +-- The same with one more join level above it. Reaching the window input is +-- not enough on its own: an intermediate join emits only what something above +-- has declared a need for, so what a DEFINE clause reads is marked needed at +-- relation 0 the way the target list's own columns are. +SELECT j.c, j.tdate, count(*) OVER w AS cnt +FROM rpr_phv_out o1 + LEFT JOIN rpr_phv_out o2 ON o1.k = o2.k + LEFT JOIN ( (SELECT n, repeat('z', n)::varchar(5) AS c FROM rpr_phv_src) s + JOIN rpr_phv_dim USING (c) ) j + ON o2.k = j.c +WINDOW w AS (ORDER BY j.tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL PATTERN (p q+) + DEFINE p AS TRUE, q AS c > ''); + c | tdate | cnt +------+------------+----- + zz | 01-01-2024 | 2 + zzzz | 01-02-2024 | 0 +(2 rows) + +-- Control: with both sides of USING at the same typmod the merged column is a +-- plain Var of one side, no PlaceHolderVar is built, and neither shape above +-- needs any of this. +SELECT j.c, j.tdate, count(*) OVER w AS cnt +FROM rpr_phv_out o1 + LEFT JOIN rpr_phv_out o2 ON o1.k = o2.k + LEFT JOIN ( (SELECT n, repeat('z', n)::varchar(10) AS c + FROM rpr_phv_src) s + JOIN rpr_phv_dim USING (c) ) j + ON o2.k = j.c +WINDOW w AS (ORDER BY j.tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL PATTERN (p q+) + DEFINE p AS TRUE, q AS c > ''); + c | tdate | cnt +------+------------+----- + zz | 01-01-2024 | 2 + zzzz | 01-02-2024 | 0 +(2 rows) + +DROP TABLE rpr_phv_src, rpr_phv_dim, rpr_phv_out; +-- A WINDOW clause no window function names is never executed, so its DEFINE +-- clause is emptied before build_base_rel_tlists() could mark what it reads +-- as needed at relation 0, which would keep the outer join from being +-- removed. The three plans below are the assertion: no WINDOW clause, a +-- plain one and a row pattern one all lose the join alike. +CREATE TABLE rpr_jr (id int, v int); +CREATE TABLE rpr_jr_u (id int PRIMARY KEY, uval int); +INSERT INTO rpr_jr SELECT g, g * 10 FROM generate_series(1, 5) g; +INSERT INTO rpr_jr_u SELECT g, g * 100 FROM generate_series(1, 5) g; +EXPLAIN (COSTS OFF) +SELECT t.id FROM rpr_jr t LEFT JOIN rpr_jr_u u ON t.id = u.id; + QUERY PLAN +---------------------- + Seq Scan on rpr_jr t +(1 row) + +EXPLAIN (COSTS OFF) +SELECT t.id FROM rpr_jr t LEFT JOIN rpr_jr_u u ON t.id = u.id +WINDOW w AS (ORDER BY t.id); + QUERY PLAN +---------------------- + Seq Scan on rpr_jr t +(1 row) + +EXPLAIN (COSTS OFF) +SELECT t.id FROM rpr_jr t LEFT JOIN rpr_jr_u u ON t.id = u.id +WINDOW w AS (ORDER BY t.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) DEFINE B AS uval > PREV(uval)); + QUERY PLAN +---------------------- + Seq Scan on rpr_jr t +(1 row) + +DROP TABLE rpr_jr, rpr_jr_u; +-- ============================================================ +-- 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 +); + 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 +); + 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 +); + 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 +); + 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 +); + 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 +); +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 +); +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) + +-- ------------------------------------------------------------ +-- RPR over grouped input +-- ------------------------------------------------------------ +-- A DEFINE clause is the only part of a WindowClause that holds an +-- expression tree of its own, so parseCheckAggregates() has to substitute +-- its grouped columns separately from the target list's. These pin the +-- grouping shapes that reach that substitution, and what each returns once +-- a grouping set nulls a column the pattern reads. +CREATE TABLE rpr_grp (id int PRIMARY KEY, category text, val int); +INSERT INTO rpr_grp VALUES (1, 'A', 10), (2, 'B', 20); +-- Grouped input works; the pattern matches over the grouped rows +SELECT category, sum(val) AS total, count(*) OVER w AS 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 category IS NOT NULL) +ORDER BY category; + category | total | cnt +----------+-------+----- + A | 90 | 2 + B | 120 | 0 +(2 rows) + +-- Navigation over a grouping column +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY category +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B*) + DEFINE B AS category > PREV(category)) +ORDER BY category; + category | cnt +----------+----- + A | 2 + B | 0 +(2 rows) + +-- GROUP BY () builds no RTE_GROUP, so there is no grouped column for a +-- DEFINE clause to name and nothing that could diverge +SELECT count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY () +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS true); + cnt +----- + 1 +(1 row) + +-- A single grouping set collapses to a plain GROUP BY and cannot null the +-- column +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY GROUPING SETS ((category)) +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS category IS NOT NULL) +ORDER BY category; + category | cnt +----------+----- + A | 1 + B | 1 +(2 rows) + +-- Duplicated sets leave the column in every set, so it is never nulled +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY GROUPING SETS ((category), (category)) +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS category IS NOT NULL) +ORDER BY category; + category | cnt +----------+----- + A | 1 + A | 1 + B | 1 + B | 1 +(4 rows) + +-- ROLLUP with a DEFINE clause that holds no column reference at all +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY ROLLUP(category) +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS true) +ORDER BY category NULLS LAST; + category | cnt +----------+----- + A | 1 + B | 1 + | 1 +(3 rows) + +-- The DEFINE clause names only a column that every grouping set contains, +-- so gset_common covers it and no varnullingrels are attached +SELECT category, val, count(*) OVER w AS cnt +FROM rpr_sort +WHERE val < 30 +GROUP BY category, ROLLUP(val) +WINDOW w AS ( + ORDER BY category, val + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS category IS NOT NULL) +ORDER BY category, val NULLS LAST; + category | val | cnt +----------+-----+----- + A | 10 | 1 + A | | 1 + B | 20 | 1 + B | | 1 +(4 rows) + +-- The window's own PARTITION BY and ORDER BY reference the target list, so +-- a nullable grouping column reaches them unharmed +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY ROLLUP(category) +WINDOW w AS ( + PARTITION BY category + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS true) +ORDER BY category NULLS LAST; + category | cnt +----------+----- + A | 1 + B | 1 + | 1 +(3 rows) + +-- An aggregate query without GROUP BY produces one grouped row +SELECT count(*) OVER w AS cnt +FROM rpr_sort +HAVING count(*) > 0 +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS true); + cnt +----- + 1 +(1 row) + +SELECT count(*) OVER w AS cnt, sum(val) AS total +FROM rpr_sort +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS true); + cnt | total +-----+------- + 1 | 210 +(1 row) + +-- GROUP BY together with HAVING +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY category +HAVING count(*) > 1 +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS category IS NOT NULL) +ORDER BY category; + category | cnt +----------+----- + A | 1 + B | 1 +(2 rows) + +-- A grouping key that is not a plain Var does not stand in the way of a +-- DEFINE clause that names a plain-Var grouping key +SELECT category, val + 1 AS bumped, count(*) OVER w AS cnt +FROM rpr_grp +GROUP BY val + 1, category +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS category IS NOT NULL) +ORDER BY category; + category | bumped | cnt +----------+--------+----- + A | 11 | 1 + B | 21 | 1 +(2 rows) + +-- Grouping by the primary key exposes the dependent columns +SELECT id, count(*) OVER w AS cnt +FROM rpr_grp +GROUP BY id +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS val > 0) +ORDER BY id; + id | cnt +----+----- + 1 | 1 + 2 | 1 +(2 rows) + +-- An aggregate is available to the window's ORDER BY, and it is the ordering +-- the pattern runs over: sum(val) descending puts B first, so the greedy +-- match starts there. Ordering by category instead would start at A. +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY category +WINDOW w AS ( + ORDER BY sum(val) DESC + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS category IS NOT NULL) +ORDER BY category; + category | cnt +----------+----- + A | 0 + B | 2 +(2 rows) + +-- Grouping in a subquery leaves the outer RPR window alone +SELECT category, total, count(*) OVER w AS cnt +FROM (SELECT category, sum(val) AS total FROM rpr_sort GROUP BY category) s +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B*) + DEFINE B AS total > PREV(total)) +ORDER BY category; + category | total | cnt +----------+-------+----- + A | 90 | 2 + B | 120 | 0 +(2 rows) + +-- A view over grouped input round-trips +CREATE VIEW rpr_grp_v AS +SELECT category, count(*) OVER w AS 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 category IS NOT NULL); +SELECT pg_get_viewdef('rpr_grp_v'::regclass, true); + pg_get_viewdef +----------------------------------------------------------------------------------- + SELECT category, + + count(*) OVER w AS cnt + + FROM rpr_sort + + GROUP BY category + + WINDOW w AS (ORDER BY category ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS category IS NOT NULL); +(1 row) + +SELECT * FROM rpr_grp_v ORDER BY category; + category | cnt +----------+----- + A | 2 + B | 0 +(2 rows) + +DROP VIEW rpr_grp_v; +-- ROLLUP, with a DEFINE clause naming a column it can null. The grouping +-- step's NULL reaches the predicate, which is then false, so the total row +-- is unmatched. +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY ROLLUP(category) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS category IS NOT NULL); + category | cnt +----------+----- + | 0 + B | 1 + A | 1 +(3 rows) + +-- A match that spans several grouped rows, so the reduced frame is not just +-- the current row: A+ is greedy and stops at the row ROLLUP nulled. +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY ROLLUP(category) +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS category >= 'A') +ORDER BY category NULLS LAST; + category | cnt +----------+----- + A | 2 + B | 0 + | 0 +(3 rows) + +-- The same for CUBE +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY CUBE(category) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS category IS NOT NULL); + category | cnt +----------+----- + | 0 + B | 1 + A | 1 +(3 rows) + +-- The same for an explicit set list containing the empty set +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY GROUPING SETS ((category), ()) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS category IS NOT NULL); + category | cnt +----------+----- + | 0 + B | 1 + A | 1 +(3 rows) + +-- The same when a second set simply omits the column +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY GROUPING SETS ((category), (val)) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS category IS NOT NULL); + category | cnt +----------+----- + B | 1 + A | 1 + | 0 + | 0 + | 0 + | 0 + | 0 + | 0 +(8 rows) + +-- Naming val, which ROLLUP can null, works the same way as naming category +-- above: the rows where it is nulled do not match +SELECT category, val, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY category, ROLLUP(val) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS val > 0); + category | val | cnt +----------+-----+----- + B | 40 | 1 + A | 10 | 1 + B | 60 | 1 + A | 30 | 1 + B | 20 | 1 + A | 50 | 1 + B | | 0 + A | | 0 +(8 rows) + +-- A navigation in the DEFINE clause reads the grouped column the same way +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY ROLLUP(category) +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B*) + DEFINE B AS PREV(category) IS NOT NULL); + category | cnt +----------+----- + A | 3 + B | 0 + | 0 +(3 rows) + +-- The same for a compound navigation +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY ROLLUP(category) +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B*) + DEFINE B AS PREV(LAST(category)) IS NOT NULL); + category | cnt +----------+----- + A | 3 + B | 0 + | 0 +(3 rows) + +-- The same one query level down +SELECT * FROM ( + SELECT category, count(*) OVER w AS cnt + FROM rpr_sort + GROUP BY ROLLUP(category) + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS category IS NOT NULL)) s; + category | cnt +----------+----- + | 0 + B | 1 + A | 1 +(3 rows) + +-- And in a view definition. get_query_def() expands the DEFINE clause's +-- GROUP Vars like the target list's, so the deparsed text names the column +-- rather than the grouping step and the view re-parses. +CREATE VIEW rpr_grp_v2 AS +SELECT category, count(*) OVER w AS cnt +FROM rpr_sort +GROUP BY ROLLUP(category) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS category IS NOT NULL); +SELECT pg_get_viewdef('rpr_grp_v2'::regclass, true); + pg_get_viewdef +----------------------------------------------------------------- + SELECT category, + + count(*) OVER w AS cnt + + FROM rpr_sort + + GROUP BY ROLLUP(category) + + WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a) + + DEFINE + + a AS category IS NOT NULL); +(1 row) + +SELECT * FROM rpr_grp_v2 ORDER BY category NULLS LAST; + category | cnt +----------+----- + A | 1 + B | 1 + | 0 +(3 rows) + +DROP VIEW rpr_grp_v2; +-- A DEFINE clause may spell a GROUP BY expression. The planner stops at an +-- expression the window's input target already computes whole rather than +-- asking for the columns underneath it on their own, which grouping does not +-- make available; the DEFINE copy then resolves against that same column. +SELECT val + 1 AS bumped, count(*) OVER w AS cnt +FROM rpr_grp +GROUP BY val + 1 +WINDOW w AS ( + ORDER BY val + 1 + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS val + 1 > 0) +ORDER BY bumped; + bumped | cnt +--------+----- + 11 | 1 + 21 | 1 +(2 rows) + +-- A volatile expression that GROUP BY spells too is not rejected: the DEFINE +-- copy becomes a GROUP Var, and the pattern match reads the value the +-- grouping step computed, once per input row, not the expression. The +-- sequence advancing by the number of input rows, not by the number of +-- DEFINE evaluations, shows that. +CREATE SEQUENCE rpr_grp_seq; +SELECT val, count(*) OVER w AS cnt +FROM rpr_grp +GROUP BY val, nextval('rpr_grp_seq') +WINDOW w AS ( + ORDER BY val + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS nextval('rpr_grp_seq') > 0) +ORDER BY val; + val | cnt +-----+----- + 10 | 2 + 20 | 0 +(2 rows) + +SELECT last_value = (SELECT count(*) FROM rpr_grp) AS once_per_row +FROM rpr_grp_seq; + once_per_row +-------------- + t +(1 row) + +DROP SEQUENCE rpr_grp_seq; +-- The same for a function call +SELECT upper(category) AS u, count(*) OVER w AS cnt +FROM rpr_grp +GROUP BY upper(category) +WINDOW w AS ( + ORDER BY upper(category) + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS upper(category) = 'A') +ORDER BY u; + u | cnt +---+----- + A | 1 + B | 0 +(2 rows) + +-- The same for a cast +SELECT val::text AS t, count(*) OVER w AS cnt +FROM rpr_grp +GROUP BY val::text +WINDOW w AS ( + ORDER BY val::text + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS val::text > '0') +ORDER BY t; + t | cnt +----+----- + 10 | 1 + 20 | 1 +(2 rows) + +-- And through a navigation operation, whose argument is read the same way +SELECT val + 1 AS bumped, count(*) OVER w AS cnt +FROM rpr_grp +GROUP BY val + 1 +WINDOW w AS ( + ORDER BY val + 1 + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B*) + DEFINE B AS PREV(val + 1) > 0) +ORDER BY bumped; + bumped | cnt +--------+----- + 11 | 2 + 21 | 0 +(2 rows) + +-- The same under a grouping set, where the row the set nulls leaves the +-- predicate unknown and so unmatched. +SELECT val + 1 AS bumped, count(*) OVER w AS cnt +FROM rpr_grp +GROUP BY ROLLUP(val + 1) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS val + 1 > 0); + bumped | cnt +--------+----- + | 0 + 11 | 1 + 21 | 1 +(3 rows) + +-- A DEFINE clause may repeat an expression the window itself orders by, with +-- no grouping in sight. Adding the bare Vars a DEFINE clause reads to the +-- window's input target is what makes this hold: the DEFINE copy of +-- ROW(val, 1) IS NOT NULL is broken into per field tests before the plan is +-- built, and the bare val the break leaves behind is added to the input on +-- its own, next to the whole ROW(val, 1) the window orders by. +SELECT id, count(*) OVER w AS cnt +FROM rpr_grp +WINDOW w AS ( + ORDER BY ROW(val, 1) + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS ROW(val, 1) IS NOT NULL) +ORDER BY id; + id | cnt +----+----- + 1 | 2 + 2 | 0 +(2 rows) + +-- ERROR: the bare column is another matter; grouping by an expression does +-- not make the columns inside it available on their own +SELECT val + 1 AS bumped, count(*) OVER w AS cnt +FROM rpr_grp +GROUP BY val + 1 +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS val > 0); +ERROR: column "rpr_grp.val" must appear in the GROUP BY clause or be used in an aggregate function +LINE 7: DEFINE A AS val > 0); + ^ +-- ERROR: a column that was never grouped is still reported as one +SELECT category, count(*) OVER w AS cnt +FROM rpr_grp +GROUP BY category +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS val > 0); +ERROR: column "rpr_grp.val" must appear in the GROUP BY clause or be used in an aggregate function +LINE 7: DEFINE A AS val > 0); + ^ +-- An inline OVER (...) carries a window clause of its own, and the +-- substitution reaches it the same way +SELECT category, + count(*) OVER (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS category IS NOT NULL) AS cnt +FROM rpr_sort +GROUP BY ROLLUP(category); + category | cnt +----------+----- + | 0 + B | 1 + A | 1 +(3 rows) + +-- The substitution visits every window clause, not just the first. Here +-- the first window is a plain one and the row pattern is on the second. +SELECT category, count(*) OVER w1 AS plain, count(*) OVER w2 AS rpr +FROM rpr_sort +GROUP BY ROLLUP(category) +WINDOW w1 AS (ORDER BY category), + w2 AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS category IS NOT NULL); + category | plain | rpr +----------+-------+----- + A | 1 | 1 + B | 2 | 1 + | 3 | 0 +(3 rows) + +-- ERROR: an unreferenced window is substituted like any other, so its +-- DEFINE clause still cannot read a column that is not grouped, even +-- though the planner later drops the window +SELECT category +FROM rpr_sort +GROUP BY ROLLUP(category) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS val > 0); +ERROR: column "rpr_sort.val" must appear in the GROUP BY clause or be used in an aggregate function +LINE 7: DEFINE A AS val > 0); + ^ +-- An inner join's USING column of matching types is just the left input's +-- column, not a join alias Var, so plain grouping matches the DEFINE clause's +-- reference to it directly and the pattern reads it unharmed. +SELECT id, count(*) OVER w AS cnt +FROM rpr_grp JOIN rpr_sort USING (id) +GROUP BY id +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id > 0) +ORDER BY id; + id | cnt +----+----- + 1 | 2 + 2 | 0 +(2 rows) + +-- The same query through the join, with a grouping set that can null the +-- column the pattern reads +SELECT id, count(*) OVER w AS cnt +FROM rpr_grp JOIN rpr_sort USING (id) +GROUP BY ROLLUP(id) +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id > 0) +ORDER BY id; + id | cnt +----+----- + 1 | 2 + 2 | 0 + | 0 +(3 rows) + +-- A FULL JOIN's USING column is a merged column -- a COALESCE over the two +-- sides rather than either one -- so a DEFINE clause naming it holds a join +-- alias Var, and only flatten_join_alias_for_parser() turns that into +-- something the grouped target list can be matched against. The inner joins +-- above reach the pattern without that step. +SELECT id, count(*) OVER w AS cnt +FROM rpr_grp FULL JOIN rpr_sort USING (id) +GROUP BY ROLLUP(id) +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id > 0) +ORDER BY id NULLS LAST; + id | cnt +----+----- + 1 | 6 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 + 6 | 0 + | 0 +(7 rows) + +-- A grouping set list holding only the empty set builds no RTE_GROUP +-- either, just like GROUP BY () +SELECT count(*) OVER w AS cnt +FROM rpr_grp +GROUP BY GROUPING SETS (()) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS true); + cnt +----- + 1 +(1 row) + +-- GROUP BY spelled as the merged column's COALESCE expansion, rather than the +-- join's own name, while DEFINE reads that same column: the two spellings +-- must compare equal despite the different tree shapes. +SELECT id + 1 AS b, count(*) OVER w AS cnt +FROM rpr_grp FULL JOIN rpr_sort USING (id) +GROUP BY COALESCE(rpr_grp.id, rpr_sort.id) + 1 +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) DEFINE A AS (id + 1) > 0) +ORDER BY 1; + b | cnt +---+----- + 2 | 1 + 3 | 1 + 4 | 1 + 5 | 1 + 6 | 1 + 7 | 1 +(6 rows) + +-- Same construct as a view: the DEFINE clause must deparse to the plain +-- join column, not the two-sided COALESCE GROUP BY computed, or the printed +-- text would not re-parse. +CREATE VIEW rpr_fjcoal_v AS +SELECT COALESCE(rpr_grp.id, rpr_sort.id) + 1 AS idp1, count(*) OVER w AS cnt +FROM rpr_grp FULL JOIN rpr_sort USING (id) +GROUP BY COALESCE(rpr_grp.id, rpr_sort.id) + 1 +WINDOW w AS ( + ORDER BY COALESCE(rpr_grp.id, rpr_sort.id) + 1 + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id + 1 > 0); +SELECT pg_get_viewdef('rpr_fjcoal_v'::regclass, true); + pg_get_viewdef +------------------------------------------------------------------------------------------------------------------ + SELECT COALESCE(rpr_grp.id, rpr_sort.id) + 1 AS idp1, + + count(*) OVER w AS cnt + + FROM rpr_grp + + FULL JOIN rpr_sort USING (id) + + GROUP BY (COALESCE(rpr_grp.id, rpr_sort.id) + 1) + + WINDOW w AS (ORDER BY (COALESCE(rpr_grp.id, rpr_sort.id) + 1) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS (id + 1) > 0); +(1 row) + +SELECT * FROM rpr_fjcoal_v ORDER BY 1; + idp1 | cnt +------+----- + 2 | 6 + 3 | 0 + 4 | 0 + 5 | 0 + 6 | 0 + 7 | 0 +(6 rows) + +-- The deparsed definition re-parses into an identical view. +CREATE VIEW rpr_fjcoal_v2 AS + SELECT COALESCE(rpr_grp.id, rpr_sort.id) + 1 AS idp1, + count(*) OVER w AS cnt + FROM rpr_grp + FULL JOIN rpr_sort USING (id) + GROUP BY (COALESCE(rpr_grp.id, rpr_sort.id) + 1) + WINDOW w AS (ORDER BY (COALESCE(rpr_grp.id, rpr_sort.id) + 1) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS (id + 1) > 0); +SELECT pg_get_viewdef('rpr_fjcoal_v2'::regclass, true) = + pg_get_viewdef('rpr_fjcoal_v'::regclass, true) AS same_definition; + same_definition +----------------- + t +(1 row) + +DROP VIEW rpr_fjcoal_v2; +DROP VIEW rpr_fjcoal_v; +DROP TABLE rpr_grp; +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; +-- A DEFINE clause reading a compound GROUP BY expression. After grouping +-- only the expression itself exists, so make_window_input_target() has to +-- take it whole and stop: asking for the Vars underneath would ask the +-- grouping step for columns it cannot produce. "((a + b))" alone on the +-- Output lines, with no bare a or b anywhere above the HashAggregate, is the +-- assertion. +CREATE TABLE rpr_gexp (a int, b int); +INSERT INTO rpr_gexp VALUES (1, 1), (2, 2), (3, 3), (4, 4); +SELECT a + b AS ab, count(*) OVER w AS c +FROM rpr_gexp +GROUP BY a + b +WINDOW w AS (ORDER BY a + b + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (X+) DEFINE X AS a + b > 2); + ab | c +----+--- + 2 | 0 + 4 | 3 + 6 | 0 + 8 | 0 +(4 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT a + b AS ab, count(*) OVER w AS c +FROM rpr_gexp +GROUP BY a + b +WINDOW w AS (ORDER BY a + b + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (X+) DEFINE X AS a + b > 2); + QUERY PLAN +-------------------------------------------------------------------------------------------------------- + WindowAgg + Output: ((a + b)), count(*) OVER w + Window: w AS (ORDER BY ((rpr_gexp.a + rpr_gexp.b)) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: x+# + -> Sort + Output: ((a + b)) + Sort Key: ((rpr_gexp.a + rpr_gexp.b)) + -> HashAggregate + Output: ((a + b)) + Group Key: (rpr_gexp.a + rpr_gexp.b) + -> Seq Scan on public.rpr_gexp + Output: (a + b) +(12 rows) + +-- Reaching below the grouping expression is rejected, as it would be in any +-- other clause evaluated after grouping. +SELECT a + b AS ab, count(*) OVER w AS c +FROM rpr_gexp +GROUP BY a + b +WINDOW w AS (ORDER BY a + b + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (X+) DEFINE X AS a > 2); +ERROR: column "rpr_gexp.a" must appear in the GROUP BY clause or be used in an aggregate function +LINE 6: PATTERN (X+) DEFINE X AS a > 2); + ^ +DROP TABLE rpr_gexp; +-- ============================================================ +-- 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 +); + 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; + 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) +); + 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 +); + 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 +); + id | val | cnt +----+-----+----- + 10 | 100 | 1 +(1 row) + +DROP TABLE rpr_stress; +-- ============================================================ +-- Error Limit Tests +-- ============================================================ +-- Tests for error conditions in parse_rpr.c and 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 + ^ +-- Row pattern variable-count boundary: 240 variables are accepted, 241 +-- rejected. A varId is one byte and the high nibble (0xF0-0xFF) is reserved +-- for control elements, so RPR_VARID_MAX is 0xEF and the 241st distinct +-- variable would fall into that reserved range. +-- The rejecting case names V241 in PATTERN only. The limit counts distinct +-- PATTERN variables whether or not DEFINE names them, so V241 is still +-- counted, and that is what carries the total past the limit. +-- ECHO is silenced so the generated 240-variable clauses do not flood the +-- expected output. +-- 240 variables -> maximum, accepted. +-- 241 variables -> over maximum, rejected. +\set ECHO none + count +------- + 0 + 0 +(2 rows) + +ERROR: too many row pattern variables +LINE 3: ...V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241) DEFI... + ^ +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..e77fcef25bb --- /dev/null +++ b/src/test/regress/expected/rpr_explain.out @@ -0,0 +1,7042 @@ +-- ============================================================ +-- 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 consume B. +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 +-- rpr_ev_ctx_absorb_unbounded 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 and A never matches, so two empty iterations fill +-- the lower bound -> 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 +-- BEGIN-to-END 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 navigation offsets for tuplestore trim, resolved at executor init. +-- 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" (unresolved at init); EXPLAIN ANALYZE shows +-- "retain all" (resolved per scan). +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 WindowObject's mark position". 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..e71696e453a --- /dev/null +++ b/src/test/regress/expected/rpr_integration.out @@ -0,0 +1,2645 @@ +-- ============================================================ +-- 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 or SKIP) +-- A5. Unused output removal around an RPR window +-- 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 + DEFINE-only column 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; + 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 for the parser-level tests because only inline windows are +-- subject to the parser's dedup path; the planner's frame optimization +-- can also merge named windows (covered at the end). +-- 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; + 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 would converge only if frame optimization rewrote both, and the +-- RPR window is skipped by it, 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 or SKIP) +-- ============================================================ +-- Verify that inline-window dedup does not merge two RPR windows that +-- share the same PATTERN structure but differ in one other part of the +-- row pattern common syntax. Even though the ORDER BY, frame, and +-- PATTERN coincide, a differing DEFINE classifies rows differently and +-- a differing AFTER MATCH SKIP resumes the scan differently, so either +-- must yield two separate WindowAgg nodes. transformWindowFuncCall() +-- compares the whole RPCommonSyntax node, which carries rpDefs and +-- rpSkipTo alongside rpPattern; the cases below cover one field each. +-- 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; + 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) + +-- Two inline RPR windows alike in every way but the AFTER MATCH SKIP +-- mode must also remain separate. SKIP PAST LAST ROW resumes after the +-- match, SKIP TO NEXT ROW resumes one row in, so the later rows of a +-- match can start a match of their own. +EXPLAIN (COSTS OFF) +SELECT + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS cnt_past, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS cnt_next +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 the two windows disagree on the rows that a skipped-past match +-- covered, confirming the skip modes were not collapsed by dedup. +SELECT + id, val, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS cnt_past, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS cnt_next +FROM rpr_integ; + id | val | cnt_past | cnt_next +----+-----+----------+---------- + 1 | 10 | 2 | 2 + 2 | 20 | 0 | 0 + 3 | 15 | 2 | 2 + 4 | 25 | 0 | 0 + 5 | 5 | 3 | 3 + 6 | 30 | 0 | 2 + 7 | 35 | 0 | 0 + 8 | 20 | 3 | 3 + 9 | 40 | 0 | 2 + 10 | 45 | 0 | 0 +(10 rows) + +-- ============================================================ +-- A5. Unused output removal around an RPR window +-- ============================================================ +-- the outer query only counts rows and never reads count(*) OVER w, so the +-- window function is replaced with NULL, the window becomes inactive, and its +-- WindowAgg is dropped -- leaving a plain Aggregate over the scan. The row +-- count (and thus count(*)) is unchanged. +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 + -> Seq Scan on rpr_integ +(2 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) + +-- sum(cnt) reads the window function's value, so the column cannot be removed. +EXPLAIN (COSTS OFF, VERBOSE) +SELECT count(*), sum(cnt) FROM ( + SELECT count(*) OVER w as cnt 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 + Output: count(*), sum((count(*) OVER w)) + -> WindowAgg + Output: count(*) OVER w + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+# + Nav Mark Lookback: 1 + -> Seq Scan on public.rpr_integ + Output: rpr_integ.val +(9 rows) + +SELECT count(*), sum(cnt) FROM ( + SELECT count(*) OVER w as cnt 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) + +-- Navigation-free DEFINE: DEFINE A AS TRUE matches every row, so PATTERN (A+) +-- still reduces the frame (to the whole remaining partition) even without a +-- PREV/NEXT navigation. sum(c) reads the window value, so the WindowAgg is +-- retained; this checks that a trivial DEFINE still drives frame reduction +-- and yields the expected counts. +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) + +-- "val" is a non-resjunk subquery output that the outer query never reads, so +-- remove_unused_subquery_outputs() replaces it with NULL; "NULL::integer" on +-- the WindowAgg Output line shows that. DEFINE does not read that output +-- entry but rpr_integ.val below it, which build_base_rel_tlists() and +-- make_window_input_target() carry into the WindowAgg's input, as the Sort +-- and Seq Scan Output lines show. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT count(*) 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 val > PREV(val)) +) t WHERE c > 0; + QUERY PLAN +----------------------------------------------------------------------------------------------------- + Aggregate + Output: count(*) + -> Subquery Scan on t + Output: t.val, t.c, rpr_integ.id + Filter: (t.c > 0) + -> WindowAgg + Output: NULL::integer, count(*) OVER w, rpr_integ.id + Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Output: rpr_integ.id, rpr_integ.val + Sort Key: rpr_integ.id + -> Seq Scan on public.rpr_integ + Output: rpr_integ.id, rpr_integ.val +(15 rows) + +SELECT count(*) 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 val > PREV(val)) +) t WHERE c > 0; + count +------- + 4 +(1 row) + +-- The same column has to survive at the top level, where +-- remove_unused_subquery_outputs() never runs at all: "val" is referenced only +-- by DEFINE, so build_base_rel_tlists() marking it needed is the only thing +-- carrying it up the join tree, and make_window_input_target() is what asks +-- for it again. "val" on the Sort and Seq Scan Output lines, below a +-- WindowAgg that does not output it, is the assertion. +EXPLAIN (VERBOSE, COSTS OFF) +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)); + QUERY PLAN +----------------------------------------------------------------------------------------- + WindowAgg + Output: id, count(*) OVER w + Window: w 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 +(10 rows) + +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)); + id | cnt +----+----- + 1 | 2 + 2 | 0 + 3 | 2 + 4 | 0 + 5 | 3 + 6 | 0 + 7 | 0 + 8 | 3 + 9 | 0 + 10 | 0 +(10 rows) + +-- The DEFINE column also has to survive join removal: build_base_rel_tlists() +-- marks u.uval, which DEFINE reads, needed at relation 0, so the LEFT JOIN is +-- kept and the DEFINE Var still points at a relation in the plan. +CREATE TABLE rpr_integ_u (id INT PRIMARY KEY, uval INT); +INSERT INTO rpr_integ_u SELECT i, i * 10 FROM generate_series(1, 5) i; +EXPLAIN (COSTS OFF) +SELECT id, c FROM ( + SELECT t.id AS id, u.uval AS uv, count(*) OVER w AS c + FROM rpr_integ t LEFT JOIN rpr_integ_u u ON t.id = u.id + WINDOW w AS (ORDER BY t.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS uval > PREV(uval)) +) s ORDER BY id; + QUERY PLAN +--------------------------------------------------------------------------------------- + Subquery Scan on s + -> WindowAgg + Window: w AS (ORDER BY t.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Sort Key: t.id + -> Hash Left Join + Hash Cond: (t.id = u.id) + -> Seq Scan on rpr_integ t + -> Hash + -> Seq Scan on rpr_integ_u u +(12 rows) + +SELECT id, c FROM ( + SELECT t.id AS id, u.uval AS uv, count(*) OVER w AS c + FROM rpr_integ t LEFT JOIN rpr_integ_u u ON t.id = u.id + WINDOW w AS (ORDER BY t.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS uval > PREV(uval)) +) s ORDER BY id; + id | c +----+--- + 1 | 5 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 + 6 | 0 + 7 | 0 + 8 | 0 + 9 | 0 + 10 | 0 +(10 rows) + +-- A flattened subquery output that an outer join makes nullable reaches the +-- DEFINE clause as a PlaceHolderVar rather than a Var. +-- build_base_rel_tlists() marks it needed and +-- make_window_input_target() asks for it, so it reaches the +-- WindowAgg's input: the trailing "(COALESCE(rpr_integ_u.uval, 0))" is the +-- assertion. coalesce() is deliberate and must not be simplified away: a +-- strict expression such as "uval + 1" goes to NULL on its own when the join +-- finds no match, so pullup does not wrap it and the case degenerates into an +-- ordinary Var. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t.id, count(*) OVER w AS c +FROM rpr_integ t + LEFT JOIN (SELECT id AS uid, coalesce(uval, 0) AS uv1 FROM rpr_integ_u) s + ON t.id = s.uid +WINDOW w AS (ORDER BY t.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS uv1 > PREV(uv1)); + QUERY PLAN +--------------------------------------------------------------------------------- + WindowAgg + Output: t.id, count(*) OVER w + Window: w AS (ORDER BY t.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Output: t.id, (COALESCE(rpr_integ_u.uval, 0)) + Sort Key: t.id + -> Hash Left Join + Output: t.id, (COALESCE(rpr_integ_u.uval, 0)) + Inner Unique: true + Hash Cond: (t.id = rpr_integ_u.id) + -> Seq Scan on public.rpr_integ t + Output: t.id, t.val + -> Hash + Output: rpr_integ_u.id, (COALESCE(rpr_integ_u.uval, 0)) + -> Seq Scan on public.rpr_integ_u + Output: rpr_integ_u.id, COALESCE(rpr_integ_u.uval, 0) +(18 rows) + +SELECT t.id, count(*) OVER w AS c +FROM rpr_integ t + LEFT JOIN (SELECT id AS uid, coalesce(uval, 0) AS uv1 FROM rpr_integ_u) s + ON t.id = s.uid +WINDOW w AS (ORDER BY t.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS uv1 > PREV(uv1)); + id | c +----+--- + 1 | 5 + 2 | 0 + 3 | 0 + 4 | 0 + 5 | 0 + 6 | 0 + 7 | 0 + 8 | 0 + 9 | 0 + 10 | 0 +(10 rows) + +-- The same shape with the window dead: nothing reads count(*) OVER w, so its +-- entry goes and w goes with it. grouping_planner() empties w's DEFINE +-- clause when the subquery is planned, so nothing marks u.uval needed, and +-- "uv" is unread as well. That leaves rpr_integ_u unreferenced, so join +-- removal takes the LEFT JOIN too and the scan is left alone. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id FROM ( + SELECT t.id AS id, u.uval AS uv, count(*) OVER w AS c + FROM rpr_integ t LEFT JOIN rpr_integ_u u ON t.id = u.id + WINDOW w AS (ORDER BY t.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS uval > PREV(uval)) +) s; + QUERY PLAN +--------------------------------------------------- + Subquery Scan on s + Output: s.id + -> Seq Scan on public.rpr_integ t + Output: t.id, NULL::integer, NULL::bigint +(4 rows) + +SELECT id FROM ( + SELECT t.id AS id, u.uval AS uv, count(*) OVER w AS c + FROM rpr_integ t LEFT JOIN rpr_integ_u u ON t.id = u.id + WINDOW w AS (ORDER BY t.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS uval > PREV(uval)) +) s ORDER BY id; + id +---- + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +(10 rows) + +-- A live window and a dead RPR window in one subquery. Emptying the dead +-- one's DEFINE clause must not disturb winref, which is a position in +-- windowClause, nor the live window's result. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id, c1 FROM ( + SELECT t.id AS id, u.uval AS uv, + count(*) OVER w1 AS c1, count(*) OVER w2 AS c2 + FROM rpr_integ t LEFT JOIN rpr_integ_u u ON t.id = u.id + WINDOW w1 AS (ORDER BY t.id), + w2 AS (ORDER BY t.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS uval > PREV(uval)) +) s; + QUERY PLAN +--------------------------------------------------------------------- + Subquery Scan on s + Output: s.id, s.c1 + -> WindowAgg + Output: t.id, NULL::integer, count(*) OVER w1, NULL::bigint + Window: w1 AS (ORDER BY t.id) + -> Sort + Output: t.id + Sort Key: t.id + -> Seq Scan on public.rpr_integ t + Output: t.id +(10 rows) + +SELECT id, c1 FROM ( + SELECT t.id AS id, u.uval AS uv, + count(*) OVER w1 AS c1, count(*) OVER w2 AS c2 + FROM rpr_integ t LEFT JOIN rpr_integ_u u ON t.id = u.id + WINDOW w1 AS (ORDER BY t.id), + w2 AS (ORDER BY t.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS uval > PREV(uval)) +) s ORDER BY id; + id | c1 +----+---- + 1 | 1 + 2 | 2 + 3 | 3 + 4 | 4 + 5 | 5 + 6 | 6 + 7 | 7 + 8 | 8 + 9 | 9 + 10 | 10 +(10 rows) + +DROP TABLE rpr_integ_u; +-- w2 is declared and no window function references it, so +-- select_active_windows() drops it when the subquery is planned and +-- grouping_planner() empties its DEFINE clause. The unread output for val +-- becomes a null Const, and with nothing left asking for rpr_integ.val the +-- Sort and Seq Scan below the WindowAgg do not carry it either. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT c FROM ( + SELECT count(*) OVER w1 AS c, val + FROM rpr_integ + WINDOW w1 AS (ORDER BY id), + w2 AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t; + QUERY PLAN +--------------------------------------------------------------- + Subquery Scan on t + Output: t.c + -> WindowAgg + Output: count(*) OVER w1, NULL::integer, rpr_integ.id + Window: w1 AS (ORDER BY rpr_integ.id) + -> Sort + Output: rpr_integ.id + Sort Key: rpr_integ.id + -> Seq Scan on public.rpr_integ + Output: rpr_integ.id +(10 rows) + +-- Here w2 does have a window function, but the outer query does not read +-- it, so remove_unused_subquery_outputs() replaces that entry with a null +-- Const, and w2 goes inactive when the subquery is planned. Its DEFINE +-- clause is emptied as above, so rpr_integ.val is not carried below the +-- WindowAgg. Both null Consts on the WindowAgg's Output line, and "val" +-- missing from the Sort and Seq Scan, are the assertion. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT c FROM ( + SELECT count(*) OVER w1 AS c, count(*) OVER w2 AS unread, val + FROM rpr_integ + WINDOW w1 AS (ORDER BY id), + w2 AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t; + QUERY PLAN +----------------------------------------------------------------------------- + Subquery Scan on t + Output: t.c + -> WindowAgg + Output: count(*) OVER w1, NULL::bigint, NULL::integer, rpr_integ.id + Window: w1 AS (ORDER BY rpr_integ.id) + -> Sort + Output: rpr_integ.id + Sort Key: rpr_integ.id + -> Seq Scan on public.rpr_integ + Output: rpr_integ.id +(10 rows) + +-- The same shape with the window function one level down, inside an +-- expression. The whole entry is replaced with a null Const, taking the +-- nested window function with it, so w2 goes inactive just as above. This +-- plan matching the one above is the assertion. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT c FROM ( + SELECT count(*) OVER w1 AS c, (count(*) OVER w2) + 1 AS unread, val + FROM rpr_integ + WINDOW w1 AS (ORDER BY id), + w2 AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t; + QUERY PLAN +----------------------------------------------------------------------------- + Subquery Scan on t + Output: t.c + -> WindowAgg + Output: count(*) OVER w1, NULL::bigint, NULL::integer, rpr_integ.id + Window: w1 AS (ORDER BY rpr_integ.id) + -> Sort + Output: rpr_integ.id + Sort Key: rpr_integ.id + -> Seq Scan on public.rpr_integ + Output: rpr_integ.id +(10 rows) + +SELECT c FROM ( + SELECT count(*) OVER w1 AS c, (count(*) OVER w2) + 1 AS unread, val + FROM rpr_integ + WINDOW w1 AS (ORDER BY id), + w2 AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t; + c +---- + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +(10 rows) + +CREATE TABLE rpr_integ_two (id int, v1 int, v2 int); +INSERT INTO rpr_integ_two SELECT i, i * 10, i * 100 FROM generate_series(1, 5) i; +-- Whether a window is active is decided per window clause, not for row pattern +-- recognition as a whole: w3's function goes, and the column only w3's DEFINE +-- names goes with it, while the column w2's DEFINE reads is still carried to +-- the WindowAgg's input. Both unread outputs, v1 and v2, become null Consts. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT c2 FROM ( + SELECT count(*) OVER w2 AS c2, count(*) OVER w3 AS c3, v1, v2 + FROM rpr_integ_two + WINDOW w2 AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS v1 > PREV(v1)), + w3 AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS v2 > PREV(v2)) +) t; + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on t + Output: t.c2 + -> WindowAgg + Output: count(*) OVER w2, NULL::bigint, NULL::integer, NULL::integer, rpr_integ_two.id + Window: w2 AS (ORDER BY rpr_integ_two.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Output: rpr_integ_two.id, rpr_integ_two.v1 + Sort Key: rpr_integ_two.id + -> Seq Scan on public.rpr_integ_two + Output: rpr_integ_two.id, rpr_integ_two.v1 +(12 rows) + +-- A window function entry can be kept for a reason other than the upper query +-- reading it -- here the subquery's own ORDER BY -- and then its window stays +-- active and keeps its DEFINE clause, so the column that clause reads is +-- still carried to the WindowAgg's input while the unread output v1 becomes a +-- null Const. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT c FROM ( + SELECT count(*) OVER w1 AS c, count(*) OVER w2 AS ord, v1 + FROM rpr_integ_two + WINDOW w1 AS (ORDER BY id), + w2 AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS v1 > PREV(v1)) + ORDER BY 2 +) t; + QUERY PLAN +---------------------------------------------------------------------------------------------------------- + Subquery Scan on t + Output: t.c + -> Sort + Output: (count(*) OVER w1), (count(*) OVER w2), NULL::integer, rpr_integ_two.id + Sort Key: (count(*) OVER w2) + -> WindowAgg + Output: (count(*) OVER w1), count(*) OVER w2, NULL::integer, rpr_integ_two.id + Window: w2 AS (ORDER BY rpr_integ_two.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> WindowAgg + Output: rpr_integ_two.id, rpr_integ_two.v1, count(*) OVER w1 + Window: w1 AS (ORDER BY rpr_integ_two.id) + -> Sort + Output: rpr_integ_two.id, rpr_integ_two.v1 + Sort Key: rpr_integ_two.id + -> Seq Scan on public.rpr_integ_two + Output: rpr_integ_two.id, rpr_integ_two.v1 +(18 rows) + +DROP TABLE rpr_integ_two; +-- Whole-row Var in DEFINE is not allowed +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; +ERROR: whole-row reference is not allowed in DEFINE clause +LINE 6: DEFINE B AS rpr_integ IS NOT NULL) + ^ +HINT: A DEFINE condition may reference individual columns only. +-- It still reaches a DEFINE clause without being written there: pulling up a +-- subquery substitutes that subquery's output expressions into defineClause, +-- and one of them can be a whole-row Var (attribute number 0). The window +-- input target takes it like any other DEFINE column, so the pattern match +-- sees the full row regardless of what the subquery projects. The unused +-- scalar output "val" is therefore free to be replaced with NULL +-- (nothing reads it), while c is kept because sum(c) reads it; the match +-- result is unchanged. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT sum(c) FROM ( + SELECT val, count(*) OVER w AS c + FROM (SELECT r, r.id AS id, r.val AS val FROM rpr_integ r) s + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS r IS NOT NULL) +) t; + QUERY PLAN +--------------------------------------------------------------------------------------- + Aggregate + Output: sum((count(*) OVER w)) + -> WindowAgg + Output: NULL::integer, count(*) OVER w, r.id + Window: w AS (ORDER BY r.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + -> Sort + Output: r.id, r.* + Sort Key: r.id + -> Seq Scan on public.rpr_integ r + Output: r.id, r.* +(11 rows) + +SELECT sum(c) FROM ( + SELECT val, count(*) OVER w AS c + FROM (SELECT r, r.id AS id, r.val AS val FROM rpr_integ r) s + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS r IS NOT NULL) +) t; + sum +----- + 10 +(1 row) + +-- A window function may also sit in a sub-select's test expression, where it +-- belongs to this query level rather than the sub-select's. The outer query +-- filters on m, so the entry is kept, and the window function in its test +-- expression keeps w active: the DEFINE clause stays, and rpr_integ.val is +-- carried to the WindowAgg's input while the unread output val becomes a null +-- Const. Were w taken for inactive, its DEFINE clause would be emptied, B +-- would match every row, and no row would start a match of length 2; here +-- rows 1 and 3 do. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT count(*) FROM ( + SELECT id, val, (count(*) OVER w) IN (SELECT 2) AS m + 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)) + OFFSET 0 +) t WHERE m; + QUERY PLAN +---------------------------------------------------------------------------------------------------------- + Aggregate + Output: count(*) + -> Subquery Scan on t + Output: t.id, t.val, t.m + Filter: t.m + -> WindowAgg + Output: rpr_integ.id, NULL::integer, (ANY (count(*) OVER w = (hashed SubPlan any_1).col1)) + Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b+ + Nav Mark Lookback: 1 + -> Sort + Output: rpr_integ.id, rpr_integ.val + Sort Key: rpr_integ.id + -> Seq Scan on public.rpr_integ + Output: rpr_integ.id, rpr_integ.val + SubPlan any_1 + -> Result + Output: 2 +(18 rows) + +SELECT count(*) FROM ( + SELECT id, val, (count(*) OVER w) IN (SELECT 2) AS m + 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)) + OFFSET 0 +) t WHERE m; + count +------- + 2 +(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 rpr_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 rpr_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))') + > + rpr_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 rpr_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 are added to the window input target; 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. The column DEFINE reads ("val") shows up only at and +-- below the RPR WindowAgg, not on the outer one. +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 + 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 defers it to +-- execution ("runtime"), and the per-scan offset resolution rejects it. +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. +-- The outer query leaves lat unread, so remove_unused_subquery_outputs() +-- replaces it with a NULL, as it does in the control, while y, which the +-- DEFINE clause reads from rpr_lat_i, is still carried to the WindowAgg's +-- input. +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. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT o.a, s.c +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; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Nested Loop + Output: o.a, (count(*) OVER w) + -> Seq Scan on public.rpr_lat_o o + Output: o.a, o.b + -> WindowAgg + Output: NULL::integer, count(*) OVER w, rpr_lat_i.x + Window: w AS (ORDER BY rpr_lat_i.x ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+# + -> Sort + Output: rpr_lat_i.x, rpr_lat_i.y + Sort Key: rpr_lat_i.x + -> Seq Scan on public.rpr_lat_i + Output: rpr_lat_i.x, rpr_lat_i.y +(13 rows) + +SELECT o.a, s.c +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 | c +---+--- + 1 | 2 + 1 | 0 +(2 rows) + +-- Control: the outer reference is o.a, attribute 1, which cannot be mistaken +-- for y. The plan and the counts must match the query above. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT o.a, s.c +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; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Nested Loop + Output: o.a, (count(*) OVER w) + -> Seq Scan on public.rpr_lat_o o + Output: o.a, o.b + -> WindowAgg + Output: NULL::integer, count(*) OVER w, rpr_lat_i.x + Window: w AS (ORDER BY rpr_lat_i.x ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+# + -> Sort + Output: rpr_lat_i.x, rpr_lat_i.y + Sort Key: rpr_lat_i.x + -> Seq Scan on public.rpr_lat_i + Output: rpr_lat_i.x, rpr_lat_i.y +(13 rows) + +SELECT o.a, s.c +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 | c +---+--- + 1 | 2 + 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 number of times a row's DEFINE predicate is +-- evaluated is not something a query can rely on, 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 + DEFINE-only column pruning +-- ============================================================ +-- Verify that carrying a DEFINE-only column to the WindowAgg's input +-- 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: oc becomes a null Const and rpr_over2's scan contributes no column; +-- rpr_over1.a, which DEFINE reads, reaches the WindowAgg's input. +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; +-- A DEFINE clause can come to hold a Var, or a PlaceHolderVar, of an outer +-- query level, although none may be written in one: inlining a SQL function +-- substitutes the call's actual arguments into the body and raises the level +-- of what it plants there, and subquery pull-up may wrap that in a +-- PlaceHolderVar. Each query below leaves the function's output x unread, +-- so its entry is replaced with a NULL, and is followed by the same query +-- reading x, which prunes nothing; the counts must agree. +CREATE TABLE rpr_up (p int, x int); +INSERT INTO rpr_up SELECT g, 100 + g FROM generate_series(1, 6) g; +CREATE TABLE rpr_drv (k int); +INSERT INTO rpr_drv VALUES (2), (4); +-- an outer Var, in a clause that does not read the pruned column: +CREATE FUNCTION rpr_up_f(th int) RETURNS TABLE (cnt bigint, x int) +LANGUAGE sql STABLE AS $$ + SELECT count(*) OVER w, x FROM rpr_up + WINDOW w AS (ORDER BY p ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS p > th) +$$; +SELECT d.k, max(g.cnt) FROM rpr_drv d, LATERAL rpr_up_f(d.k) g +GROUP BY d.k ORDER BY 1; + k | max +---+----- + 2 | 4 + 4 | 2 +(2 rows) + +SELECT d.k, max(g.cnt), max(g.x) FROM rpr_drv d, LATERAL rpr_up_f(d.k) g +GROUP BY d.k ORDER BY 1; + k | max | max +---+-----+----- + 2 | 4 | 106 + 4 | 2 | 106 +(2 rows) + +-- the same, in a clause that does read it: the output entry x still goes to +-- NULL, while the DEFINE clause reads rpr_up.x, which is carried to the +-- WindowAgg's input. +CREATE FUNCTION rpr_up_h(th int) RETURNS TABLE (cnt bigint, x int) +LANGUAGE sql STABLE AS $$ + SELECT count(*) OVER w, x FROM rpr_up + WINDOW w AS (ORDER BY p ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS x > th + 100) +$$; +SELECT d.k, max(g.cnt) FROM rpr_drv d, LATERAL rpr_up_h(d.k) g +GROUP BY d.k ORDER BY 1; + k | max +---+----- + 2 | 4 + 4 | 2 +(2 rows) + +SELECT d.k, max(g.cnt), max(g.x) FROM rpr_drv d, LATERAL rpr_up_h(d.k) g +GROUP BY d.k ORDER BY 1; + k | max | max +---+-----+----- + 2 | 4 | 106 + 4 | 2 | 106 +(2 rows) + +-- and with a constant argument, where no outer reference arises at all: +SELECT max(cnt) FROM rpr_up_h(2); + max +----- + 4 +(1 row) + +SELECT max(cnt) FROM rpr_up_h(4); + max +----- + 2 +(1 row) + +-- an outer PlaceHolderVar, which pulling up the subquery that supplies the +-- argument puts there in place of the Var: +CREATE FUNCTION rpr_up_n(int) RETURNS TABLE (cnt bigint, x int) +LANGUAGE sql STABLE AS $$ + SELECT count(*) OVER w, x FROM rpr_up + WINDOW w AS (ORDER BY p ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL PATTERN (A+) DEFINE A AS PREV(p + $1) > 0) +$$; +SELECT s.k, max(g.cnt) FROM (SELECT 3 AS k FROM rpr_drv) s, + LATERAL rpr_up_n(s.k) g GROUP BY s.k; + k | max +---+----- + 3 | 5 +(1 row) + +SELECT s.k, max(g.cnt), max(g.x) FROM (SELECT 3 AS k FROM rpr_drv) s, + LATERAL rpr_up_n(s.k) g GROUP BY s.k; + k | max | max +---+-----+----- + 3 | 5 | 106 +(1 row) + +DROP FUNCTION rpr_up_f(int), rpr_up_h(int), rpr_up_n(int); +DROP TABLE rpr_up, rpr_drv; +-- ============================================================ +-- 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 uses its own offset (counts 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. +-- 1 + k overflows int64 at the last offset, 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 (val) of one window and the sort key (grp) of another +-- window are both absent from the select list. The sort key reaches the plan +-- as a junk targetlist entry; the DEFINE column never enters the targetlist +-- and is added to the WindowAgg's input by make_window_input_target(). Each +-- window must still read its own column. +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) + +-- The opposite declaration order 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..f36ee926b6d --- /dev/null +++ b/src/test/regress/expected/rpr_nfa.out @@ -0,0 +1,7421 @@ +-- ============================================================ +-- 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 +-- ============================================================ +-- Every test in this section uses SKIP PAST LAST ROW with an unbounded +-- frame, the only setting in which buildRPRPattern() enables absorption, +-- so absorbable shapes are really absorbed and the non-absorbable ones are +-- excluded by their structure rather than by the SKIP mode. +-- Absorbable pattern (A+) +-- The contexts started at rows 2-4 are absorbed into row 1's; one match 1-4. +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 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) + +-- Mixed absorbable/non-absorbable ((A+) | B) +-- Only the A+ branch is absorbable: rows 2-3 are absorbed into the 1-3 +-- match, and the context of row 4 still matches through the B branch. +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 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} | | + 4 | {B} | 4 | 4 + 5 | {_} | | +(5 rows) + +-- State coverage (same elemIdx, different count) +-- A{2,} is absorbable: row 2's A state (count 1) is covered by row 1's +-- (count 2), even below the minimum, and likewise for row 3. +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 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 | 4 + 2 | {A} | | + 3 | {A} | | + 4 | {B} | | + 5 | {_} | | +(5 rows) + +-- Reluctant pattern (A+?) - not absorbable +-- Compare with greedy A+ above: the settings allow absorption, but a +-- reluctant quantifier is never absorbable, and each context stops at its +-- one-row minimum, so rows 1-4 each start their own match. +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 PAST LAST 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 +-- Rows 2-3 are absorbed while row 1 is still in A+; B then ends the 1-4 +-- match, and row 4's context, starting inside it, is skipped. +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 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} | | + 4 | {B} | | + 5 | {X} | | +(5 rows) + +-- Per-branch absorption with ALT: B+ C | B+ D +-- Row 1's B+ states in both branches cover those of rows 2-3, which are +-- absorbed; the D branch ends the 1-4 match. +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 PAST LAST 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} | | + 3 | {B} | | + 4 | {D} | | + 5 | {X} | | +(5 rows) + +-- Non-absorbable: A B+ (unbounded not in first position) +-- Nothing is absorbed although the settings allow it; the contexts of rows +-- 2-4 are instead skipped as the 1-4 match grows over them. +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 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 | {B} | | + 3 | {B} | | + 4 | {B} | | + 5 | {X} | | +(5 rows) + +-- GROUP merge enables absorption: (A B) (A B)+ optimized to (A B){2,} +-- The contexts of rows 3 and 5 reach the group END one iteration behind +-- row 1's and are absorbed there; the match is 1-6. +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 PAST LAST 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} | | + 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) +-- Row 2 is absorbed while row 1 is in A+; once in B+ nothing is +-- absorbable, and rows 3-4 are skipped by the 1-4 match instead. +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 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 | {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. +-- SKIP TO NEXT ROW disables absorption (and A+? is not absorbable in any +-- case), so the overlapping contexts of rows 1 and 2 both 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); + 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) + +-- The same greedy/reluctant contrast with a MULTI-ELEMENT body. The columns +-- above all have a single-variable body, so the empty-preferred bit reaching +-- the group's END always came from one child; here it has to survive the +-- AND-reduction fillRPRPattern() performs over a sequence's children. A +-- greedy body takes the longest match, a reluctant one prefers the empty +-- derivation, and the min>=2 pair shows the outer bound does not change that. +WITH t(id, isa, isb) AS + (VALUES (1,true,false),(2,false,true),(3,true,false),(4,false,true),(5,false,false)) +SELECT id, + count(*) OVER gg AS gg, -- (A? B?)+ greedy body + count(*) OVER gr AS gr, -- (A?? B??)+ reluctant body + count(*) OVER gg2 AS gg2, -- (A? B?){2,} greedy body, min>=2 + count(*) OVER gr2 AS gr2 -- (A?? B??){2,} reluctant body, min>=2 +FROM t +WINDOW gg AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A? B?)+) DEFINE A AS isa, B AS isb), + gr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?? B??)+) DEFINE A AS isa, B AS isb), + gg2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A? B?){2,}) DEFINE A AS isa, B AS isb), + gr2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?? B??){2,}) DEFINE A AS isa, B AS isb); + id | gg | gr | gg2 | gr2 +----+----+----+-----+----- + 1 | 4 | 0 | 4 | 0 + 2 | 0 | 0 | 0 | 0 + 3 | 0 | 0 | 0 | 0 + 4 | 0 | 0 | 0 | 0 + 5 | 0 | 0 | 0 | 0 +(5 rows) + +-- Branch position inside a quantified alternation. fillRPRPatternAlt() ORs +-- nullability across every branch but takes empty-preferred from the FIRST +-- branch alone, so the two reductions are asymmetric. Every empty-preferred +-- branch elsewhere in this file leads its alternation, which only exercises +-- the direction that propagates the bit; these columns exercise the direction +-- that must suppress it. With the empty-preferred branch second the group is +-- nullable but not empty-preferred, so the loop-back is explored first and the +-- match runs long; swapping the branches makes the empty derivation win. The +-- min>=2 forms are the ones that can tell the two apart -- at min 1 the exit +-- is reachable either way. +WITH t(id, isa, isb) AS + (VALUES (1,true,false),(2,true,false),(3,true,false),(4,false,false)) +SELECT id, + count(*) OVER nf1 AS nf1, -- (A | B??){2,} empty-preferred branch second + count(*) OVER nf2 AS nf2, -- (A | B*?){2,} likewise, with a star + count(*) OVER fst AS fst, -- (B?? | A){2,} empty-preferred branch first + count(*) OVER nfp AS nfp -- (A | B??)+ same as nf1 at min 1 +FROM t +WINDOW nf1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A | B??){2,}) DEFINE A AS isa, B AS isb), + nf2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A | B*?){2,}) DEFINE A AS isa, B AS isb), + fst AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((B?? | A){2,}) DEFINE A AS isa, B AS isb), + nfp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A | B??)+) DEFINE A AS isa, B AS isb); + id | nf1 | nf2 | fst | nfp +----+-----+-----+-----+----- + 1 | 3 | 3 | 0 | 3 + 2 | 0 | 0 | 0 | 0 + 3 | 0 | 0 | 0 | 0 + 4 | 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 +); + 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) + +-- Long partition (> 1024 rows), a match at every other row +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 +-- ============================================================ +-- Run a query under EXPLAIN ANALYZE and keep only the Pattern line and the +-- NFA counters, which are platform-independent; the rest of the plan +-- (sort and storage memory) is not. Plan output is covered in rpr_explain. +CREATE FUNCTION rpr_nfa_counters(query text) RETURNS SETOF text +LANGUAGE plpgsql AS $$ +DECLARE + ln text; +BEGIN + FOR ln IN EXECUTE + 'EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) ' + || query + LOOP + IF ln ~ '^\s*(Pattern|NFA)' THEN + RETURN NEXT ltrim(ln); + END IF; + END LOOP; +END; +$$; +-- 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 A+? is never absorbable. The rows and pattern are those of +-- test_reluctant_absorption, whose results show four one-row matches; +-- here the Pattern line has no absorption marker and no context is +-- absorbed or skipped. +SELECT rpr_nfa_counters($$ +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 PAST LAST ROW + PATTERN (A+?) + DEFINE + A AS 'A' = ANY(flags) +)$$); + rpr_nfa_counters +-------------------------------------------- + Pattern: a+? + NFA States: 2 peak, 10 total, 0 merged + NFA Contexts: 2 peak, 6 total, 1 pruned + NFA: 4 matched (len 1/1/1.0), 0 mismatched +(4 rows) + +-- Absorbed contexts: greedy A+ over the same rows (as test_absorbable). +-- The Pattern line marks A+ absorbable, and the contexts of rows 2-4 are +-- absorbed into row 1's, which matches 1-4. +SELECT rpr_nfa_counters($$ +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 PAST LAST ROW + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +)$$); + rpr_nfa_counters +-------------------------------------------- + Pattern: a+# + NFA States: 3 peak, 10 total, 0 merged + NFA Contexts: 3 peak, 6 total, 1 pruned + NFA: 1 matched (len 4/4/4.0), 0 mismatched + NFA: 3 absorbed (len 1/1/1.0), 0 skipped +(5 rows) + +-- The same query with SKIP TO NEXT ROW: absorption is disabled, so the +-- pattern carries no marker, nothing is absorbed, and each of rows 1-4 +-- gets its own match. +SELECT rpr_nfa_counters($$ +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) +)$$); + rpr_nfa_counters +-------------------------------------------- + Pattern: a+ + NFA States: 9 peak, 16 total, 0 merged + NFA Contexts: 6 peak, 6 total, 1 pruned + NFA: 4 matched (len 1/4/2.5), 0 mismatched +(4 rows) + +-- Skipped contexts: A B C is not absorbable, so the contexts started at +-- rows 2 and 3 are still live when row 1's match ends at row 3. +-- SKIP PAST LAST ROW frees both as skipped (lengths 2 and 1); row 4 has no +-- match. +WITH test_skipped AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A', 'B']), + (3, ARRAY['A', 'B', 'C']), -- Completes match starting at row 1 + (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_skipped +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 | {A,B} | | + 3 | {A,B,C} | | + 4 | {C} | | +(4 rows) + +SELECT rpr_nfa_counters($$ +WITH test_skipped AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A', 'B']), + (3, ARRAY['A', '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_skipped +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_nfa_counters +-------------------------------------------- + Pattern: a b c + NFA States: 3 peak, 5 total, 0 merged + NFA Contexts: 3 peak, 5 total, 1 pruned + NFA: 1 matched (len 3/3/3.0), 0 mismatched + NFA: 0 absorbed, 2 skipped (len 1/2/1.5) +(5 rows) + +-- The same rows with SKIP TO NEXT ROW: nothing is skipped, and row 2's +-- context goes on to its own overlapping match 2-4. +WITH test_skipped AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A', 'B']), + (3, ARRAY['A', '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_skipped +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,B} | 2 | 4 + 3 | {A,B,C} | | + 4 | {C} | | +(4 rows) + +SELECT rpr_nfa_counters($$ +WITH test_skipped AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A', 'B']), + (3, ARRAY['A', '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_skipped +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) +)$$); + rpr_nfa_counters +---------------------------------------------------------- + Pattern: a b c + NFA States: 4 peak, 5 total, 0 merged + NFA Contexts: 4 peak, 5 total, 1 pruned + NFA: 2 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0) +(4 rows) + +DROP FUNCTION rpr_nfa_counters(text); +-- ============================================================ +-- 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*? tries the skip path first; no row is B here, so nothing matches +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) + +-- ((A B){1,3})+: flattened to (A B)+ by the optimizer, so only one +-- group level runs; kept as a result check for the nested spelling +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) + +-- Reluctant group (A B)+? with following element C +-- The group tries to exit after each iteration: from row 1 row 3 is not C, +-- so it takes a second iteration (1-5); from row 3 one suffices (3-5) +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 nullable groups: ((A?){2,3}){2,3} +-- The child's min is 0 at both levels, so the optimizer multiplies them +-- and this runs as A{0,9}: no group END or fast-forward path remains. +-- Data has no A rows, so every row matches empty. +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 +-- Newer contexts are absorbed row by row; the !rpr_prepare_row() path at +-- partition end only finalizes the remaining contexts. +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) +-- Each A row's advance adds a non-absorbable B state beside A+; it dies on +-- the next A row before the absorb phase, so the contexts of rows 2-3 are +-- still absorbed. Row 4's context is skipped by the 1-4 match. +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 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} | | + 4 | {B} | | + 5 | {_} | | +(5 rows) + +-- Dynamic flag update ((A+) | B) +-- A new context starts with states in both branches; once its B state dies +-- it becomes absorbable, so rows 2-3 are absorbed into the 1-3 match. +-- Rows 4 and 6 then match through B, and row 5 alone through A+. +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 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} | | + 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 state 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, even with SKIP PAST LAST ROW. +-- Compare with greedy (A+) | B above: rows 1-4 each match one row here. +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 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 | {A} | 2 | 2 + 3 | {A} | 3 | 3 + 4 | {B} | 4 | 4 + 5 | {_} | | +(5 rows) + +-- ============================================================ +-- Zero-Consumption Cycle Detection +-- ============================================================ +-- (A*)*: flattened to A* by the optimizer, so no group END and no cycle +-- guard is involved; kept as a result check for the nested spelling +WITH test_cycle_nonzero AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']) -- A* stops here + ) AS 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. +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 permitted in the WINDOW clause +-- Per 6.13, "the anchors (^ and $) are not permitted with row pattern +-- matching in windows". R020 conformance: these must stay rejected; +-- this is not a gap to be filled later. +-- ------------------------------------------------------------ +-- ^ anchor: rejected +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: rejected +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) +-- ------------------------------------------------------------ +-- The standard works this rule out by listing the iteration traces of +-- the quantifier. Below, A is an iteration that matched a row and () +-- one that matched nothing. An empty iteration is allowed only as the +-- last one, or at a position below the lower bound. +-- (A?){0,3}: (), (A), (()), (A A), (A ()), (A A A), (A A ()) +-- (A?){1,3}: the same, less () -- it does not meet the lower bound +-- (A?){2,3}: (A A), (A ()), (() A), (() ()), (A A A), (A A ()), +-- (() A A), (() A ()) -- a non-final empty iteration at +-- position 1 fills the lower bound of 2 +-- (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. Two empty iterations -- ( () () ) -- +-- are valid here: the first is below the lower bound, so it does not +-- stop the loop. +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, 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. Distinct variable names keep the alternations +-- from being merged into the rolled form above, yet 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)); + 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){1,2} C over the same rows: the lower bound is met by the first +-- iteration, so an empty first iteration via A? stops the loop at once; +-- C fails at row 1 and backtracking takes B, then A, C = rows 1-3, as Perl's +-- (?:a?|b){1,2}c does. This is the first arrival at the group's END, which +-- the cycle guard has to recognize as empty although it has not seen that +-- END before; an engine that misses it keeps the empty iteration and +-- returns rows 1-2 through empty, B, C. +WITH test_728_stop_first_iteration 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_first_iteration +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 '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) + +-- The same with a lower bound of zero, which the standard groups with one. +WITH test_728_stop_first_iteration_min0 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_first_iteration_min0 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? | B){0,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 | {B} | 1 | 3 + 2 | {A,C} | 2 | 3 + 3 | {C} | 3 | 3 +(3 rows) + +-- The same under SKIP PAST LAST ROW: rows 2 and 3 fall inside the match. +WITH test_728_stop_first_iteration_past 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_first_iteration_past +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A? | B){1,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 | {B} | 1 | 3 + 2 | {A,C} | | + 3 | {C} | | +(3 rows) + +-- (B?? | B*){0,2} C: the reluctant first branch goes empty first, which at +-- min 0 stops the loop, and C fails at row 1. Backtracking lets B?? take +-- row 1, the second iteration goes empty and stops, C fails at row 2, so +-- B?? takes row 2 as well and C matches row 3: rows 1-3, as in Perl. +-- Missing the empty first iteration instead lets the loop run again after +-- it, and the match becomes the longer rows 1-4 (empty, then B* over rows +-- 1-3, then C) -- a derivation 7.2.8 excludes. +WITH test_728_stop_first_iteration_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['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_728_stop_first_iteration_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((B?? | B*){0,2} C) + DEFINE + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | 1 | 3 + 2 | {B} | 2 | 3 + 3 | {B,C} | 3 | 3 + 4 | {C} | 4 | 4 +(4 rows) + +-- (A?? | B*){0,2} C: the reluctant empty iteration stops the loop, and +-- backtracking reaches B* over rows 1-2, then A?? taking row 3 in the +-- second iteration and C on row 4: rows 1-4, as in Perl. Missing the empty +-- first iteration returns rows 1-2 instead. +WITH test_728_stop_first_iteration_mixed AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B','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_728_stop_first_iteration_mixed +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?? | B*){0,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 | {B} | 1 | 4 + 2 | {B,C} | 2 | 2 + 3 | {A} | 3 | 4 + 4 | {C} | 4 | 4 +(4 rows) + +-- (A? | B){3} C over the rows of test_728_stop_binds_at_min: 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 in that test. +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) + diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 38c9b04f72c..8753d2f13de 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -3781,6 +3781,60 @@ INSERT INTO hats VALUES ('h7', 'black') RETURNING *; DROP RULE hat_confsel ON hats; drop table hats; drop table hat_data; +-- An unnamed FULL JOIN USING makes USING names unique query-wide, but an RTE +-- outside the FROM clause has no alias list to carry a renamed column. +create table rule_uniq1 (x int, y int); +create table rule_uniq2 (x int, z int); +create table rule_uniq3 (x int, w int); +create table rule_uniq_log (x int); +create rule rule_uniq_r as on update to rule_uniq1 do also + insert into rule_uniq_log + select g1.y from rule_uniq1 g1, rule_uniq2 full join rule_uniq3 using (x) + where g1.x <> new.x; +create rule rule_uniq_u as on update to rule_uniq1 do also + update rule_uniq_log set x = 1 + from rule_uniq2 full join rule_uniq3 using (x) + where rule_uniq_log.x = 0; +-- g1 is in the FROM clause and is re-aliased to x_1; new and the update +-- target are not, so they keep x +select rulename, definition from pg_rules where tablename = 'rule_uniq1' + order by rulename; + rulename | definition +-------------+----------------------------------------------------------------------------------- + rule_uniq_r | CREATE RULE rule_uniq_r AS + + | ON UPDATE TO public.rule_uniq1 DO INSERT INTO rule_uniq_log (x) SELECT g1.y+ + | FROM rule_uniq1 g1(x_1, y), + + | (rule_uniq2 + + | FULL JOIN rule_uniq3 USING (x)) + + | WHERE (g1.x_1 <> new.x); + rule_uniq_u | CREATE RULE rule_uniq_u AS + + | ON UPDATE TO public.rule_uniq1 DO UPDATE rule_uniq_log SET x = 1 + + | FROM (rule_uniq2 + + | FULL JOIN rule_uniq3 USING (x)) + + | WHERE (rule_uniq_log.x = 0); +(2 rows) + +drop table rule_uniq1, rule_uniq2, rule_uniq3, rule_uniq_log; +-- The subquery an INSERT ... SELECT reads from is outside the FROM clause +-- too, but it is printed, so its unnamed columns still need unique names. +-- LIMIT keeps the subquery from being pulled up, and the unfilled column +-- makes the plan project through a subquery scan. +create table rule_uniq_ins (a int, b int, c text, d int); +explain (verbose, costs off) + insert into rule_uniq_ins (a, b, c) + select a + 1, b + 1, c || c from rule_uniq_ins limit 2; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------- + Insert on public.rule_uniq_ins + -> Subquery Scan on unnamed_subquery + Output: unnamed_subquery."?column?", unnamed_subquery."?column?_1", unnamed_subquery."?column?_2", NULL::integer + -> Limit + Output: ((rule_uniq_ins_1.a + 1)), ((rule_uniq_ins_1.b + 1)), ((rule_uniq_ins_1.c || rule_uniq_ins_1.c)) + -> Seq Scan on public.rule_uniq_ins rule_uniq_ins_1 + Output: (rule_uniq_ins_1.a + 1), (rule_uniq_ins_1.b + 1), (rule_uniq_ins_1.c || rule_uniq_ins_1.c) +(7 rows) + +drop table rule_uniq_ins; -- test for pg_get_functiondef properly regurgitating SET parameters -- Note that the function is kept around to stress pg_dump. CREATE FUNCTION func_with_set_params() RETURNS integer diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out index c0bde1c5eec..2a267e6fc8d 100644 --- a/src/test/regress/expected/window.out +++ b/src/test/regress/expected/window.out @@ -1037,6 +1037,42 @@ FROM tenk1 WHERE unique1 < 10; 7 | 7 | 3 (10 rows) +SELECT nth_value(unique1,2) over (ORDER BY four rows between current row and 3 following exclude ties), + unique1, four +FROM tenk1 WHERE unique1 < 10; + nth_value | unique1 | four +-----------+---------+------ + 5 | 0 | 0 + 5 | 8 | 0 + 5 | 4 | 0 + 6 | 5 | 1 + 6 | 9 | 1 + 6 | 1 | 1 + 3 | 6 | 2 + 3 | 2 | 2 + | 3 | 3 + | 7 | 3 +(10 rows) + +SELECT last_value(unique1) over (ORDER BY four rows between 1 following and 2 following exclude ties), + unique1, four +FROM tenk1 WHERE unique1 < 12 ORDER BY four, unique1; + last_value | unique1 | four +------------+---------+------ + 1 | 0 | 0 + | 4 | 0 + 5 | 8 | 0 + 6 | 1 | 1 + | 5 | 1 + 10 | 9 | 1 + 3 | 2 | 2 + | 6 | 2 + 7 | 10 | 2 + | 3 | 3 + | 7 | 3 + | 11 | 3 +(12 rows) + SELECT sum(unique1) over (rows between 2 preceding and 1 preceding), unique1, four FROM tenk1 WHERE unique1 < 10; -- 2.43.0