From 8e5bcb8be68f094d424833799cc1f3ca2d558b06 Mon Sep 17 00:00:00 2001 From: Henson Choi Date: Mon, 28 Sep 2026 15:53:22 +0900 Subject: [PATCH 08/10] Tidy and extend the row pattern recognition regression tests This commit changes tests only. Apart from the new and fixed tests, the expected output changes only where it echoes a comment, shows statement text or names a renamed object (error messages and hints, a plan, deparsed view definitions), and in the LINE and caret of the variable-count error. 1. Mechanical cleanups Object names. The five row pattern recognition test files share one parallel_schedule group, so a permanent object one of them creates is visible to the other four while they run. Names such as stock_price, truthyint, boolish, nt, ct and get_windowagg_cost are not reserved by anything, and a later test choosing one of them would fail only in a parallel run. Prefix every object these files create with rpr_, and at the same time give each a name that says what it holds: stock becomes rpr_price, t1 and t2 become rpr_join_left and rpr_join_right, nt and ct become rpr_nav_rows and rpr_composite_rows, rpr1 becomes rpr_prev_multicol, and so on. Temporary tables are renamed as well, for consistency. The prev and next functions that the name-resolution tests are about keep their names, as does last(), which already lives in the rpr_navns schema; so do the data/stock.data file and common table expressions. Redundant ORDER BY. Drop the outer ORDER BY from about a hundred statements, nearly all of them in rpr_base.sql and a few in rpr_nfa.sql and rpr_integration.sql, whose only sort is the one their window already performs, so the clause asked for an order the plan produced anyway. Statements where the clause does something are left alone: those with no window, a different key, direction or null placement, a set operation or a join above the window, and EXPLAIN tests where it is part of the plan under test. Variable-count boundary. Build the two variable-count boundary cases in rpr_base.sql with string_agg() over generate_series() and run them through \gexec under \set ECHO none, as the nesting-depth boundary beside them already does, instead of spelling out all 240 variables in PATTERN and DEFINE and echoing them back in the expected output. The comment now says why the limit is 240: a varId is one byte whose high nibble is reserved for control elements, so RPR_VARID_MAX is 0xEF. The rejected case adds V241 to PATTERN only, relying on its implicit TRUE definition being counted. The error and its detail are unchanged; only the LINE and caret move, since PATTERN is now on one line. 2. New tests Add tests for combinations that nothing else in the suite pinned: - rpr.sql: a DEFINE that navigates twice, to different rows, over a bare text column, so the second fetch frees the tuple the first result points into and only the datumCopy in EEOP_RPR_NAV_RESTORE keeps that result alive. The neighboring tests navigate through a cast, whose result is freshly allocated and never points into the fetched tuple. - rpr_nfa.sql: two queries beside the greedy/reluctant matrix. One uses a multi-element sequence body, where the empty-preferred flag must survive the AND-reduction fillRPRPattern() performs over a sequence's children. The other puts an empty-preferred alternation branch in a non-first position, which fillRPRPatternAlt() must not propagate, since it takes that flag from the first branch only. - rpr_integration.sql: the A4 dedup section gains an EXPLAIN and a result query over two inline windows that differ only in AFTER MATCH SKIP mode, so a dedup that stopped comparing rpSkipTo would be caught; its header comment now says that transformWindowFuncCall() compares the whole RPCommonSyntax node. - rpr_base.sql: a GROUP BY written as the COALESCE expansion of a FULL JOIN ... USING column plus one, while DEFINE reads the merged column itself as id + 1; the two spellings must match as the same grouping expression. A view built the same way checks that the DEFINE clause deparses to the plain join column rather than the COALESCE, and that the printed definition re-parses into a view with the same definition. Another test checks that a DEFINE clause repeating a volatile GROUP BY expression runs, reading the value the grouping step computed once per input row. - window.sql: EXCLUDE TIES coverage. nth_value() had it only for the first row of the frame, never for a later position, which the adjustment must shift past the current row's peers; and last_value() had it only over frames containing the current row, never over a frame lying wholly after it, where the tie adjustment lands on the current row, outside the frame. - create_view.sql: a function result column grown after the view was created and dropped again, with another grown behind it. 3. Tests that tested less than their comments said Fix tests that the earlier commits of this series left testing less than their comments say. - rpr_base.sql: - The dead CASE arm volatility test and the WHERE false dummy-rel test now reference their window, since the planner drops the DEFINE clause of an unreferenced window before either check runs. - The unreferenced-window substitution test now reads an ungrouped column and expects the grouping error, which only the substitution can raise. - rpr_partrow gets rows sharing a partition so that its DEFINE condition is evaluated. - rpr_res_fct() builds its row with json_populate_record(), so the rpr_res_sys views return rows instead of failing with an unrelated cast error. - rpr_integration.sql: the two SubLink tests are arranged so that the entry holding the SubLink survives and is walked, and they and the lateral varno/varattno test show the plan, so that each reaches and shows the code its comment names. The tests written around the DEFINE handling that remove_unused_subquery_outputs() no longer has now say what they check under the current code, and one that only exercised its SubLink walk is dropped. - rpr_nfa.sql: the absorption and skip tests ran under AFTER MATCH SKIP TO NEXT ROW, which disables absorption and skips nothing. They now use SKIP PAST LAST ROW, the statistics tests print the NFA counters through a small helper that keeps only the platform-independent lines, and the skip test uses a pattern that cannot be absorbed, so that its overlapping contexts are really skipped. SKIP TO NEXT ROW variants are kept where the contrast is the point. 4. Test comments Reword test comments that still described removed mechanisms: DEFINE Var planting, the "frame must start at current row" error, the exact match path, the rowExists variable, BEGIN.jump as a skip target, the join alias Vars an inner JOIN USING does not make, and the DEFINE columns an upper WindowAgg no longer carries; and correct the 7.2.8 trace summaries in rpr_nfa.sql and the absorbability measurement comment in rpr_base.sql. Correct the other test comments that the code or the expected output below them does not bear out. - rpr_base.sql: the pattern optimization comments name the rewrite that actually applies (quantifier multiplication rather than a GROUP merge or unwrap, the SUFFIX merge for the reluctant case), the absorbability labels match what the patterns are, the error limit and variable-count comments give the real reason, and the deparse, serialization and quoting section headers say what their tests do. - rpr_nfa.sql: the absorption and skip comments no longer claim absorption or skipped contexts under SKIP TO NEXT ROW, which rules both out; nested group tests say where the planner flattens the pattern, so no nested END transition or cycle guard is reached; and the 7.2.8 cross references point at the right tests. The 7.2.6 anchor tests were labelled "not yet implemented", but ISO/IEC 19075-5 6.13 does not permit the anchors ^ and $ with row pattern matching in windows, so rejecting them is what Feature R020 requires rather than a gap. The 7.2.8 comments used the standard's STRE and STRnn trace shorthand without its definitions; write the traces out instead, and likewise in the one such comment in rpr_explain.sql. - rpr_explain.sql: the pattern and deparse descriptions match the output, the offset section headers say the executor resolves the offsets, and a quoted error message is one the code raises. - rpr_integration.sql: the join removal, PlaceHolderVar, negative offset and overflow comments describe the current mechanisms, and B9 no longer credits backtracking, which the engine does not do. - rpr.sql: the last_value IGNORE NULLS comment describes the behavior the test checks. Author: Henson Choi --- src/test/regress/expected/create_view.out | 40 + src/test/regress/expected/rpr.out | 485 +++--- src/test/regress/expected/rpr_base.out | 1310 ++++++++++------- src/test/regress/expected/rpr_explain.out | 64 +- src/test/regress/expected/rpr_integration.out | 393 +++-- src/test/regress/expected/rpr_nfa.out | 495 +++++-- src/test/regress/expected/window.out | 36 + src/test/regress/sql/create_view.sql | 16 + src/test/regress/sql/rpr.sql | 434 +++--- src/test/regress/sql/rpr_base.sql | 1014 ++++++------- src/test/regress/sql/rpr_explain.sql | 64 +- src/test/regress/sql/rpr_integration.sql | 291 ++-- src/test/regress/sql/rpr_nfa.sql | 381 +++-- src/test/regress/sql/window.sql | 8 + 14 files changed, 3052 insertions(+), 1979 deletions(-) diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out index c471a3eb974..3d9cbfdad45 100644 --- a/src/test/regress/expected/create_view.out +++ b/src/test/regress/expected/create_view.out @@ -1379,6 +1379,46 @@ select * from view_of_grown_input_2; 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; diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out index c713588c6a3..1958fc67e32 100644 --- a/src/test/regress/expected/rpr.out +++ b/src/test/regress/expected/rpr.out @@ -20,8 +20,8 @@ CREATE TABLE rpr_stock ( \set filename :abs_srcdir '/data/stock.data' COPY rpr_stock FROM :'filename'; ANALYZE rpr_stock; -CREATE TEMP TABLE stock (company TEXT, tdate DATE, price INTEGER); -INSERT INTO stock VALUES +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), @@ -32,7 +32,7 @@ INSERT INTO stock VALUES ('company2', '2023-07-05', 1500), ('company2', '2023-07-06', 60), ('company2', '2023-07-07', 1100), ('company2', '2023-07-08', 1300), ('company2', '2023-07-09', 1200), ('company2', '2023-07-10', 1300); -SELECT * FROM stock; +SELECT * FROM rpr_price; company | tdate | price ----------+------------+------- company1 | 07-01-2023 | 100 @@ -63,7 +63,7 @@ SELECT * FROM stock; -- basic test using PREV SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, nth_value(tdate, 2) OVER w AS nth_second - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -101,7 +101,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- basic test using PREV. UP appears twice SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, nth_value(tdate, 2) OVER w AS nth_second - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -139,7 +139,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- basic test using PREV. Use '*' SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, nth_value(tdate, 2) OVER w AS nth_second - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -177,7 +177,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- basic test using PREV. Use '?' SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, nth_value(tdate, 2) OVER w AS nth_second - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -214,7 +214,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- test using alternation (|) with sequence SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -251,7 +251,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- test using alternation (|) with group quantifier SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -288,7 +288,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- test using nested alternation SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -326,7 +326,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- test using group with quantifier SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -363,7 +363,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- test using absolute threshold values (not relative PREV) -- HIGH: price > 150, LOW: price < 100, MID: neutral range SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -400,7 +400,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- test threshold-based pattern with alternation SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -437,7 +437,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- basic test with fixed-length pattern (A A A = exactly 3) SELECT company, tdate, price, count(*) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -472,7 +472,7 @@ SELECT company, tdate, price, count(*) OVER w -- test using {n} quantifier (A A A should be optimized to A{3}) SELECT company, tdate, price, count(*) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -507,7 +507,7 @@ SELECT company, tdate, price, count(*) OVER w -- test using {n,} quantifier (2 or more) SELECT company, tdate, price, count(*) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -542,7 +542,7 @@ SELECT company, tdate, price, count(*) OVER w -- test using {n,m} quantifier (2 to 4) SELECT company, tdate, price, count(*) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -577,15 +577,15 @@ SELECT company, tdate, price, count(*) OVER w -- test prefix/suffix merge optimization with bounded quantifier -- Pattern A B (A B){1,2} A B should be optimized to (A B){3,4} -CREATE TEMP TABLE rpr_t (id int, val text); -INSERT INTO rpr_t VALUES +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_t +FROM rpr_ab_pairs WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -609,10 +609,10 @@ WINDOW w AS ( 9 | X | 0 (9 rows) -DROP TABLE rpr_t; +DROP TABLE rpr_ab_pairs; -- last_value() should remain consistent SELECT company, tdate, price, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -652,7 +652,7 @@ SELECT company, tdate, price, last_value(price) OVER w -- implicitly defined. per spec. SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, nth_value(tdate, 2) OVER w AS nth_second - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -688,7 +688,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- the first row start with less than or equal to 100 SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -725,7 +725,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- second row raises 120% SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -762,7 +762,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- using NEXT SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -800,7 +800,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- match length is always 2, so result is identical to SKIP PAST LAST ROW. -- SKIP TO NEXT ROW's distinct effect is tested in backtracking section.) SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -837,7 +837,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- PREV returns NULL at the partition's first row (no earlier row to fetch) SELECT company, tdate, price, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -873,7 +873,7 @@ WINDOW w AS ( -- NEXT returns NULL at the partition's last row (no later row to fetch) SELECT company, tdate, price, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -910,7 +910,7 @@ WINDOW w AS ( -- DESC order: PREV refers to the row with later date SELECT company, tdate, price, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate DESC @@ -1009,7 +1009,7 @@ WINDOW w AS ( -- Error cases: PREV/NEXT usage restrictions -- -- Nested PREV -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1022,7 +1022,7 @@ LINE 7: DEFINE A AS price > PREV(PREV(price)) ^ HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. -- Nested NEXT -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1035,7 +1035,7 @@ LINE 7: DEFINE A AS price > NEXT(NEXT(price)) ^ HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. -- PREV nested inside NEXT -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1048,7 +1048,7 @@ LINE 7: DEFINE A AS price > NEXT(PREV(price)) ^ HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. -- PREV nested inside expression inside NEXT -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1061,7 +1061,7 @@ LINE 7: DEFINE A AS price > NEXT(price * PREV(price)) ^ HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. -- Triple nesting: error reported at outermost PREV -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1075,7 +1075,7 @@ LINE 7: DEFINE A AS price > PREV(PREV(PREV(price))) HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. -- No column reference in PREV/NEXT argument -- PREV(1): constant only, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1087,7 +1087,7 @@ ERROR: argument of row pattern navigation operation must include at least one c LINE 7: DEFINE A AS PREV(1) > 0 ^ -- NEXT(1 + 2): constant expression, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1099,7 +1099,7 @@ ERROR: argument of row pattern navigation operation must include at least one c LINE 7: DEFINE A AS NEXT(1 + 2) > 0 ^ -- 2-arg form: PREV(1, 1): constant expression as first arg -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1113,7 +1113,7 @@ LINE 7: DEFINE A AS PREV(1, 1) > 0 -- Compound navigation without a column reference must be rejected too, -- consistent with the simple forms above. -- PREV(FIRST(1)): compound, constant only, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1125,7 +1125,7 @@ ERROR: argument of row pattern navigation operation must include at least one c LINE 7: DEFINE A AS PREV(FIRST(1)) > 0 ^ -- NEXT(LAST(1 + 2)): compound, constant expression, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1137,7 +1137,7 @@ ERROR: argument of row pattern navigation operation must include at least one c LINE 7: DEFINE A AS NEXT(LAST(1 + 2)) > 0 ^ -- PREV(FIRST(1, 2)): compound, two-arg inner, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1149,7 +1149,7 @@ ERROR: argument of row pattern navigation operation must include at least one c LINE 7: DEFINE A AS PREV(FIRST(1, 2)) > 0 ^ -- PREV(FIRST(1), 2): compound, outer offset only, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1161,7 +1161,7 @@ ERROR: argument of row pattern navigation operation must include at least one c LINE 7: DEFINE A AS PREV(FIRST(1), 2) > 0 ^ -- PREV(FIRST(1, 2), 3): compound, inner and outer offsets, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1173,7 +1173,7 @@ ERROR: argument of row pattern navigation operation must include at least one c LINE 7: DEFINE A AS PREV(FIRST(1, 2), 3) > 0 ^ -- Non-constant offset: column reference as offset -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1185,7 +1185,7 @@ ERROR: row pattern navigation offset must be a run-time constant LINE 7: DEFINE A AS PREV(price, price) > 0 ^ -- Non-constant offset: column reference in compound inner offset -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1197,7 +1197,7 @@ ERROR: row pattern navigation offset must be a run-time constant LINE 7: DEFINE A AS PREV(LAST(price, price), 2) > 0 ^ -- Non-constant offset: column reference in compound outer offset -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1209,7 +1209,7 @@ ERROR: row pattern navigation offset must be a run-time constant LINE 7: DEFINE A AS PREV(LAST(price, 1), price) > 0 ^ -- Non-constant offset: volatile function as offset -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1219,7 +1219,7 @@ WINDOW w AS ( ); ERROR: DEFINE clause cannot contain volatile functions -- Non-constant offset: volatile function as compound outer offset -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1229,7 +1229,7 @@ WINDOW w AS ( ); ERROR: DEFINE clause cannot contain volatile functions -- Non-constant offset: subquery as offset -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1241,7 +1241,7 @@ ERROR: cannot use subquery in DEFINE expression LINE 7: DEFINE A AS PREV(price, (SELECT 1)) > 0 ^ -- First arg: subquery (caught by DEFINE-level subquery restriction) -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1255,7 +1255,7 @@ LINE 7: DEFINE A AS PREV(price + (SELECT 1)) > 0 -- Volatile function inside nav.arg is rejected in the planner SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1265,7 +1265,7 @@ WINDOW w AS ( ERROR: DEFINE clause cannot contain volatile functions -- nextval is volatile, so a DEFINE that calls it is rejected CREATE SEQUENCE rpr_seq; -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1279,7 +1279,7 @@ DROP SEQUENCE rpr_seq; -- created successfully and errors only when read. CREATE TEMP VIEW rpr_volatile_view AS SELECT company, tdate, price, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1296,7 +1296,7 @@ DROP VIEW rpr_volatile_view; -- Qualified outer reference (o.threshold): SELECT * FROM (VALUES (95)) AS o(threshold), LATERAL ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1311,7 +1311,7 @@ LINE 9: DEFINE A AS price > o.threshold -- Unqualified name resolving to the outer column (threshold): SELECT * FROM (VALUES (95)) AS o(threshold), LATERAL ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1326,7 +1326,7 @@ LINE 9: DEFINE A AS price > threshold -- Outer reference inside a navigation argument is rejected too: SELECT * FROM (VALUES (95)) AS o(threshold), LATERAL ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1342,7 +1342,7 @@ LINE 8: DEFINE A AS PREV(o.threshold, 1) > 0 -- keeps its own diagnosis rather than being reported as a qualifier problem. SELECT * FROM (VALUES (95)) AS o(threshold), LATERAL ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1357,7 +1357,7 @@ 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 stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1376,7 +1376,7 @@ HINT: Perhaps you meant to reference the column "o.threshold". -- 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 stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1394,7 +1394,7 @@ DECLARE n bigint; BEGIN SELECT count(*) INTO n FROM ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1411,7 +1411,7 @@ 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 stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1424,7 +1424,7 @@ 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 stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1447,7 +1447,7 @@ DROP FUNCTION rpr_sqlfn(int); -- enter into it. CREATE FUNCTION rpr_pv(threshold int) RETURNS SETOF int LANGUAGE sql AS $$ - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1462,7 +1462,7 @@ LINE 9: DEFINE rpr_pv AS price > rpr_pv.threshold) -- defined: any pattern variable of that name reserves it. CREATE FUNCTION rpr_pv(threshold int) RETURNS SETOF int LANGUAGE sql AS $$ - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1479,7 +1479,7 @@ LINE 9: DEFINE A AS price > rpr_pv.threshold) 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 stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1493,7 +1493,7 @@ 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 stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1520,7 +1520,7 @@ DECLARE n bigint; BEGIN SELECT count(*) INTO n FROM ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1544,12 +1544,12 @@ DROP FUNCTION rpr_plfn_var(int); CREATE FUNCTION rpr_conflictfn_err() RETURNS bigint LANGUAGE plpgsql AS $$ DECLARE - a stock%ROWTYPE; + a rpr_price%ROWTYPE; n bigint; BEGIN a.price := 95; SELECT count(*) INTO n FROM ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1565,7 +1565,7 @@ ERROR: pattern variable qualified expression "a.price" is not supported in DEFI LINE 8: DEFINE A AS price > a.price) ^ QUERY: SELECT count(*) FROM ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1579,12 +1579,12 @@ CREATE FUNCTION rpr_conflictfn() RETURNS bigint LANGUAGE plpgsql AS $$ #variable_conflict use_variable DECLARE - a stock%ROWTYPE; + a rpr_price%ROWTYPE; n bigint; BEGIN a.price := 95; SELECT count(*) INTO n FROM ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1736,7 +1736,7 @@ 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 stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1751,53 +1751,53 @@ 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. (stock --- is a temp table, so it is qualified with pg_temp here.) +-- 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 stock +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.stock.price > 0 + DEFINE A AS pg_temp.rpr_price.price > 0 ); -ERROR: qualified expression "pg_temp.stock.price" is not allowed in DEFINE clause -LINE 7: DEFINE A AS pg_temp.stock.price > 0 +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 stock +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.stock.*) IS NOT NULL + 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.stock.*) IS NOT NULL +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 stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A) - DEFINE A AS (stock.*) IS NOT NULL + DEFINE A AS (rpr_price.*) IS NOT NULL ); ERROR: whole-row reference is not allowed in DEFINE clause -LINE 7: DEFINE A AS (stock.*) IS NOT NULL +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 stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1810,7 +1810,7 @@ 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 stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1826,60 +1826,60 @@ HINT: A DEFINE condition may reference individual columns only. -- transformExpressionList(), whose star expansion binds them by RTE into -- individual column Vars, past every check. DEFINE skips it. -- ROW(schema.table.*): -SELECT price FROM stock +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.stock.*) IS NOT NULL + 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.stock.*) IS NOT NULL +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 stock +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(stock.*) IS NOT NULL + 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(stock.*) IS NOT NULL +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 stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A) - DEFINE A AS (stock.*, 1) IS NOT NULL + DEFINE A AS (rpr_price.*, 1) IS NOT NULL ); ERROR: whole-row reference is not allowed in DEFINE clause -LINE 7: DEFINE A AS (stock.*, 1) IS NOT NULL +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 stock +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((stock.*)) IS NOT NULL + 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((stock.*)) IS NOT NULL +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 stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1892,7 +1892,7 @@ 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 stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1906,7 +1906,7 @@ 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 stock AS a +SELECT price FROM rpr_price AS a WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1917,40 +1917,41 @@ WINDOW w AS ( ERROR: pattern variable qualified expression "a.price" is not supported in DEFINE clause LINE 7: DEFINE A AS a.price > 100 ^ --- Each rejection above classifies 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 stock +-- 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 stock.pric > 0 + DEFINE A AS rpr_price.pric > 0 ); -ERROR: column stock.pric does not exist -LINE 7: DEFINE A AS stock.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 "stock.price". -SELECT price FROM stock +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.stock.pric > 0 + DEFINE A AS pg_temp.rpr_price.pric > 0 ); -ERROR: column stock.pric does not exist -LINE 7: DEFINE A AS pg_temp.stock.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 "stock.price". +HINT: Perhaps you meant to reference the column "rpr_price.price". -- the same typo outside a DEFINE clause, for comparison: -SELECT price FROM stock WHERE stock.pric > 0; -ERROR: column stock.pric does not exist -LINE 1: SELECT price FROM stock WHERE stock.pric > 0; - ^ -HINT: Perhaps you meant to reference the column "stock.price". +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) @@ -2020,7 +2021,7 @@ 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 stock +FROM rpr_price WHERE company = 'company2' AND tdate <= '2023-07-03' WINDOW w AS ( PARTITION BY company @@ -2170,7 +2171,7 @@ DROP TABLE rpr_nest_i, rpr_nest_o; -- 200 -> 150, then 110 -> 130 -> 120, which stops where 130 only ties 130. SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2208,7 +2209,7 @@ WINDOW w AS ( -- then 140, 150 up to where 90 falls short of 130. SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2243,7 +2244,7 @@ WINDOW w AS ( -- PREV(price - 50, 1): fetches (price - 50) from 1 row back SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2277,7 +2278,7 @@ WINDOW w AS ( -- NEXT(price * 2, 1): fetches (price * 2) from 1 row ahead SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2344,7 +2345,7 @@ LIMIT 3; -- A+ matches entire partition as one group; count = partition size SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2377,7 +2378,7 @@ WINDOW w AS ( -- 2-arg PREV/NEXT: negative offset SELECT company, tdate, price, first_value(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2387,7 +2388,7 @@ WINDOW w AS ( ERROR: row pattern navigation offset must not be negative -- 2-arg PREV/NEXT: NULL offset (typed) SELECT company, tdate, price, first_value(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2397,7 +2398,7 @@ WINDOW w AS ( ERROR: row pattern navigation offset must not be null -- 2-arg PREV/NEXT: NULL offset (untyped) SELECT company, tdate, price, first_value(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2408,7 +2409,7 @@ ERROR: row pattern navigation offset must not be null -- 2-arg PREV/NEXT: host variable negative and NULL PREPARE test_prev_offset(int8) AS SELECT company, tdate, price, first_value(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2423,7 +2424,7 @@ DEALLOCATE test_prev_offset; -- 2-arg PREV/NEXT: host variable with expression (0 + $1) PREPARE test_prev_offset(int8) AS SELECT company, tdate, price, first_value(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2441,7 +2442,7 @@ DEALLOCATE test_prev_offset; SET plan_cache_mode = force_generic_plan; PREPARE test_prev_offset(int8) AS SELECT company, tdate, price, first_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2505,7 +2506,7 @@ RESET plan_cache_mode; -- B: price exceeds both 1-back and 2-back values SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2543,7 +2544,7 @@ WINDOW w AS ( -- A: price exceeds 1-back and is below 1-ahead (ascending interior point) SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2580,7 +2581,7 @@ WINDOW w AS ( -- 1-back and 2-back tdate text. SELECT company, tdate, tdate::text AS tdate_text, first_value(tdate::text) OVER w, last_value(tdate::text) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2618,7 +2619,7 @@ WINDOW w AS ( -- B matches when price 1-back > price 2-back (ascending pair). SELECT company, tdate, price::numeric AS nprice, first_value(price::numeric) OVER w, last_value(price::numeric) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2652,6 +2653,33 @@ WINDOW w AS ( 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. @@ -2678,14 +2706,14 @@ DROP TABLE rpr_typmod; -- -- Test data for FIRST/LAST: values cycle back so FIRST(val) = LAST(val) -- at specific positions. -CREATE TEMP TABLE rpr_nav (id int, val int); -INSERT INTO rpr_nav VALUES (1,10),(2,20),(3,30),(4,10),(5,50),(6,10); +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 WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -2705,7 +2733,7 @@ FROM rpr_nav WINDOW w AS ( -- LAST(val): always equals current row's val (offset 0 default) -- Equivalent to: B AS val > 15 SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -2728,7 +2756,7 @@ FROM rpr_nav WINDOW w AS ( -- id2(20!=10), id3(30!=10), id4(10=10) -> match {1,2,3,4} -- match_start=5(50): id6(10!=50) -> no match SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -2750,7 +2778,7 @@ FROM rpr_nav WINDOW w AS ( -- match_start=1(10): greedy A eats all, B tries last: -- id6(10=10) -> match {1,2,3,4,5,6} SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -2770,7 +2798,7 @@ FROM rpr_nav WINDOW w AS ( -- SKIP TO NEXT ROW with FIRST(val) = LAST(val): overlapping match attempts. -- Each row reports only the match that starts at it. SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP TO NEXT ROW @@ -2791,7 +2819,7 @@ FROM rpr_nav WINDOW w AS ( -- -- FIRST(val, 0) = FIRST(val): match_start row SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -2809,10 +2837,11 @@ FROM rpr_nav WINDOW w AS ( (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=1(10): FIRST(val,1)=20, B needs val=20 +-- -> id2(20) match, id3(30) no -- match_start=3(30): FIRST(val,1)=10, B needs val=10 -> id4(10) match SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -2831,7 +2860,7 @@ FROM rpr_nav WINDOW w AS ( -- FIRST(val, 99): offset beyond match range -> NULL, no match SELECT id, val, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -2850,7 +2879,7 @@ FROM rpr_nav WINDOW w AS ( -- LAST(val, 0) = LAST(val): current row SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -2871,7 +2900,7 @@ FROM rpr_nav WINDOW w AS ( -- At B evaluation on id2: LAST(val,1) = val at id1 = 10 -- B matches when previous row val < 30 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -2890,7 +2919,7 @@ FROM rpr_nav WINDOW w AS ( -- LAST(val, 99): offset before match_start -> NULL SELECT id, val, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -2908,7 +2937,7 @@ FROM rpr_nav WINDOW w AS ( (6 rows) -- Error: NULL offset -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -2916,7 +2945,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( ); ERROR: row pattern navigation offset must not be null -- Error: negative offset -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -2934,10 +2963,10 @@ SELECT prev(f), next(f), first(f), last(f) FROM rpr_names f; DROP TABLE rpr_names; -- Compound navigation: PREV(FIRST(val), M) --- rpr_nav: (1,10),(2,20),(3,30),(4,10),(5,50),(6,10) +-- 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 WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -2957,7 +2986,7 @@ FROM rpr_nav WINDOW w AS ( -- NEXT(FIRST(val, 1), 1): target = match_start + 1 + 1 = match_start + 2 -- At match_start=1, B on id2: target=1+1+1=3(val=30), 30>0 -> true SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -2975,12 +3004,13 @@ FROM rpr_nav WINDOW w AS ( (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). +-- 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. +-- At currentpos=3 (start id=2): target=1(val=10) -> in range -> B runs on +-- id3..id6, so the match is id2..id6. SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -3001,10 +3031,10 @@ FROM rpr_nav WINDOW w AS ( -- 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. +-- B stays true through id5 (target=6); at id6 target=7 -> out of range +-- -> NULL, so the match is id1..id5. SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -3023,7 +3053,7 @@ FROM rpr_nav WINDOW w AS ( -- Compound: outer offset beyond partition (PREV far back) SELECT id, val, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B+) @@ -3041,7 +3071,7 @@ FROM rpr_nav WINDOW w AS ( -- Compound: outer offset beyond partition (NEXT far forward) SELECT id, val, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B+) @@ -3059,7 +3089,7 @@ FROM rpr_nav WINDOW w AS ( -- Compound: inner offset beyond match range (FIRST offset too large) SELECT id, val, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B+) @@ -3077,7 +3107,7 @@ FROM rpr_nav WINDOW w AS ( -- Compound: inner offset beyond match range (LAST offset too large) SELECT id, val, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B+) @@ -3094,7 +3124,7 @@ FROM rpr_nav WINDOW w AS ( (6 rows) -- Compound: NULL outer offset (runtime error) -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -3102,7 +3132,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( ); ERROR: row pattern navigation offset must not be null -- Compound: negative outer offset (runtime error) -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( +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+) @@ -3110,30 +3140,30 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( ); ERROR: row pattern navigation offset must not be negative -- Compound: an out-of-range inner offset must not skip validation of the outer --- one. All four arms resolve their outer offset through the same call, so each --- appears once, and the negative and the null case take two arms apiece. -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( +-- 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 WINDOW w 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(LAST(val, 99), NULL::int8) IS NULL ); ERROR: row pattern navigation offset must not be null -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( +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 WINDOW w 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+) @@ -3145,7 +3175,7 @@ ERROR: row pattern navigation offset must not be negative -- The reach reads "runtime" here; a custom plan would fold it to 99 - 1 = 98. SET plan_cache_mode = force_generic_plan; PREPARE test_compound_illegal(int8, int8) AS -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( +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+) @@ -3160,7 +3190,7 @@ EXPLAIN (COSTS OFF) EXECUTE test_compound_illegal(99, 1); Nav Mark Lookahead: runtime -> Sort Sort Key: id - -> Seq Scan on rpr_nav + -> Seq Scan on rpr_nav_cycle (7 rows) EXECUTE test_compound_illegal(99, 1); @@ -3225,7 +3255,7 @@ RESET plan_cache_mode; DROP TABLE rpr_nav_empty; -- Outer offset overflows int64: target position out of range -> NULL. -- Plain NEXT(val, INT64_MAX): currentpos + INT64_MAX overflows. -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( +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+) @@ -3243,7 +3273,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( -- Compound NEXT(FIRST()): outer offset overflow. Inner offset 1 forces -- inner_pos >= 1, so inner_pos + INT64_MAX overflows at every match. -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( +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+) @@ -3260,7 +3290,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( (6 rows) -- Compound NEXT(LAST()): outer offset overflow. -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -3282,7 +3312,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( -- 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 WINDOW w 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+) @@ -3300,7 +3330,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( -- 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 WINDOW w 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+) @@ -3319,7 +3349,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( -- Compound: default offsets on both sides -- PREV(FIRST(val)): inner=0 (match_start), outer=1 -> target = match_start - 1 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -3338,7 +3368,7 @@ FROM rpr_nav WINDOW w AS ( -- NEXT(LAST(val)): inner=0 (currentpos), outer=1 -> target = currentpos + 1 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -3356,7 +3386,7 @@ FROM rpr_nav WINDOW w AS ( (6 rows) -- Compound: inner NULL offset (runtime error) -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -3364,7 +3394,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( ); ERROR: row pattern navigation offset must not be null -- Compound: inner negative offset (runtime error) -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( +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+) @@ -3372,7 +3402,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( ); ERROR: row pattern navigation offset must not be negative -- Offset argument whose type has no implicit cast to bigint (parse error) -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( +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+) @@ -3384,7 +3414,7 @@ LINE 5: DEFINE A AS TRUE, B AS val > PREV(val, 1.5) -- Compound + host variable offsets PREPARE test_compound_offset(int8, int8) AS SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -3416,7 +3446,7 @@ EXECUTE test_compound_offset(1, 1); DEALLOCATE test_compound_offset; -- Compound + SKIP TO NEXT ROW: overlapping matches with PREV(FIRST()) SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP TO NEXT ROW @@ -3458,7 +3488,7 @@ FROM rpr_nav_part WINDOW w AS ( DROP TABLE rpr_nav_part; -- Reverse nesting: FIRST wrapping PREV is prohibited -SELECT id, val FROM rpr_nav WINDOW w AS ( +SELECT id, val FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B) @@ -3469,7 +3499,7 @@ LINE 5: DEFINE A AS TRUE, B AS FIRST(PREV(val)) > 0 ^ HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed. -- Reverse nesting: LAST wrapping NEXT is prohibited -SELECT id, val FROM rpr_nav WINDOW w AS ( +SELECT id, val FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B) @@ -3479,13 +3509,13 @@ 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; +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 stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -3523,7 +3553,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- nth_value beyond reduced frame (no IGNORE NULLS) SELECT company, tdate, price, nth_value(price, 5) OVER w AS nth_5 -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -3562,7 +3592,7 @@ WINDOW w AS ( -- backtracking with reclassification of rows -- using AFTER MATCH SKIP PAST LAST ROW SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -3601,7 +3631,7 @@ SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER -- backtracking with reclassification of rows -- using AFTER MATCH SKIP TO NEXT ROW SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -3690,7 +3720,7 @@ WINDOW w AS ( -- ROWS BETWEEN CURRENT ROW AND offset FOLLOWING SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w, count(*) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -3738,7 +3768,7 @@ SELECT company, tdate, price, sum(price) OVER w, avg(price) OVER w, count(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -3783,7 +3813,7 @@ SELECT company, tdate, price, sum(price) OVER w, avg(price) OVER w, count(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -3821,7 +3851,7 @@ DOWN AS price < PREV(price) -- row_number() within RPR reduced frame SELECT company, tdate, price, row_number() OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -3861,15 +3891,15 @@ WINDOW w AS ( -- SQL Integration: JOIN, CTE, LATERAL -- -- JOIN case -CREATE TEMP TABLE t1 (i int, v1 int); -CREATE TEMP TABLE t2 (j int, v2 int); -INSERT INTO t1 VALUES(1,10); -INSERT INTO t1 VALUES(1,11); -INSERT INTO t1 VALUES(1,12); -INSERT INTO t2 VALUES(2,10); -INSERT INTO t2 VALUES(2,11); -INSERT INTO t2 VALUES(2,12); -SELECT * FROM t1, t2 WHERE t1.v1 <= 11 AND t2.v2 <= 11; +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 @@ -3878,9 +3908,9 @@ SELECT * FROM t1, t2 WHERE t1.v1 <= 11 AND t2.v2 <= 11; 1 | 11 | 2 | 11 (4 rows) -SELECT *, count(*) OVER w FROM t1, t2 +SELECT *, count(*) OVER w FROM rpr_join_left, rpr_join_right WINDOW w AS ( - PARTITION BY t1.i + PARTITION BY rpr_join_left.i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A) @@ -3902,7 +3932,7 @@ WINDOW w AS ( -- WITH case WITH wstock AS ( - SELECT * FROM stock WHERE tdate < '2023-07-08' + SELECT * FROM rpr_price WHERE tdate < '2023-07-08' ) SELECT tdate, price, first_value(tdate) OVER w, @@ -3964,10 +3994,10 @@ ORDER BY g.x, sub.id; (5 rows) -- PREV has multiple column reference -CREATE TEMP TABLE rpr1 (id INTEGER, i SERIAL, j INTEGER); -INSERT INTO rpr1(id, j) SELECT 1, g*2 FROM generate_series(1, 10) AS g; +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 rpr1 + FROM rpr_prev_multicol WINDOW w AS ( PARTITION BY id ORDER BY i @@ -4119,11 +4149,12 @@ RESET jit; -- -- IGNORE NULLS -- --- no NULL rows case. The result should be identical with "basic test using PREV" +-- no NULL rows case. The result should be identical with +-- "basic test using PREV" SELECT company, tdate, price, first_value(price) IGNORE NULLS OVER w, last_value(price) IGNORE NULLS OVER w, nth_value(tdate, 2) IGNORE NULLS OVER w AS nth_second - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -4210,7 +4241,7 @@ WITH data AS ( -- nth_value beyond reduced frame with IGNORE NULLS SELECT company, tdate, price, nth_value(price, 5) IGNORE NULLS OVER w AS nth_5_in -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -4296,7 +4327,8 @@ WINDOW w AS ( -- -- last_value IGNORE NULLS when the reduced frame ends with NULLs --- The search for a non-NULL value runs past the end of the reduced frame. +-- 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); @@ -4473,13 +4505,13 @@ DROP TABLE rpr_dormant; -- -- NULL handling -- -CREATE TEMP TABLE stock_null (company TEXT, tdate DATE, price INTEGER); -INSERT INTO stock_null VALUES ('c1', '2023-07-01', 100); -INSERT INTO stock_null VALUES ('c1', '2023-07-02', NULL); -- NULL in middle -INSERT INTO stock_null VALUES ('c1', '2023-07-03', 200); -INSERT INTO stock_null VALUES ('c1', '2023-07-04', 150); +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 stock_null +FROM rpr_stock_null WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -4788,7 +4820,8 @@ SELECT * FROM ( 1517 | 90.310 | 95.070 | 2 (15 rows) --- Price-volume divergence: price rising while volume declining (bearish signal) +-- 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, diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out index 112892451ec..560e89ac611 100644 --- a/src/test/regress/expected/rpr_base.out +++ b/src/test/regress/expected/rpr_base.out @@ -54,8 +54,7 @@ CREATE TABLE rpr_keywords ( ); INSERT INTO rpr_keywords VALUES (1, 10, 20, 30, 40, 45, 50, 60); SELECT id, define, initial, past, pattern, permute, seek, skip -FROM rpr_keywords -ORDER BY id; +FROM rpr_keywords; id | define | initial | past | pattern | permute | seek | skip ----+--------+---------+------+---------+---------+------+------ 1 | 10 | 20 | 30 | 40 | 45 | 50 | 60 @@ -66,13 +65,13 @@ DROP TABLE rpr_keywords; -- DEFINE Clause Tests -- ============================================================ -- Simple column references -CREATE TABLE stock_price ( +CREATE TABLE rpr_stock_price ( dt DATE, symbol TEXT, price NUMERIC, volume INT ); -INSERT INTO stock_price VALUES +INSERT INTO rpr_stock_price VALUES ('2024-01-01', 'AAPL', 150, 1000), ('2024-01-02', 'AAPL', 155, 1200), ('2024-01-03', 'AAPL', 152, 900), @@ -80,15 +79,14 @@ INSERT INTO stock_price VALUES ('2024-01-05', 'AAPL', 158, 1100); -- Simple column reference SELECT dt, price, COUNT(*) OVER w as cnt -FROM stock_price +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 -) -ORDER BY dt; +); dt | price | cnt ------------+-------+----- 01-01-2024 | 150 | 0 @@ -100,15 +98,14 @@ ORDER BY dt; -- Multiple column references SELECT dt, price, volume, COUNT(*) OVER w as cnt -FROM stock_price +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 -) -ORDER BY dt; +); dt | price | volume | cnt ------------+-------+--------+----- 01-01-2024 | 150 | 1000 | 0 @@ -120,15 +117,14 @@ ORDER BY dt; -- Expression in DEFINE SELECT dt, price, COUNT(*) OVER w as cnt -FROM stock_price +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 -) -ORDER BY dt; +); dt | price | cnt ------------+-------+----- 01-01-2024 | 150 | 0 @@ -140,15 +136,14 @@ ORDER BY dt; -- Arithmetic and functions SELECT dt, price, volume, COUNT(*) OVER w as cnt -FROM stock_price +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 -) -ORDER BY dt; +); dt | price | volume | cnt ------------+-------+--------+----- 01-01-2024 | 150 | 1000 | 0 @@ -158,7 +153,7 @@ ORDER BY dt; 01-05-2024 | 158 | 1100 | 0 (5 rows) -DROP TABLE stock_price; +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); @@ -170,8 +165,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ B*) DEFINE A AS val > 15 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -189,8 +183,7 @@ WINDOW w AS ( PATTERN (A B C) DEFINE A AS val > 0 -- B and C have no DEFINE entry, so they match every row -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 3 @@ -210,8 +203,7 @@ WINDOW w AS ( X AS val > 10, Y AS val > 20, Z AS val < 20 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -260,8 +252,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (T+) DEFINE T AS flag -) -ORDER BY id; +); id | flag | cnt ----+------+----- 1 | t | 1 @@ -276,8 +267,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (N+) DEFINE N AS NULL::boolean -) -ORDER BY id; +); id | cnt ----+----- 1 | 0 @@ -285,14 +275,14 @@ ORDER BY id; (2 rows) -- Implicit cast to boolean via custom type -CREATE TYPE truthyint AS (v int); -CREATE FUNCTION truthyint_to_bool(truthyint) RETURNS boolean AS $$ +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 (truthyint AS boolean) - WITH FUNCTION truthyint_to_bool(truthyint) +CREATE CAST (rpr_truthyint AS boolean) + WITH FUNCTION rpr_truthyint_to_bool(rpr_truthyint) AS ASSIGNMENT; -CREATE TABLE rpr_coerce (id int, val truthyint); +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, @@ -314,14 +304,14 @@ FROM (SELECT id, val, (4 rows) DROP TABLE rpr_coerce; -DROP CAST (truthyint AS boolean); -DROP FUNCTION truthyint_to_bool(truthyint); -DROP TYPE truthyint; +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 boolish AS boolean; -CREATE TABLE rpr_domain (id int, flag boolish); +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 @@ -330,8 +320,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS flag -) -ORDER BY id; +); id | cnt ----+----- 1 | 1 @@ -340,7 +329,7 @@ ORDER BY id; (3 rows) DROP TABLE rpr_domain; -DROP DOMAIN boolish; +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); @@ -352,8 +341,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (UP+) DEFINE UP AS id > PREV(val) -) -ORDER BY id; +); id | cnt ----+----- 1 | 0 @@ -405,8 +393,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (C+) DEFINE C AS CASE WHEN val1 > 10 THEN val2 > 20 ELSE false END -) -ORDER BY id; +); id | val1 | val2 | cnt ----+------+------+----- 1 | 10 | 20 | 0 @@ -426,8 +413,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS id > 0, B AS id > 5 -- B not in pattern -) -ORDER BY id; +); ERROR: DEFINE variable "b" is not used in PATTERN LINE 7: DEFINE A AS id > 0, B AS id > 5 -- B not in pattern ^ @@ -444,8 +430,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B) DEFINE A AS v < 0, B AS 1 / (v - v) > 0 -) -ORDER BY id; +); id | v | cnt ----+---+----- 1 | 1 | 0 @@ -492,8 +477,8 @@ WINDOW w AS ( (3 rows) DROP TABLE rpr_navcoll; --- A system column in a DEFINE expression. It reaches the expression as a --- scan system attribute rather than an outer Var, and navigation still +-- 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); @@ -545,7 +530,7 @@ ORDER BY id; 6 | 30 | 1 (6 rows) --- ERROR: frame must start at current row when row pattern recognition is used +-- frame must start at CURRENT ROW, not UNBOUNDED PRECEDING SELECT COUNT(*) OVER w FROM rpr_frame WINDOW w AS ( @@ -653,7 +638,7 @@ 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". --- ERROR: frame must start at current row when row pattern recognition is used +-- frame must start at CURRENT ROW, not offset PRECEDING SELECT COUNT(*) OVER w FROM rpr_frame WINDOW w AS ( @@ -666,7 +651,7 @@ 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". --- ERROR: frame must start at current row with RPR +-- frame must start at CURRENT ROW, not offset FOLLOWING SELECT COUNT(*) OVER w FROM rpr_frame WINDOW w AS ( @@ -713,8 +698,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A) DEFINE A AS val > 0 -) -ORDER BY id; +); ERROR: unsupported frame for row pattern recognition LINE 5: ROWS BETWEEN CURRENT ROW AND CURRENT ROW ^ @@ -729,8 +713,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A) DEFINE A AS val > 0 -) -ORDER BY id; +); ERROR: frame ending offset must be positive with row pattern recognition -- A non-constant frame end offset is allowed; a zero value is rejected by the -- same execution-time check the literal 0 above reaches. @@ -743,8 +726,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A) DEFINE A AS val > 0 -) -ORDER BY id; +); EXECUTE rpr_end_offset(2); id | val | cnt ----+-----+----- @@ -768,8 +750,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 6 @@ -789,8 +770,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 6 @@ -814,8 +794,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 6 @@ -835,8 +814,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A B?) DEFINE A AS val >= 0, B AS val >= 0 -) -ORDER BY id; +); ERROR: unsupported frame for row pattern recognition LINE 5: RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING ^ @@ -850,8 +828,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A B?) DEFINE A AS val >= 0, B AS val >= 0 -) -ORDER BY id; +); ERROR: unsupported frame for row pattern recognition LINE 5: GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING ^ @@ -875,8 +852,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A B+) DEFINE A AS val >= 10, B AS val > 15 -) -ORDER BY id; +); id | grp | val | cnt ----+-----+-----+----- 1 | 1 | 10 | 3 @@ -897,8 +873,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A B?) DEFINE A AS val >= 10, B AS val >= 20 -) -ORDER BY id; +); ERROR: unsupported frame for row pattern recognition LINE 6: RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING ^ @@ -920,8 +895,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ | B+ | C+) DEFINE A AS val > 35, B AS val BETWEEN 15 AND 35, C AS val < 15 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 5 | 2 @@ -945,8 +919,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (((A B) C)+) DEFINE A AS val > 10, B AS val > 20, C AS val > 30 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 5 | 0 @@ -975,8 +948,7 @@ WINDOW w AS ( C AS val BETWEEN 25 AND 35, D AS val BETWEEN 35 AND 45, E AS val >= 45 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 5 | 0 @@ -1000,8 +972,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A B) | (C D)) DEFINE A AS val < 20, B AS val >= 20, C AS val < 30, D AS val >= 30 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 5 | 0 @@ -1029,8 +1000,7 @@ WINDOW w AS ( DOWN AS val <= 30, FLAT AS val BETWEEN 25 AND 35, FINISH AS val > 40 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 5 | 0 @@ -1053,8 +1023,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A | B) (C | D)) DEFINE A AS val < 15, B AS val BETWEEN 15 AND 25, C AS val BETWEEN 25 AND 35, D AS val > 35 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 5 | 0 @@ -1086,8 +1055,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 10 @@ -1110,8 +1078,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 50 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -1134,8 +1101,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A?) DEFINE A AS val = 50 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -1159,8 +1125,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{0} B) DEFINE A AS val > 1000, B AS val > 0 -) -ORDER BY id; +); ERROR: quantifier bound must be between 1 and 2147483646 LINE 6: PATTERN (A{0} B) ^ @@ -1172,8 +1137,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{0,0} B) DEFINE A AS val > 1000, B AS val > 0 -) -ORDER BY id; +); ERROR: quantifier bound must be between 1 and 2147483646 LINE 6: PATTERN (A{0,0} B) ^ @@ -1185,8 +1149,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{0,1}) DEFINE A AS val = 50 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -1210,8 +1173,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{3}) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 3 @@ -1235,8 +1197,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}) DEFINE A AS val > 40 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -1260,8 +1221,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 3 @@ -1285,8 +1245,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{3,7}) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 7 @@ -1418,8 +1377,8 @@ WINDOW w AS ( (3 rows) -- {n}? (exactly n): min == max, so the reluctant flag is cleared and the --- plan is indistinguishable from A{2}. This pins the normalization, not --- shortest-match behaviour. +-- 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 ( @@ -1741,8 +1700,8 @@ WINDOW w AS ( 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 +-- 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 ( @@ -1927,8 +1886,8 @@ DETAIL: Pattern has 32768 elements, maximum is 32767. -- ============================================================ 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. +-- 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 @@ -2251,11 +2210,11 @@ WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING 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 preprocess_expression() --- documents them as mandatory, not as optimizations. Each of the three below --- reaches the executor only if those rewrites reach inside a navigation +-- 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); @@ -2380,8 +2339,7 @@ WINDOW w AS ( DEFINE A AS val > 0, B AS val > PREV(val) -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 2 @@ -2401,8 +2359,7 @@ WINDOW w AS ( DEFINE A AS val < NEXT(val), B AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 2 @@ -2423,8 +2380,7 @@ WINDOW w AS ( A AS val > 0, B AS val > PREV(val) AND val < NEXT(val), C AS val > PREV(val) -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -2444,8 +2400,7 @@ WINDOW w AS ( DEFINE A AS val > 0, B AS val > PREV(val) -) -ORDER BY id; +); ERROR: function prev(integer) does not exist LINE 1: SELECT PREV(id), id, val, COUNT(*) OVER w as cnt ^ @@ -2460,8 +2415,7 @@ WINDOW w AS ( DEFINE A AS val > 0, B AS val > PREV(val) -) -ORDER BY id; +); ERROR: function next(integer) does not exist LINE 1: SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt ^ @@ -2476,8 +2430,7 @@ WINDOW w AS ( DEFINE A AS val > 0, B AS val > FIRST(val) -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 5 @@ -2497,8 +2450,7 @@ WINDOW w AS ( DEFINE A AS val > 0, B AS LAST(val) > PREV(val) -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 2 @@ -2518,8 +2470,7 @@ WINDOW w AS ( DEFINE A AS val > 0, B AS val > FIRST(val) AND LAST(val) > PREV(val) -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 2 @@ -2542,52 +2493,53 @@ 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 +-- Name-space: prev/next/first/last are navigation functions, +-- not ordinary functions CREATE SCHEMA rpr_navns; SET search_path TO rpr_navns, public; -CREATE TABLE nt (g text, id int, val int); -INSERT INTO nt VALUES ('x', 1, 100), ('x', 2, 200), ('x', 3, 150), +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 nt; +SELECT prev(val) FROM rpr_nav_rows; ERROR: function prev(integer) does not exist -LINE 1: SELECT prev(val) FROM nt; +LINE 1: SELECT prev(val) FROM rpr_nav_rows; ^ DETAIL: There is no function of that name. -SELECT next(val) FROM nt; +SELECT next(val) FROM rpr_nav_rows; ERROR: function next(integer) does not exist -LINE 1: SELECT next(val) FROM nt; +LINE 1: SELECT next(val) FROM rpr_nav_rows; ^ DETAIL: There is no function of that name. -SELECT prev(val, 2) FROM nt; +SELECT prev(val, 2) FROM rpr_nav_rows; ERROR: function prev(integer, integer) does not exist -LINE 1: SELECT prev(val, 2) FROM nt; +LINE 1: SELECT prev(val, 2) FROM rpr_nav_rows; ^ DETAIL: There is no function of that name. -SELECT next(val, 2) FROM nt; +SELECT next(val, 2) FROM rpr_nav_rows; ERROR: function next(integer, integer) does not exist -LINE 1: SELECT next(val, 2) FROM nt; +LINE 1: SELECT next(val, 2) FROM rpr_nav_rows; ^ DETAIL: There is no function of that name. -SELECT first(val) FROM nt; +SELECT first(val) FROM rpr_nav_rows; ERROR: function first(integer) does not exist -LINE 1: SELECT first(val) FROM nt; +LINE 1: SELECT first(val) FROM rpr_nav_rows; ^ DETAIL: There is no function of that name. -SELECT last(val) FROM nt; +SELECT last(val) FROM rpr_nav_rows; ERROR: function last(integer) does not exist -LINE 1: SELECT last(val) FROM nt; +LINE 1: SELECT last(val) FROM rpr_nav_rows; ^ DETAIL: There is no function of that name. -SELECT first(val, 1) FROM nt; +SELECT first(val, 1) FROM rpr_nav_rows; ERROR: function first(integer, integer) does not exist -LINE 1: SELECT first(val, 1) FROM nt; +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 nt; +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 nt; +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' @@ -2600,7 +2552,7 @@ SELECT next(10); -- Inside DEFINE, unqualified PREV is nav whether or not a user prev() exists SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (START UP+) @@ -2620,7 +2572,7 @@ SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id CREATE FUNCTION prev(integer) RETURNS integer LANGUAGE plpgsql VOLATILE AS 'BEGIN RETURN -999; END'; SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (START UP+) @@ -2636,7 +2588,7 @@ SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id (5 rows) SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A+) @@ -2648,7 +2600,7 @@ ERROR: DEFINE clause cannot contain volatile functions CREATE OR REPLACE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999' LANGUAGE sql VOLATILE; SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A+) @@ -2666,7 +2618,7 @@ SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id -- No OVER references the window, so flattening the subquery drops it -- before the check runs, the same way an unreferenced CTE is never planned SELECT id FROM ( - SELECT id FROM nt + 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 @@ -2680,55 +2632,55 @@ ORDER BY id; 5 (5 rows) --- OFFSET 0 keeps the subquery, but still no OVER references the window, so --- the planner withdraws the DEFINE clause of a window it will not run and the --- check finds nothing left to reject +-- 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 nt + 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: a subquery window that does run keeps its DEFINE, so it is checked +-- 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 nt + 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 --- WHERE false makes the subquery rel dummy, so the planner never plans it --- and nothing looks at its DEFINE -SELECT id FROM ( - SELECT id FROM nt +-- 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) OFFSET 0) sub + PATTERN (A+) DEFINE A AS random() > 0.5)) sub WHERE false; - id ----- + id | c +----+--- (0 rows) --- The volatile is in a dead CASE arm that folds away, so nothing --- volatile is left for the check to find -SELECT id FROM ( - SELECT id FROM nt - WINDOW w AS ( +-- 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)) s + ELSE val > 5 END) ORDER BY id; - id ----- - 1 - 2 - 3 - 4 - 5 + 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 +-- 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) @@ -2739,12 +2691,12 @@ DROP FUNCTION rpr_off_leak(bigint); -- A UNION ALL leaf is flattened like any other subquery, so its -- unreferenced window goes the same way SELECT id FROM ( - SELECT id FROM nt + 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 nt) s; + SELECT id FROM rpr_nav_rows) s; id ---- 1 @@ -2762,7 +2714,7 @@ SELECT id FROM ( -- An unreferenced CTE is never planned, so nothing looks at its -- DEFINE WITH unused AS ( - SELECT id FROM nt + SELECT id FROM rpr_nav_rows WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS random() > 0.5)) @@ -2774,7 +2726,7 @@ SELECT 1; -- ERROR: referencing it plans the CTE, and the check reaches the DEFINE there WITH used AS ( - SELECT id FROM nt + SELECT id FROM rpr_nav_rows WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS random() > 0.5)) @@ -2785,7 +2737,7 @@ DROP FUNCTION prev(integer); CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999' LANGUAGE sql IMMUTABLE; SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (START UP+) @@ -2800,10 +2752,11 @@ SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id 5 | 150 | 0 | (5 rows) --- (val).prev is attribute notation, so it calls the ordinary function prev(val) +-- (val).prev is attribute notation, +-- so it calls the ordinary function prev(val) -- (the IMMUTABLE user prev here), the same as the schema-qualified call below SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A+) @@ -2819,7 +2772,7 @@ SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id (5 rows) SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A+) @@ -2835,7 +2788,7 @@ SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id (5 rows) -- Zero or more than two arguments is an error, with no function fallback -SELECT count(*) OVER w FROM nt +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); @@ -2843,7 +2796,7 @@ ERROR: too few arguments for row pattern navigation function PREV LINE 4: PATTERN (A+) DEFINE A AS PREV() IS NULL); ^ DETAIL: PREV takes a value expression and an optional offset argument. -SELECT count(*) OVER w FROM nt +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); @@ -2854,7 +2807,7 @@ DETAIL: PREV takes a value expression and an optional offset argument. -- the error stands even when a user function of that exact arity exists CREATE FUNCTION prev(integer, integer, integer) RETURNS integer AS 'SELECT -999' LANGUAGE sql IMMUTABLE; -SELECT count(*) OVER w FROM nt +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); @@ -2864,63 +2817,63 @@ LINE 4: PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL); DETAIL: PREV takes a value expression and an optional offset argument. DROP FUNCTION prev(integer, integer, integer); -- Syntactic decoration is rejected -SELECT count(*) OVER w FROM nt +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 nt +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 nt +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 nt +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 nt +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 nt +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 nt +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 nt +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 nt +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); @@ -2929,7 +2882,7 @@ LINE 4: PATTERN (A+) DEFINE A AS PREV(val) IGNORE NULLS IS NULL)... ^ -- Quoting does not escape: "prev" is nav, "PREV" is an ordinary name SELECT id, val, count(*) OVER w AS cnt - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (START UP+) @@ -2944,7 +2897,7 @@ SELECT id, val, count(*) OVER w AS cnt 5 | 150 | 0 (5 rows) -SELECT count(*) OVER w FROM nt +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); @@ -2954,22 +2907,22 @@ LINE 4: PATTERN (A+) DEFINE A AS "PREV"(val) IS NULL); DETAIL: There is no function of that name. -- A view round-trips: bare PREV stays a navigation function, and a qualified -- user prev() stays schema-qualified so it does not reparse as navigation -CREATE VIEW navns_nav AS - SELECT id, count(*) OVER w AS cnt FROM nt +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 navns_fn AS - SELECT id, count(*) OVER w AS cnt FROM nt +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('navns_nav'); +SELECT pg_get_viewdef('rpr_navns_nav'); pg_get_viewdef -------------------------------------------------------------------------------------------- SELECT id, + count(*) OVER w AS cnt + - FROM nt + + 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 + @@ -2979,12 +2932,12 @@ SELECT pg_get_viewdef('navns_nav'); up AS (val > PREV(val))); (1 row) -SELECT pg_get_viewdef('navns_fn'); +SELECT pg_get_viewdef('rpr_navns_fn'); pg_get_viewdef -------------------------------------------------------------------------------------------- SELECT id, + count(*) OVER w AS cnt + - FROM nt + + 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 + @@ -2993,21 +2946,21 @@ SELECT pg_get_viewdef('navns_fn'); a AS (rpr_navns.prev(val) = '-999'::integer)); (1 row) -DROP VIEW navns_nav, navns_fn; +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 navns_fn_last AS - SELECT id, count(*) OVER w AS cnt FROM nt +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('navns_fn_last'); +SELECT pg_get_viewdef('rpr_navns_fn_last'); pg_get_viewdef -------------------------------------------------------------------------------------------- SELECT id, + count(*) OVER w AS cnt + - FROM nt + + 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 + @@ -3016,20 +2969,21 @@ SELECT pg_get_viewdef('navns_fn_last'); a AS (rpr_navns.last(val) = '-999'::integer)); (1 row) -DROP VIEW navns_fn_last; +DROP VIEW rpr_navns_fn_last; DROP FUNCTION rpr_navns.last(integer); --- Attribute notation is field selection only, never a function fallback +-- 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 ct (id int, p rpr_navns_pair); -INSERT INTO ct VALUES (1, (10, 20)), (2, (30, 40)); -SELECT (p).last FROM ct ORDER BY id; +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 ct +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); @@ -3039,7 +2993,7 @@ SELECT count(*) OVER w FROM ct 0 (2 rows) -SELECT count(*) OVER w FROM ct +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); @@ -3048,7 +3002,7 @@ LINE 4: PATTERN (A+) DEFINE A AS (p).prev > 0); ^ -- Navigation offset must not contain a navigation operation SELECT id, val - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A+) @@ -3077,8 +3031,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A B C) DEFINE A AS val > 0, B AS val > 2, C AS val > 4 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 1 | 0 @@ -3101,8 +3054,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN (A B C) DEFINE A AS val > 0, B AS val > 2, C AS val > 4 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 1 | 0 @@ -3124,8 +3076,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B) DEFINE A AS val > 0, B AS val > 1 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 1 | 2 @@ -3197,8 +3148,7 @@ WINDOW w AS ( INITIAL PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 4 @@ -3215,8 +3165,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 4 @@ -3342,8 +3291,8 @@ SELECT pg_get_viewdef('rpr_permute_v'::regclass); (1 row) -- Quoted even where no group follows: the deparser quotes the name wherever --- it appears rather than looking ahead for the "(" that would make it --- ambiguous +-- 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 ( @@ -3393,9 +3342,9 @@ DROP TABLE rpr_permute; -- ============================================================ -- Serialization/Deserialization Tests -- ============================================================ --- RPR-defining views and tables here are intentionally left in place (not --- dropped) so that pg_dump/pg_upgrade exercise the deparse-then-re-parse --- round-trip of the RPR window clause. +-- 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 @@ -3898,7 +3847,7 @@ SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass); b AS (val > 0)); (1 row) --- Quoted identifier round-trip: mixed case and reserved words need quoting +-- Quoted identifier round-trip: mixed-case names need quoting CREATE VIEW rpr_serial_quoted AS SELECT id, val, count(*) OVER w FROM rpr_serial @@ -3949,7 +3898,8 @@ SELECT pg_get_viewdef('rpr_serial_permute'::regclass); b AS (val > 20)); (1 row) --- Inline OVER round-trip: inline window spec (no WINDOW alias) deparses inside OVER (...) +-- 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 @@ -3974,7 +3924,7 @@ SELECT pg_get_viewdef('rpr_serial_inline_over'::regclass); -- Multi-relation view: a DEFINE column is deparsed with no qualifier, so it -- must stay unambiguous across the join for the view to re-parse. This one --- is left in place like the rest of the section, which is what puts an +-- 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); @@ -4166,8 +4116,20 @@ SELECT * FROM rpr_pin_on_v ORDER BY id; 2 | 0 (2 rows) --- 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. +-- 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); @@ -4859,13 +4821,16 @@ 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. +-- 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 ROW(1)::rpr_res_ct $$; + 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 @@ -4889,43 +4854,279 @@ SELECT pg_get_viewdef('rpr_res_sys_v'::regclass, true); 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, +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_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 + 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 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; + 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_res_sys_v; -ERROR: cannot cast type record to rpr_res_ct -LINE 1: SELECT ROW(1)::rpr_res_ct - ^ -DETAIL: Input has too few columns. -QUERY: SELECT ROW(1)::rpr_res_ct -CONTEXT: SQL function "rpr_res_fct" statement 1 -SELECT * FROM rpr_res_sys_rt; -ERROR: cannot cast type record to rpr_res_ct -LINE 1: SELECT ROW(1)::rpr_res_ct - ^ -DETAIL: Input has too few columns. -QUERY: SELECT ROW(1)::rpr_res_ct -CONTEXT: SQL function "rpr_res_fct" statement 1 -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; +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 @@ -5767,11 +5968,11 @@ SELECT * FROM rpr_res_fa_rt; 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 anonymous 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 would have moved to is --- kept clear as well. +-- 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); @@ -6330,8 +6531,8 @@ SELECT * FROM rpr_cds_null_rt ORDER BY idp1; 4 | 0 (3 rows) --- and the same nesting where the join above nulls nothing, which the exact --- match takes +-- 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 @@ -6601,17 +6802,6 @@ SELECT * FROM rpr_pvar_rt; DROP VIEW rpr_pvar_rt, rpr_pvar_v; DROP TABLE rpr_pvar_a, rpr_pvar_b; --- The same query written fresh is rejected, since nothing pins the name for --- it. Pinning is what lets the stored definition above 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); - ^ -- Materialized view (if supported) CREATE TABLE rpr_mview (id INT, val INT); INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30); @@ -6703,7 +6893,7 @@ SELECT * FROM rpr_insert_target ORDER BY id; DROP TABLE rpr_ctas_result; DROP TABLE rpr_insert_target; DROP TABLE rpr_ctas; --- Prepared statements (tests outfuncs.c / readfuncs.c) +-- 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 @@ -6715,8 +6905,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); EXECUTE rpr_prep_simple; id | val | cnt ----+-----+----- @@ -6744,8 +6933,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 10 -) -ORDER BY id; +); EXECUTE rpr_prep_param(2); id | val | cnt ----+-----+----- @@ -6775,8 +6963,7 @@ WINDOW w AS ( A AS val > 5, B AS val > 15, C AS val <= 15 -) -ORDER BY id; +); EXECUTE rpr_prep_complex; id | val | cnt ----+-----+----- @@ -6818,7 +7005,7 @@ SELECT * FROM rpr_cte ORDER BY id; 4 | 40 | 0 (4 rows) --- CTE with multiple references (forces node copy) +-- 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 @@ -6853,8 +7040,7 @@ FROM ( DEFINE A AS val > 10, B AS val > 20 ) ) sub -WHERE cnt > 0 -ORDER BY id; +WHERE cnt > 0; id | val | cnt ----+-----+----- 2 | 20 | 2 @@ -6876,8 +7062,7 @@ FROM ( ) ) inner_sub WHERE cnt > 0 -) outer_sub -ORDER BY id; +) outer_sub; id | val | cnt ----+-----+----- 1 | 10 | 4 @@ -7139,8 +7324,9 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_op'), E'\n')) AS (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. +-- 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), @@ -7222,7 +7408,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_struct'), E'\n')) DROP VIEW rpr_dp_struct; -- Execution semantics (deparse cannot show reluctant shortest-match). The --- rpr_glue rows -- an A-run followed by B rows -- show when the '|B' +-- 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 "+" @@ -7235,8 +7421,7 @@ FROM rpr_glue WINDOW gs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0), rs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0), gp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0), - rp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0) -ORDER BY id; + 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 @@ -7264,7 +7449,8 @@ SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRE 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 +-- 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... @@ -7455,7 +7641,8 @@ 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) +-- 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 ( @@ -7467,7 +7654,8 @@ WINDOW w AS ( 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 +-- PATTERN-only variable qualified name: +-- not supported even without DEFINE entry SELECT COUNT(*) OVER w FROM rpr_err WINDOW w AS ( @@ -7491,7 +7679,8 @@ WINDOW w AS ( 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) +-- 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 ( @@ -7503,8 +7692,9 @@ WINDOW w AS ( ERROR: range variable qualified expression "rpr_err.val" is not allowed in DEFINE clause LINE 7: DEFINE A AS rpr_err.val > 0 ^ --- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check --- must fall through so that normal column resolution produces a sensible error. +-- 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 ( @@ -7551,7 +7741,8 @@ WINDOW w AS ( 3 | 25 | 1 (3 rows) --- Composite type field selection (qualified forms): the ColumnRef portion ("A.items" or +-- 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 @@ -7642,9 +7833,10 @@ WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING DROP TABLE rpr_ordrow; -- The same split by way of a pulled-up composite target, both as a plain --- subquery and as a view. +-- 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), (2, 2), (3, 3); +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 @@ -7652,10 +7844,11 @@ WINDOW w AS (PARTITION BY k ORDER BY b PATTERN (p q+) DEFINE q AS k IS NOT NULL); count ------- + 3 0 0 0 -(3 rows) +(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; @@ -7665,10 +7858,11 @@ WINDOW w AS (PARTITION BY k ORDER BY b PATTERN (p q+) DEFINE q AS k IS NOT NULL); count ------- + 3 0 0 0 -(3 rows) +(4 rows) -- Control: PATTERN/DEFINE aside, the same window clause runs fine. SELECT count(*) OVER w @@ -7677,10 +7871,11 @@ WINDOW w AS (PARTITION BY k ORDER BY b ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING); count ------- + 3 + 2 1 1 - 1 -(3 rows) +(4 rows) DROP VIEW rpr_partrow_v; DROP TYPE rpr_partrow_t; @@ -7769,8 +7964,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0, B AS val > 5, C AS val > 10 -) -ORDER BY id; +); ERROR: DEFINE variable "b" is not used in PATTERN LINE 7: DEFINE A AS val > 0, B AS val > 5, C AS val > 10 ^ @@ -7786,8 +7980,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 15 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -7803,8 +7996,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (N+) DEFINE N AS val IS NULL -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -7820,8 +8012,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (NN+) DEFINE NN AS val IS NOT NULL -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 1 @@ -7958,7 +8149,8 @@ ERROR: row pattern navigation offset cannot contain a row pattern navigation op 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 +-- 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 @@ -8102,7 +8294,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -> Seq Scan on rpr_plan (6 rows) --- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged +-- 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 @@ -8148,7 +8341,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -> Seq Scan on rpr_plan (6 rows) --- Consecutive GROUP merge at the boundary: (A B){1073741823,} (A B){1073741823,} +-- 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). @@ -8361,7 +8555,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING (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 +-- 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 @@ -8534,8 +8729,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -> Seq Scan on rpr_plan (6 rows) --- (A+){2,4} -> a{2,} (outer range, unbounded child: every interval reaches INF, --- so they always touch) +-- (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 @@ -8550,7 +8745,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -> Seq Scan on rpr_plan (6 rows) --- (A{2,3}){2,4} stays nested for the same reason, even though the counts +-- (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 @@ -8567,7 +8762,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING (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) +-- 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 @@ -8583,8 +8779,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING (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) +-- 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 @@ -8661,7 +8857,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -> Seq Scan on rpr_plan (6 rows) --- Consecutive GROUP merge with unbounded: (A+) (A+) -> a{2,} +-- 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 @@ -8676,7 +8872,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -> Seq Scan on rpr_plan (6 rows) --- Consecutive GROUP merge finite: (A{10}){20} -> a{200} +-- 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 @@ -8754,7 +8950,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -> Seq Scan on rpr_plan (6 rows) --- Multiple SUFFIX absorption with skipUntil: (A B)+ A B A B C +-- 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 @@ -8821,7 +9017,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -> Seq Scan on rpr_plan (6 rows) --- PREFIX merge with multiple quantifiers: A+ B* C? (A+ B* C?)+ -> (a+ b* c?){2,} +-- 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 @@ -8908,8 +9105,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -> Seq Scan on rpr_plan (6 rows) --- Reluctant optimization bypass: GROUP merge --- (A B)+? (A B) stays separate (greedy merges to (a b){2,}) +-- 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 @@ -9067,7 +9264,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING (6 rows) -- Reluctant preserved through ALT flatten --- (A | (B | C))+? flattens to (a | b | c)+? - inner ALT flattened, reluctant kept +-- (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 @@ -9176,7 +9374,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING (6 rows) -- Unwrap single-item ALT after dedup: (A | A)+ -> a+ --- ALT dedup reduces to single-item, then GROUP unwrap +-- 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 @@ -9370,7 +9568,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -> Seq Scan on rpr_plan (6 rows) --- ALT inside unbounded GROUP: (A+ B | A B)* -> (a+# b | a b)* (first iteration absorbable) +-- 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 @@ -9482,7 +9681,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -> Seq Scan on rpr_plan (6 rows) --- Non-absorbable (no unbounded branch): (A | B){2,} -> (a | b){2,} (no markers) +-- 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 @@ -9594,8 +9794,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B) DEFINE A AS val <= 50, B AS val > 50 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 6 @@ -9620,8 +9819,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B)+ C) DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -9646,8 +9844,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN (A B+) DEFINE A AS val <= 50, B AS val > 50 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -9672,8 +9869,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ | B+) C) DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -9689,7 +9885,7 @@ ORDER BY id; (10 rows) -- ALT with Mixed Branches --- Pattern: (A+ | B C) - only first branch absorbable +-- Pattern: (A+ | B C)+ - only first branch absorbable SELECT id, val, COUNT(*) OVER w as cnt FROM rpr_plan WINDOW w AS ( @@ -9698,8 +9894,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ | B C)+) DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 3 @@ -9724,8 +9919,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN ((A | B){2,}) DEFINE A AS val <= 50, B AS val > 50 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 10 @@ -9740,8 +9934,8 @@ ORDER BY id; 10 | 100 | 0 (10 rows) --- Non-Absorbable: Nested Unbounded --- Pattern: ((A B)+ C)+ - nested GROUP structure +-- 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 ( @@ -9750,8 +9944,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN (((A B)+ C)+) DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -9776,8 +9969,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B+){2,}) DEFINE A AS val <= 50, B AS val > 50 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -9802,8 +9994,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A+ B) DEFINE A AS val <= 50, B AS val > 50 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 6 @@ -9828,8 +10019,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B) DEFINE A AS val <= 50, B AS val > 50 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 6 @@ -9881,10 +10071,12 @@ WINDOW w AS ( 6 | {A} | | (6 rows) --- Measuring a group body for absorbability. The optimizer only measures a --- body an unbounded quantifier wraps, so each pattern below puts the shape --- under test inside one. None of the three can match real rows; the point is --- that the measurement reports "not a fixed length" instead of overflowing. +-- 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 @@ -9959,7 +10151,8 @@ WINDOW w AS ( -- 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 +-- 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']), @@ -10013,8 +10206,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*) DEFINE A AS val > 1000 -- Never matches -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -10038,8 +10230,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val >= 0 -- Always true -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 10 @@ -10063,8 +10254,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{100}) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -10087,8 +10277,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{10,20}) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 10 @@ -10113,8 +10302,7 @@ WINDOW w AS ( PATTERN ((((A B) | C)+ D)+) DEFINE A AS val <= 20, B AS val > 20 AND val <= 40, C AS val > 40 AND val <= 60, D AS val > 60 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 0 @@ -10139,8 +10327,7 @@ WINDOW w AS ( PATTERN (A | B | C | D | E) DEFINE A AS val = 10, B AS val = 30, C AS val = 50, D AS val = 70, E AS val = 90 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 1 @@ -10166,8 +10353,7 @@ WINDOW w AS ( DEFINE A AS val >= 10, B AS val >= 20, C AS val >= 30, D AS val >= 40, E AS val >= 50, F AS val >= 60, G AS val >= 70, H AS val >= 80 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 8 @@ -10192,8 +10378,7 @@ WINDOW w AS ( PATTERN (A{2} B+ C{3,5} D* E{1,}) DEFINE A AS val > 0, B AS val > 0, C AS val > 0, D AS val > 0, E AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 10 @@ -10252,7 +10437,8 @@ WINDOW w AS ( -> Seq Scan on rpr_fallback (6 rows) --- Max quantifier exceeds valid range (2147483647 = INT_MAX, limit is 2147483646) +-- Max quantifier exceeds valid range +-- (2147483647 = INT_MAX, limit is 2147483646) EXPLAIN (COSTS OFF) SELECT COUNT(*) OVER w FROM rpr_fallback WINDOW w AS ( @@ -10617,8 +10803,7 @@ w2 AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (B+) DEFINE B AS val >= 40 -) -ORDER BY id; +); id | category | val | cnt1 | cnt2 ----+----------+-----+------+------ 1 | A | 10 | 9 | 0 @@ -10642,8 +10827,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY category, id; +); id | category | val | cnt ----+----------+-----+----- 1 | A | 10 | 3 @@ -10666,8 +10850,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY category DESC, val ASC; +); id | category | val | cnt ----+----------+-----+----- 7 | C | 70 | 9 @@ -10690,8 +10873,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); id | category | val | cnt ----+----------+-----+----- 1 | A | 10 | 9 @@ -10713,8 +10895,7 @@ SELECT id, category, val, PATTERN (A+) DEFINE A AS val > 0 ) as cnt -FROM rpr_planner -ORDER BY id; +FROM rpr_planner; id | category | val | cnt ----+----------+-----+----- 1 | A | 10 | 9 @@ -10744,8 +10925,7 @@ SELECT * FROM ( DEFINE A AS val > 0 ) ) sub -WHERE cnt > 5 -ORDER BY id; +WHERE cnt > 5; id | category | val | cnt ----+----------+-----+----- 1 | A | 10 | 9 @@ -10761,8 +10941,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 50 -) -ORDER BY id; +); id | category | val | cnt ----+----------+-----+----- 6 | B | 60 | 4 @@ -10862,8 +11041,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val1 + val2 > 100 -) -ORDER BY t1.id; +); id | val1 | val2 | cnt ----+------+------+----- 1 | 10 | 100 | 5 @@ -10883,8 +11061,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val1 > 0 -) -ORDER BY t1.id; +); id | val1 | val2 | cnt ----+------+------+----- 1 | 10 | 100 | 5 @@ -10905,8 +11082,7 @@ WINDOW w AS ( PATTERN (A+ B) DEFINE A AS val1 > 20, B AS val2 > 200 -) -ORDER BY t1.id; +); id | val1 | val2 | cnt ----+------+------+----- 1 | 10 | 100 | 0 @@ -10927,8 +11103,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val1 + val2 > 0 -) -ORDER BY t1.id, t2.id; +); id1 | id2 | val1 | val2 | cnt -----+-----+------+------+----- 1 | 1 | 10 | 100 | 4 @@ -10948,8 +11123,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (X+) DEFINE X AS val1 < val1_next -) -ORDER BY id; +); id | val1 | val1_next | cnt ----+------+-----------+----- 1 | 10 | 20 | 4 @@ -11134,8 +11308,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); id | doubled | added | cnt ----+---------+-------+----- 1 | 20 | 20 | 5 @@ -11159,8 +11332,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | category | cnt ----+-----+----------+----- 1 | 10 | low | 5 @@ -11180,8 +11352,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | max_val | cnt ----+-----+---------+----- 1 | 10 | 50 | 5 @@ -11202,8 +11373,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | coalesced | distance | cnt ----+-----+-----------+----------+----- 1 | 10 | 10 | 20 | 5 @@ -11223,8 +11393,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY row_id; +); row_id | value | cnt --------+-------+----- 1 | 10 | 5 @@ -11370,8 +11539,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS COUNT(*) > 0 -) -ORDER BY category; +); ERROR: aggregate functions are not allowed in DEFINE LINE 11: DEFINE A AS COUNT(*) > 0 ^ @@ -11387,8 +11555,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS COUNT(*) > 0 -) -ORDER BY category; +); ERROR: aggregate functions are not allowed in DEFINE LINE 11: DEFINE A AS COUNT(*) > 0 ^ @@ -11740,7 +11907,7 @@ SELECT * FROM rpr_grp_v ORDER BY category; 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 unknown, so the total row +-- 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 @@ -11928,10 +12095,10 @@ SELECT * FROM rpr_grp_v2 ORDER BY category NULLS LAST; (3 rows) DROP VIEW rpr_grp_v2; --- A DEFINE clause may spell a GROUP BY expression. Planting stops at one --- rather than offering the columns underneath it to the grouping logic on --- their own, which is not how grouping makes them available; the target list --- entry holding the same expression is what both copies end up naming. +-- 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 @@ -11947,6 +12114,35 @@ ORDER BY bumped; 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 @@ -12012,10 +12208,11 @@ WINDOW w AS ( (3 rows) -- A DEFINE clause may repeat an expression the window itself orders by, with --- no grouping in sight. Planting bare Vars 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 already in --- the input. +-- 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 ( @@ -12085,24 +12282,22 @@ WINDOW w1 AS (ORDER BY category), | 3 | 0 (3 rows) --- An unreferenced window is substituted like any other +-- 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 category IS NOT NULL); - category ----------- - - B - A -(3 rows) - --- A join turns the DEFINE clause's Vars into join alias Vars. Plain --- grouping still resolves them, so the column USING merges reaches the --- pattern unharmed. + 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 @@ -12175,6 +12370,87 @@ WINDOW w AS ( 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 @@ -12292,8 +12568,7 @@ w3 AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (C+) DEFINE C AS val > 100 -) -ORDER BY id; +); id | val | cnt1 | cnt2 | cnt3 ----+-----+------+------+------ 1 | 10 | 20 | 0 | 0 @@ -12334,8 +12609,7 @@ SELECT * FROM ( ) sub1 ) sub2 ) sub3 -WHERE cnt > 10 -ORDER BY id; +WHERE cnt > 10; id | val | cnt ----+-----+----- 1 | 10 | 20 @@ -12351,8 +12625,7 @@ WINDOW w AS ( PATTERN (A+ B) DEFINE A AS (val % 3 = 0 OR val % 5 = 0), B AS (val * 2 > 100 AND val / 2 < 100) -) -ORDER BY id; +); id | val | cnt ----+-----+----- 1 | 10 | 19 @@ -12387,8 +12660,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- (0 rows) @@ -12403,8 +12675,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); id | val | cnt ----+-----+----- 10 | 100 | 1 @@ -12414,7 +12685,7 @@ DROP TABLE rpr_stress; -- ============================================================ -- Error Limit Tests -- ============================================================ --- Tests for error conditions in rpr.c +-- 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) @@ -12429,102 +12700,27 @@ WINDOW w AS ( ERROR: DEFINE variable "b" is not used in PATTERN LINE 7: B AS TRUE ^ --- 240 variables in PATTERN and DEFINE (boundary - should succeed) -SELECT COUNT(*) OVER w FROM rpr_errors -WINDOW w AS ( - ORDER BY id - ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 - V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 - V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 - V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 - V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 - V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 - V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 - V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 - V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 - V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 - V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 - V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240) - DEFINE - V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0, - V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0, - V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0, - V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0, - V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0, - V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0, - V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0, - V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0, - V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0, - V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0, - V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0, - V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0, - V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0, - V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0, - V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0, - V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0, - V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0, - V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0, - V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0, - V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0, - V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0, - V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0, - V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0, - V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0 -); +-- 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: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE) -SELECT COUNT(*) OVER w FROM rpr_errors -WINDOW w AS ( - ORDER BY id - ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 - V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 - V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 - V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 - V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 - V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 - V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 - V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 - V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 - V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 - V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 - V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 - V241) - DEFINE - V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0, - V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0, - V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0, - V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0, - V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0, - V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0, - V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0, - V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0, - V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0, - V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0, - V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0, - V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0, - V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0, - V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0, - V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0, - V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0, - V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0, - V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0, - V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0, - V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0, - V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0, - V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0, - V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0, - V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0 -); ERROR: too many row pattern variables -LINE 17: V241) - ^ +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 diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out index ecfe109547a..e77fcef25bb 100644 --- a/src/test/regress/expected/rpr_explain.out +++ b/src/test/regress/expected/rpr_explain.out @@ -38,7 +38,8 @@ -- Large Scale Statistics Verification -- Nav Mark Lookback/Lookahead (tuplestore trim) -- ============================================================ --- Filter function to normalize platform-dependent memory values (not NFA statistics). +-- 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. @@ -265,7 +266,7 @@ WINDOW w AS ( (9 rows) -- Sequential alternations at the same depth --- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)" +-- 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) @@ -831,7 +832,7 @@ WINDOW w AS ( (9 rows) -- Early termination: first ALT branch (A) reaches FIN immediately, --- pruning second branch (A B+) before it can accumulate B repetitions. +-- 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) @@ -1032,7 +1033,8 @@ WINDOW w AS ( (9 rows) -- Bare unbounded quantifier: A+ absorbs redundant contexts --- min=1 commits no match until the run ends, so newer contexts absorb in-progress +-- 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) @@ -1072,7 +1074,8 @@ WINDOW w AS ( (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 +-- 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) @@ -1454,8 +1457,8 @@ WINDOW w AS ( -- Absorption preserved when DEFINE uses only LAST without offset -- LAST(v) is match_start-independent (always currentpos), so absorption --- remains active. Compare: absorbed count should be >0, like the --- PREV-only case above. +-- 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) @@ -1744,8 +1747,8 @@ WINDOW w AS ( (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) +-- 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']), @@ -2325,7 +2328,8 @@ WINDOW w AS ( (1 row) -- JSON format with skipped context statistics --- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped +-- 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) @@ -2878,7 +2882,8 @@ WINDOW w AS ( -> Function Scan on generate_series s (actual rows=3.00 loops=1) (8 rows) --- (A?){2,3}: min=2 (ISO/IEC 19075-5 7.2.8 STR06 = STRE STRE) -> 3 length-0 matches +-- (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) @@ -4250,7 +4255,8 @@ WINDOW w AS ( (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 +-- 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) @@ -5007,8 +5013,10 @@ WINDOW w AS ( -> 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 +-- 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) @@ -5048,7 +5056,7 @@ WINDOW w AS ( (9 rows) -- Three-deep stack whose innermost branch is a quantified group: the group's --- skip-target jump must not be mistaken for a branch separator at any depth +-- 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 @@ -5212,7 +5220,8 @@ WINDOW w AS ( -- 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 +-- 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) @@ -6032,9 +6041,11 @@ WINDOW w AS ( -- ============================================================ -- Nav Mark Lookback/Lookahead Tests --- Verifies planner-computed navigation offsets for tuplestore trim. --- Lookback: how far back from currentpos (PREV, LAST, compound PREV_LAST/NEXT_LAST). --- Lookahead: how far forward from match_start (FIRST, compound PREV_FIRST/NEXT_FIRST). +-- 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 @@ -6331,7 +6342,8 @@ WINDOW w AS ( -> Function Scan on generate_series s (5 rows) --- Compound PREV(FIRST(val, 1), 2): lookback from match_start, firstOffset = 1-2 = -1 +-- 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 ( @@ -6663,8 +6675,8 @@ WINDOW w AS ( (4 rows) -- Compound PREV(LAST(val, $1), $2): parameter lookback overflow -> retain all --- EXPLAIN shows "runtime" (plan-level); EXPLAIN ANALYZE shows "retain all" --- (executor-resolved). +-- 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) @@ -6756,7 +6768,8 @@ RESET plan_cache_mode; DEALLOCATE p_first_runtime; -- PREV(v) + PREV(v, $1): the implicit lookback of 1 has to count even when the -- explicit offset resolves to 0, or PREV(v) would fail with "cannot fetch row --- before mark". A generic plan settles the reach per scan instead of at init. +-- 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 @@ -6967,8 +6980,9 @@ 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 +-- 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); diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out index 2875afd5897..e71696e453a 100644 --- a/src/test/regress/expected/rpr_integration.out +++ b/src/test/regress/expected/rpr_integration.out @@ -15,7 +15,7 @@ -- 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) +-- 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 @@ -59,7 +59,8 @@ INSERT INTO rpr_integ VALUES -- 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. +-- 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 @@ -162,8 +163,7 @@ SELECT * FROM ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B+) DEFINE B AS val > PREV(val)) -) t WHERE cnt > 0 -ORDER BY id; +) t WHERE cnt > 0; id | val | cnt ----+-----+----- 1 | 10 | 2 @@ -178,11 +178,11 @@ ORDER BY id; -- Verify that PostgreSQL does not merge an RPR window with a non-RPR -- window even when both share the same ORDER BY and frame -- specification. RPR pattern matching produces results that are --- semantically different from a plain frame-based aggregate, so the --- two windows must remain as separate WindowAgg nodes. Inline window --- specs are used throughout this section because only inline windows --- are subject to the dedup path; distinct named windows are always --- kept separate regardless of equivalence. +-- 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. @@ -236,8 +236,7 @@ SELECT DEFINE B AS val > PREV(val)) AS rpr_cnt, count(*) OVER (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS normal_cnt -FROM rpr_integ -ORDER BY id; +FROM rpr_integ; id | val | rpr_cnt | normal_cnt ----+-----+---------+------------ 1 | 10 | 2 | 10 @@ -276,8 +275,8 @@ WINDOW w1 AS ( (4 rows) -- The two windows above start from the same frame. These two do not: --- they converge only after frame optimization rewrites the non-RPR one, --- and the RPR window is preserved, so they still must not be merged. +-- 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. @@ -309,14 +308,17 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed; (9 rows) -- ============================================================ --- A4. Window dedup prevention (same PATTERN, different DEFINE) +-- 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 have different DEFINE --- conditions. Even though the ORDER BY, frame, and PATTERN coincide, --- the differing DEFINE expressions classify rows differently and --- must therefore yield two separate WindowAgg nodes. Inline specs --- are used here because dedup only applies to inline windows. +-- 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 @@ -383,8 +385,7 @@ SELECT ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B+) DEFINE B AS val < PREV(val)) AS cnt_down -FROM rpr_integ -ORDER BY id; +FROM rpr_integ; id | val | cnt_up | cnt_down ----+-----+--------+---------- 1 | 10 | 2 | 0 @@ -399,6 +400,67 @@ ORDER BY id; 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 -- ============================================================ @@ -501,8 +563,11 @@ SELECT count(*), sum(c) FROM ( (1 row) -- "val" is a non-resjunk subquery output that the outer query never reads, so --- remove_unused_subquery_outputs() would replace it with NULL and DEFINE would --- then compare NULLs. The guard in allpaths.c keeps it. +-- 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 @@ -589,9 +654,9 @@ WINDOW w AS (ORDER BY id 10 | 0 (10 rows) --- The same retention has to survive join removal: nulling "uv" would leave --- rpr_integ_u referenced by nothing, the LEFT JOIN would be dropped, and the --- DEFINE Var would then point at a relation no longer in the plan. +-- 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) @@ -642,8 +707,9 @@ SELECT id, c FROM ( (10 rows) -- A flattened subquery output that an outer join makes nullable reaches the --- DEFINE clause as a PlaceHolderVar rather than a Var. The parser's targetlist --- entry is rewritten the same way, so the expression still 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 @@ -703,10 +769,10 @@ WINDOW w AS (ORDER BY t.id (10 rows) -- The same shape with the window dead: nothing reads count(*) OVER w, so its --- entry goes, w goes with it, and "uv" is no longer held by a DEFINE clause --- that will run. That was rpr_integ_u's last reference, so join removal takes --- the LEFT JOIN too and the scan is left alone. Retaining "uv" here on the --- strength of a window that will not run would keep the join alive for nothing. +-- 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 @@ -799,9 +865,11 @@ SELECT id, c1 FROM ( (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. Its DEFINE must not keep "val" alive --- for a window that never runs: the subquery output for val becomes a null Const. +-- 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 @@ -826,12 +894,12 @@ SELECT c FROM ( Output: rpr_integ.id (10 rows) --- Here w2 does have a window function, but the outer query does not read it, so --- this call replaces that entry with a null Const and w2 goes inactive as well. --- Which windows are active therefore has to be read after that substitution: --- read before it, w2 still looks active and "val" is retained for a window that --- will not run. Both null Consts on the WindowAgg's Output line are the --- assertion. +-- 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 @@ -857,11 +925,9 @@ SELECT c FROM ( (10 rows) -- The same shape with the window function one level down, inside an --- expression. The live set is read off the entries that survive, so a --- window function nested in one of them is seen and one in an entry about to --- be replaced is not; reading it from the entries' top-level nodes instead --- would report w2 live here and hold "val" for a window that goes inactive --- anyway. This plan matching the one above is the assertion. +-- 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 @@ -913,7 +979,8 @@ 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 w2 keeps its own. +-- 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 @@ -945,9 +1012,9 @@ SELECT c2 FROM ( -- 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 its DEFINE column is retained. The pass that settles the window --- function entries therefore has to apply every condition the loop after it --- applies, not just the one about the upper query. +-- 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 @@ -998,10 +1065,10 @@ HINT: A DEFINE condition may reference individual columns only. -- 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. +-- 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 @@ -1039,40 +1106,58 @@ SELECT sum(c) FROM ( 10 (1 row) --- The walk that decides which windows are still live runs on a targetlist --- subquery_planner() has not preprocessed yet, so a SubLink is still a SubLink --- there. OFFSET 0 keeps the subquery unflattened, which is what puts --- remove_unused_subquery_outputs() on the path at all. +-- 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, (SELECT 1) AS s, count(*) OVER w AS c + 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; - count -------- - 10 -(1 row) +) 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) --- 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 walk reads it --- there; a window function written inside the sub-select itself would count --- against that query's own window clauses and must not be read here. SELECT count(*) FROM ( - SELECT id, (count(*) OVER w) IN (SELECT 1) AS m + 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; +) t WHERE m; count ------- - 10 + 2 (1 row) -- ============================================================ @@ -1167,7 +1252,7 @@ DROP FUNCTION rpr_logging_minvfunc(text, anyelement); -- ============================================================ -- cost_windowagg() must account for DEFINE expression evaluation cost. -- Verify RPR WindowAgg cost > non-RPR WindowAgg cost. -CREATE FUNCTION get_windowagg_cost(query text) RETURNS numeric AS $$ +CREATE FUNCTION rpr_get_windowagg_cost(query text) RETURNS numeric AS $$ DECLARE plan json; cost numeric; @@ -1177,12 +1262,12 @@ BEGIN RETURN cost; END; $$ LANGUAGE plpgsql; -SELECT get_windowagg_cost( +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))') > - get_windowagg_cost( + 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; @@ -1191,7 +1276,7 @@ SELECT get_windowagg_cost( t (1 row) -DROP FUNCTION get_windowagg_cost(text); +DROP FUNCTION rpr_get_windowagg_cost(text); -- ============================================================ -- A8. Subquery flattening prevention -- ============================================================ @@ -1228,13 +1313,12 @@ WHERE cnt > 0; -- ============================================================ -- Verify that DEFINE expressions are not propagated into the -- targetlist of any upper WindowAgg node. Only the column references --- consumed by DEFINE should be passed up; the full DEFINE expression --- is meaningful only inside the RPR WindowAgg that owns it. --- EXPLAIN VERBOSE is therefore expected to show a clean targetlist on --- the outer WindowAgg, with no DEFINE-derived expression leaking in. --- Note: columns referenced by DEFINE (e.g., "val") may appear as --- resjunk entries in upper WindowAgg targetlists -- but that is harmless. --- The claim here is limited to the full DEFINE boolean expression. +-- 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, @@ -1749,8 +1833,8 @@ EXECUTE rpr_prev(1); 10 | 45 | 0 (10 rows) --- Negative runtime nav offset under the generic plan: init clamps it to 0 for --- trim sizing, but the per-row navigation rejects the negative offset. +-- 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; @@ -1912,14 +1996,44 @@ ORDER BY o.id, r.id; -- A lateral outer reference can share varno and varattno with a DEFINE-only -- column: here o.b and y are both attribute 2 at their own query levels. --- Only varlevelsup separates them, so the window input target has to take y --- even though a Var with the same varno and varattno is present. +-- 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. -SELECT * +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 @@ -1929,15 +2043,43 @@ LATERAL ( PATTERN (A+) DEFINE A AS y > 0) ) s; - a | b | lat | c ----+----+-----+--- - 1 | 10 | 10 | 2 - 1 | 10 | 10 | 0 + a | c +---+--- + 1 | 2 + 1 | 0 (2 rows) -- Control: the outer reference is o.a, attribute 1, which cannot be mistaken --- for y. The counts must match the query above. -SELECT * +-- 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 @@ -1947,10 +2089,10 @@ LATERAL ( PATTERN (A+) DEFINE A AS y > 0) ) s; - a | b | lat | c ----+----+-----+--- - 1 | 10 | 1 | 2 - 1 | 10 | 1 | 0 + a | c +---+--- + 1 | 2 + 1 | 0 (2 rows) DROP TABLE rpr_lat_o, rpr_lat_i; @@ -2051,8 +2193,8 @@ DROP INDEX rpr_integ_id_idx; -- B9. RPR + Volatile function in DEFINE -- ============================================================ -- Volatile functions in DEFINE are rejected in the planner. Under --- RPR's NFA engine the same row's DEFINE predicate may be evaluated --- multiple times (backtracking, PREV/NEXT navigation), so a volatile +-- 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. @@ -2170,7 +2312,8 @@ CREATE TABLE rpr_over1 (a int); CREATE TABLE rpr_over2 (c int); INSERT INTO rpr_over1 VALUES (1),(2),(3); INSERT INTO rpr_over2 VALUES (1),(2),(3); --- Plan: only the DEFINE column survives in the subquery output. +-- 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 @@ -2198,13 +2341,13 @@ SELECT cnt FROM ( (15 rows) DROP TABLE rpr_over1, rpr_over2; --- A DEFINE clause can hold a Var, or a PlaceHolderVar, of an outer query --- level by the time this pruning runs, 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. Reading the clause has to pass those by. --- Each query below prunes an output, and is followed by the same query --- reading that output, which prunes nothing and so never meets them. +-- 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); @@ -2232,8 +2375,9 @@ GROUP BY d.k ORDER BY 1; 4 | 2 | 106 (2 rows) --- the same, in a clause that does read it: the column has to be held for the --- window even though nothing above the subquery reads it. +-- 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 @@ -2297,10 +2441,11 @@ 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. +-- (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 $$ @@ -2331,7 +2476,7 @@ GROUP BY g.n ORDER BY g.n; -> Seq Scan on rpr_srf (13 rows) --- Each outer row yields its own offset (9, 8, 7), not one frozen value. +-- 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 @@ -2385,9 +2530,9 @@ GROUP BY g.n ORDER BY g.n; (3 rows) DROP FUNCTION rpr_srf_first(int); --- A compound navigation's OUTER offset must be re-resolved per scan --- as well. The last offset overflows int64, so that scan's navigation --- has no target row at all. +-- 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 @@ -2445,10 +2590,11 @@ DROP TABLE rpr_hcache_thr, rpr_hcache_stock; -- ============================================================ -- B14. RPR + Multiple window definitions -- ============================================================ --- A DEFINE-only column and a later window's sort key both become junk --- targetlist entries. Each draws its resno from p_next_resno, which is what --- keeps the two distinct: a targetlist that gives one resno to two entries is --- not a valid Query, and the parser is the only place that can prevent it. +-- 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 @@ -2478,8 +2624,7 @@ ORDER BY id; 2 | 0 | 2 (2 rows) --- Control: the opposite declaration order draws the sort key first, so the two --- never compete for a resno. It must return the same rows as the first query +-- 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) diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out index bcb7cecd862..f36ee926b6d 100644 --- a/src/test/regress/expected/rpr_nfa.out +++ b/src/test/regress/expected/rpr_nfa.out @@ -195,7 +195,12 @@ WINDOW w AS ( -- ============================================================ -- 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']), @@ -212,7 +217,7 @@ FROM test_absorbable WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS 'A' = ANY(flags) @@ -220,13 +225,15 @@ WINDOW w AS ( id | flags | match_start | match_end ----+-------+-------------+----------- 1 | {A} | 1 | 4 - 2 | {A} | 2 | 4 - 3 | {A} | 3 | 4 - 4 | {A} | 4 | 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']), @@ -243,7 +250,7 @@ FROM test_mixed_absorption WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+) | B) DEFINE A AS 'A' = ANY(flags), @@ -252,13 +259,15 @@ WINDOW w AS ( id | flags | match_start | match_end ----+-------+-------------+----------- 1 | {A} | 1 | 3 - 2 | {A} | 2 | 3 - 3 | {A} | 3 | 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']), @@ -275,7 +284,7 @@ FROM test_state_coverage WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (A{2,} B) DEFINE A AS 'A' = ANY(flags), @@ -284,15 +293,16 @@ WINDOW w AS ( id | flags | match_start | match_end ----+-------+-------------+----------- 1 | {A} | 1 | 4 - 2 | {A} | 2 | 4 + 2 | {A} | | 3 | {A} | | 4 | {B} | | 5 | {_} | | (5 rows) -- Reluctant pattern (A+?) - not absorbable --- Compare with greedy A+ above: reluctant excluded from absorption. --- Each context produces minimum match independently. +-- 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']), @@ -309,7 +319,7 @@ FROM test_reluctant_absorption WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+?) DEFINE A AS 'A' = ANY(flags) @@ -324,6 +334,8 @@ WINDOW w AS ( (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']), @@ -338,7 +350,7 @@ FROM test_absorb_suffix WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B) DEFINE A AS 'A' = ANY(flags), @@ -347,13 +359,15 @@ WINDOW w AS ( id | flags | match_start | match_end ----+-------+-------------+----------- 1 | {A} | 1 | 4 - 2 | {A} | 2 | 4 - 3 | {A} | 3 | 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']), @@ -368,7 +382,7 @@ FROM test_absorb_alt WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (B+ C | B+ D) DEFINE B AS 'B' = ANY(flags), @@ -378,13 +392,15 @@ WINDOW w AS ( id | flags | match_start | match_end ----+-------+-------------+----------- 1 | {B} | 1 | 4 - 2 | {B} | 2 | 4 - 3 | {B} | 3 | 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']), @@ -399,7 +415,7 @@ FROM test_no_absorb WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (A B+) DEFINE A AS 'A' = ANY(flags), @@ -415,6 +431,8 @@ WINDOW w AS ( (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']), @@ -431,7 +449,7 @@ FROM test_absorb_group WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B) (A B)+) DEFINE A AS 'A' = ANY(flags), @@ -441,7 +459,7 @@ WINDOW w AS ( ----+-------+-------------+----------- 1 | {A} | 1 | 6 2 | {B} | | - 3 | {A} | 3 | 6 + 3 | {A} | | 4 | {B} | | 5 | {A} | | 6 | {B} | | @@ -450,9 +468,9 @@ WINDOW w AS ( -- 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. +-- 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']), @@ -811,6 +829,8 @@ WINDOW w AS ( (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']), @@ -825,7 +845,7 @@ FROM test_multi_unbounded WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B+) DEFINE A AS 'A' = ANY(flags), @@ -834,7 +854,7 @@ WINDOW w AS ( id | flags | match_start | match_end ----+-------+-------------+----------- 1 | {A} | 1 | 4 - 2 | {A} | 2 | 4 + 2 | {A} | | 3 | {B} | | 4 | {B} | | 5 | {X} | | @@ -973,7 +993,8 @@ WINDOW w AS ( -- Reluctant context lifecycle (A+? B with SKIP TO NEXT ROW) -- A+? exits early but if B not available, falls back to loop. --- Contexts not absorbed (reluctant), so multiple survive. +-- 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']), @@ -1257,13 +1278,14 @@ WINDOW w AS ( -- 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. +-- the engine must prefer the fast-forward (exit) path when the body +-- prefers the empty match, and suppress longer matches once exit reaches +-- FIN, mirroring the sibling min<=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 @@ -1280,8 +1302,7 @@ WINDOW gg AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATT rr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+?) DEFINE A AS isa), rr2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??){2,}?) DEFINE A AS isa), ca AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??) DEFINE A AS isa), - cs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?) DEFINE A AS isa) -ORDER BY id; + 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 @@ -1290,6 +1311,63 @@ ORDER BY id; 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 @@ -1302,8 +1380,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (((A??){2,}?){2,}?) DEFINE A AS isa -) -ORDER BY id; +); id | c ----+--- 1 | 0 @@ -1828,7 +1905,7 @@ WINDOW w AS ( 10 | {J} | | (10 rows) --- Reduced frame map reallocation (> 1024 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 @@ -1857,6 +1934,24 @@ FROM ( -- ============================================================ -- 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 @@ -1949,9 +2044,11 @@ WINDOW w AS ( 5 | {B} | | (5 rows) --- Reluctant not absorbed (A+? with SKIP TO NEXT ROW) --- Compare with greedy A+ below: reluctant is not absorbable, --- so all contexts survive independently. +-- 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']), @@ -1968,21 +2065,57 @@ FROM test_reluctant_stats WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + 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 | {_} | | +)$$); + 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) --- Absorbed contexts +-- 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']), @@ -2003,23 +2136,89 @@ WINDOW w AS ( 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 | 4 - 2 | {A} | 2 | 4 - 3 | {A} | 3 | 4 - 4 | {A} | 4 | 4 - 5 | {_} | | + 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) --- Skipped contexts (SKIP TO NEXT ROW) +-- 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']), - (3, ARRAY['A']), - (4, ARRAY['B']) -- Completes match starting at row 1 + (2, ARRAY['A', 'B']), + (3, ARRAY['A', 'B', 'C']), + (4, ARRAY['C']) ) AS t(id, flags) ) SELECT id, flags, @@ -2030,19 +2229,52 @@ WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP TO NEXT ROW - PATTERN (A+ B) + PATTERN (A B C) DEFINE A AS 'A' = ANY(flags), - B AS 'B' = ANY(flags) + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) ); - id | flags | match_start | match_end -----+-------+-------------+----------- - 1 | {A} | 1 | 4 - 2 | {A} | 2 | 4 - 3 | {A} | 3 | 4 - 4 | {B} | | + 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 -- ============================================================ @@ -2741,7 +2973,7 @@ WINDOW w AS ( (4 rows) -- Reluctant nullable: A*? (prefers 0 matches) --- A*? always takes skip path (0 iterations preferred) +-- 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']), @@ -3998,8 +4230,8 @@ WINDOW w AS ( 9 | {_} | | (9 rows) --- Nested END->END between min/max --- Inner group (A B){1,3} exits between min/max -> outer END count++ +-- ((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']), @@ -4039,8 +4271,9 @@ WINDOW w AS ( 9 | {_} | | (9 rows) --- Nested reluctant group ((A B)+?) with following element C --- Inner group exits after minimum 1 iteration +-- 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']), @@ -4103,13 +4336,10 @@ WINDOW w AS ( 5 | {X} | | (5 rows) --- Nested END->END fast-forward --- When an inner group has a nullable body and count < min, the --- fast-forward path exits through the outer END, incrementing --- the outer group's count. --- Pattern: ((A?){2,3}){2,3} -- nested groups, neither collapses --- because the optimizer cannot safely multiply non-exact quantifiers. --- Data has no A rows, forcing all-empty iterations via fast-forward. +-- 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']), @@ -4320,7 +4550,8 @@ WINDOW w AS ( -- 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). +-- 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']), @@ -4770,7 +5001,8 @@ WINDOW w AS ( -- ============================================================ -- INITIAL Mode (Runtime) -- ============================================================ --- Explicit INITIAL (after AFTER MATCH SKIP, per the grammar); same as the default +-- Explicit INITIAL (after AFTER MATCH SKIP, per the grammar); +-- same as the default WITH test_initial_mode AS ( SELECT * FROM (VALUES (1, ARRAY['_']), -- Unmatched @@ -5135,7 +5367,8 @@ WINDOW w AS ( -- Partition end with absorbable pattern -- SKIP PAST LAST ROW + unbounded frame + all rows match A --- Triggers absorb in !rowExists path at partition boundary. +-- 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']), @@ -5208,6 +5441,9 @@ WINDOW w AS ( -- 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']), @@ -5224,7 +5460,7 @@ FROM test_partial_absorbable WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+) B) DEFINE A AS 'A' = ANY(flags), @@ -5233,13 +5469,16 @@ WINDOW w AS ( id | flags | match_start | match_end ----+-------+-------------+----------- 1 | {A} | 1 | 4 - 2 | {A} | 2 | 4 - 3 | {A} | 3 | 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']), @@ -5257,7 +5496,7 @@ FROM test_dynamic_flags WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+) | B) DEFINE A AS 'A' = ANY(flags), @@ -5266,8 +5505,8 @@ WINDOW w AS ( id | flags | match_start | match_end ----+-------+-------------+----------- 1 | {A} | 1 | 3 - 2 | {A} | 2 | 3 - 3 | {A} | 3 | 3 + 2 | {A} | | + 3 | {A} | | 4 | {B} | 4 | 4 5 | {A} | 5 | 5 6 | {B} | 6 | 6 @@ -5275,7 +5514,7 @@ WINDOW w AS ( -- Non-absorbable context during absorption -- Pattern (A B)+ C: A,B in absorbable group, C is not. --- When END exits to C, the cloned context becomes non-absorbable. +-- When END exits to C, the cloned state becomes non-absorbable. WITH test_non_absorbable AS ( SELECT * FROM (VALUES (1, ARRAY['A']), @@ -5389,7 +5628,8 @@ WINDOW w AS ( -- 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). +-- 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 @@ -5423,7 +5663,8 @@ WINDOW w AS ( -- 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). +-- 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 @@ -5456,7 +5697,8 @@ WINDOW w AS ( (4 rows) -- Reluctant branch in ALT not absorbable: (A+?) | B --- A+? is reluctant so not absorbable. Compare with greedy (A+) | B above. +-- 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']), @@ -5473,7 +5715,7 @@ FROM test_reluctant_alt_absorption WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+?) | B) DEFINE A AS 'A' = ANY(flags), @@ -5491,13 +5733,14 @@ WINDOW w AS ( -- ============================================================ -- Zero-Consumption Cycle Detection -- ============================================================ --- Cycle prevention at count > 0: (A*)* inner skip cycles at count=3 +-- (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']) -- Inner A* matches 0, cycles at count=3 + (4, ARRAY['B']) -- A* stops here ) AS t(id, flags) ) SELECT id, flags, @@ -5974,7 +6217,7 @@ WINDOW w AS ( (4 rows) -- (A B C | A B): the first alternative is the longer one and it fits, so --- length and written order agree. Compare with the reverse below. +-- length and written order agree. WITH test_alt_shared_prefix_long_first AS ( SELECT * FROM (VALUES (1, ARRAY['A']), @@ -6381,16 +6624,19 @@ WINDOW w AS ( (4 rows) -- ------------------------------------------------------------ --- 7.2.6 Anchors (not yet implemented - syntax error expected) +-- 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: not yet supported +-- ^ 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: not yet supported +-- $ 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); @@ -6401,13 +6647,15 @@ LINE 3: PATTERN (A $) DEFINE A AS TRUE); -- 7.2.8 Infinite repetitions of empty matches -- (Perl lower-bound stopping rule) -- ------------------------------------------------------------ --- Standard examples from 7.2.8: --- (A?){0,3}: allowed strings include STR00=(), STR01=(A), STR02=(empty), --- STR03=(AA), STR04=(A,empty), STR07=(AAA), STR08=(AA,empty) --- (A?){1,3}: same as {0,3} but STR00 excluded (min=1 not met) --- (A?){2,3}: STR03-06 (len 2) and STR07,08,11,12 (len 3) are valid --- STR06=(STRE,STRE) IS valid because non-final STRE at --- position 1 fills the lower bound +-- 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" @@ -6497,8 +6745,9 @@ WINDOW w AS ( 3 | {B} | | (3 rows) --- (A?){2,3}: min=2, nullable inner. Per ISO/IEC 19075-5 7.2.8 STR06 = (STRE STRE) --- is valid: two empty iterations satisfy min=2. +-- (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']), @@ -6554,10 +6803,10 @@ WINDOW w AS ( 4 | {A} | 4 | 4 (4 rows) --- (A? | B){3}: an empty iteration below min fills the lower bound (STR06), --- and it must outrank the later branch. Row 2 is B only, so A? derives empty --- there; repeating that derivation fills the remaining iterations and the --- match ends at row 1. Taking branch B instead would consume rows 2-3. +-- (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']), @@ -6585,8 +6834,8 @@ WINDOW w AS ( 3 | {A} | 3 | 3 (3 rows) --- The same pattern unrolled. Consecutive identical alternations are merged --- into the rolled form above, so the two must agree. +-- 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']), @@ -6771,8 +7020,7 @@ WINDOW g AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING DEFINE A AS 'A' = ANY(flags)), rr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW PATTERN (((A??){2}?)) - DEFINE A AS 'A' = ANY(flags)) -ORDER BY id; + DEFINE A AS 'A' = ANY(flags)); id | flags | greedy_body | greedy_body_rel | rel_body | rel_body_rel ----+-------+-------------+-----------------+----------+-------------- 1 | {A} | 2 | 2 | 0 | 0 @@ -7072,9 +7320,10 @@ WINDOW w AS ( 4 | {C} | 4 | 4 (4 rows) --- (A? | B){3} C over the same rows: with an exact bound the two empty --- iterations sit below min, so the loop must continue; the third takes B --- and the match is rows 1-2. Contrast with the {2,3} case above. +-- (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']), 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; diff --git a/src/test/regress/sql/create_view.sql b/src/test/regress/sql/create_view.sql index e9fb187d616..017faa54e15 100644 --- a/src/test/regress/sql/create_view.sql +++ b/src/test/regress/sql/create_view.sql @@ -570,6 +570,22 @@ select pg_get_viewdef('view_of_grown_input', true) select * from view_of_grown_input; select * from view_of_grown_input_2; +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); +select 'create view view_of_grown_input_2 as ' + || pg_get_viewdef('view_of_grown_input', true) \gexec +select pg_get_viewdef('view_of_grown_input', true) + = pg_get_viewdef('view_of_grown_input_2', true) as round_trips; +select * from view_of_grown_input; +select * from view_of_grown_input_2; + drop view view_of_grown_input_2, view_of_grown_input; drop function tblfc_f(); drop table tblfc, tblfr; diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql index acb70dd3b5b..8b6d66acdfe 100644 --- a/src/test/regress/sql/rpr.sql +++ b/src/test/regress/sql/rpr.sql @@ -24,8 +24,8 @@ CREATE TABLE rpr_stock ( COPY rpr_stock FROM :'filename'; ANALYZE rpr_stock; -CREATE TEMP TABLE stock (company TEXT, tdate DATE, price INTEGER); -INSERT INTO stock VALUES +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), @@ -37,7 +37,7 @@ INSERT INTO stock VALUES ('company2', '2023-07-07', 1100), ('company2', '2023-07-08', 1300), ('company2', '2023-07-09', 1200), ('company2', '2023-07-10', 1300); -SELECT * FROM stock; +SELECT * FROM rpr_price; -- -- Basic pattern matching with PREV/NEXT @@ -46,7 +46,7 @@ SELECT * FROM stock; -- basic test using PREV SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, nth_value(tdate, 2) OVER w AS nth_second - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -61,7 +61,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- basic test using PREV. UP appears twice SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, nth_value(tdate, 2) OVER w AS nth_second - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -76,7 +76,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- basic test using PREV. Use '*' SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, nth_value(tdate, 2) OVER w AS nth_second - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -91,7 +91,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- basic test using PREV. Use '?' SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, nth_value(tdate, 2) OVER w AS nth_second - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -105,7 +105,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- test using alternation (|) with sequence SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -119,7 +119,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- test using alternation (|) with group quantifier SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -133,7 +133,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- test using nested alternation SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -148,7 +148,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- test using group with quantifier SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -162,7 +162,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- test using absolute threshold values (not relative PREV) -- HIGH: price > 150, LOW: price < 100, MID: neutral range SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -176,7 +176,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- test threshold-based pattern with alternation SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -190,7 +190,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- basic test with fixed-length pattern (A A A = exactly 3) SELECT company, tdate, price, count(*) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -202,7 +202,7 @@ SELECT company, tdate, price, count(*) OVER w -- test using {n} quantifier (A A A should be optimized to A{3}) SELECT company, tdate, price, count(*) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -214,7 +214,7 @@ SELECT company, tdate, price, count(*) OVER w -- test using {n,} quantifier (2 or more) SELECT company, tdate, price, count(*) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -226,7 +226,7 @@ SELECT company, tdate, price, count(*) OVER w -- test using {n,m} quantifier (2 to 4) SELECT company, tdate, price, count(*) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -238,15 +238,15 @@ SELECT company, tdate, price, count(*) OVER w -- test prefix/suffix merge optimization with bounded quantifier -- Pattern A B (A B){1,2} A B should be optimized to (A B){3,4} -CREATE TEMP TABLE rpr_t (id int, val text); -INSERT INTO rpr_t VALUES +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_t +FROM rpr_ab_pairs WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -257,11 +257,11 @@ WINDOW w AS ( A AS val = 'A', B AS val = 'B' ); -DROP TABLE rpr_t; +DROP TABLE rpr_ab_pairs; -- last_value() should remain consistent SELECT company, tdate, price, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -278,7 +278,7 @@ SELECT company, tdate, price, last_value(price) OVER w -- implicitly defined. per spec. SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, nth_value(tdate, 2) OVER w AS nth_second - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -291,7 +291,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- the first row start with less than or equal to 100 SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -305,7 +305,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- second row raises 120% SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -319,7 +319,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- using NEXT SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -334,7 +334,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- match length is always 2, so result is identical to SKIP PAST LAST ROW. -- SKIP TO NEXT ROW's distinct effect is tested in backtracking section.) SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -348,7 +348,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- PREV returns NULL at the partition's first row (no earlier row to fetch) SELECT company, tdate, price, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -361,7 +361,7 @@ WINDOW w AS ( -- NEXT returns NULL at the partition's last row (no later row to fetch) SELECT company, tdate, price, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -375,7 +375,7 @@ WINDOW w AS ( -- DESC order: PREV refers to the row with later date SELECT company, tdate, price, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate DESC @@ -432,7 +432,7 @@ WINDOW w AS ( -- -- Nested PREV -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -442,7 +442,7 @@ WINDOW w AS ( ); -- Nested NEXT -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -452,7 +452,7 @@ WINDOW w AS ( ); -- PREV nested inside NEXT -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -462,7 +462,7 @@ WINDOW w AS ( ); -- PREV nested inside expression inside NEXT -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -472,7 +472,7 @@ WINDOW w AS ( ); -- Triple nesting: error reported at outermost PREV -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -483,7 +483,7 @@ WINDOW w AS ( -- No column reference in PREV/NEXT argument -- PREV(1): constant only, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -493,7 +493,7 @@ WINDOW w AS ( ); -- NEXT(1 + 2): constant expression, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -503,7 +503,7 @@ WINDOW w AS ( ); -- 2-arg form: PREV(1, 1): constant expression as first arg -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -515,7 +515,7 @@ WINDOW w AS ( -- Compound navigation without a column reference must be rejected too, -- consistent with the simple forms above. -- PREV(FIRST(1)): compound, constant only, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -525,7 +525,7 @@ WINDOW w AS ( ); -- NEXT(LAST(1 + 2)): compound, constant expression, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -535,7 +535,7 @@ WINDOW w AS ( ); -- PREV(FIRST(1, 2)): compound, two-arg inner, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -545,7 +545,7 @@ WINDOW w AS ( ); -- PREV(FIRST(1), 2): compound, outer offset only, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -555,7 +555,7 @@ WINDOW w AS ( ); -- PREV(FIRST(1, 2), 3): compound, inner and outer offsets, no column reference -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -565,7 +565,7 @@ WINDOW w AS ( ); -- Non-constant offset: column reference as offset -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -575,7 +575,7 @@ WINDOW w AS ( ); -- Non-constant offset: column reference in compound inner offset -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -585,7 +585,7 @@ WINDOW w AS ( ); -- Non-constant offset: column reference in compound outer offset -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -595,7 +595,7 @@ WINDOW w AS ( ); -- Non-constant offset: volatile function as offset -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -605,7 +605,7 @@ WINDOW w AS ( ); -- Non-constant offset: volatile function as compound outer offset -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -615,7 +615,7 @@ WINDOW w AS ( ); -- Non-constant offset: subquery as offset -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -625,7 +625,7 @@ WINDOW w AS ( ); -- First arg: subquery (caught by DEFINE-level subquery restriction) -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -637,7 +637,7 @@ WINDOW w AS ( -- Volatile function inside nav.arg is rejected in the planner SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -647,7 +647,7 @@ WINDOW w AS ( -- nextval is volatile, so a DEFINE that calls it is rejected CREATE SEQUENCE rpr_seq; -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -661,7 +661,7 @@ DROP SEQUENCE rpr_seq; -- created successfully and errors only when read. CREATE TEMP VIEW rpr_volatile_view AS SELECT company, tdate, price, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -678,7 +678,7 @@ DROP VIEW rpr_volatile_view; -- Qualified outer reference (o.threshold): SELECT * FROM (VALUES (95)) AS o(threshold), LATERAL ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -690,7 +690,7 @@ LATERAL ( -- Unqualified name resolving to the outer column (threshold): SELECT * FROM (VALUES (95)) AS o(threshold), LATERAL ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -702,7 +702,7 @@ LATERAL ( -- Outer reference inside a navigation argument is rejected too: SELECT * FROM (VALUES (95)) AS o(threshold), LATERAL ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -716,7 +716,7 @@ LATERAL ( -- keeps its own diagnosis rather than being reported as a qualifier problem. SELECT * FROM (VALUES (95)) AS o(threshold), LATERAL ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -727,7 +727,7 @@ LATERAL ( ) s; SELECT * FROM (VALUES (95)) AS o(threshold), LATERAL ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -743,7 +743,7 @@ LATERAL ( -- 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 stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -758,7 +758,7 @@ DECLARE n bigint; BEGIN SELECT count(*) INTO n FROM ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -775,7 +775,7 @@ 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 stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -794,7 +794,7 @@ DROP FUNCTION rpr_sqlfn(int); -- enter into it. CREATE FUNCTION rpr_pv(threshold int) RETURNS SETOF int LANGUAGE sql AS $$ - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -807,7 +807,7 @@ $$; -- defined: any pattern variable of that name reserves it. CREATE FUNCTION rpr_pv(threshold int) RETURNS SETOF int LANGUAGE sql AS $$ - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -822,7 +822,7 @@ $$; 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 stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -832,7 +832,7 @@ LANGUAGE sql AS $$ $$; CREATE FUNCTION rpr_compfn(p rpr_pair) RETURNS SETOF int LANGUAGE sql AS $$ - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -855,7 +855,7 @@ DECLARE n bigint; BEGIN SELECT count(*) INTO n FROM ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -875,12 +875,12 @@ DROP FUNCTION rpr_plfn_var(int); CREATE FUNCTION rpr_conflictfn_err() RETURNS bigint LANGUAGE plpgsql AS $$ DECLARE - a stock%ROWTYPE; + a rpr_price%ROWTYPE; n bigint; BEGIN a.price := 95; SELECT count(*) INTO n FROM ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -898,12 +898,12 @@ CREATE FUNCTION rpr_conflictfn() RETURNS bigint LANGUAGE plpgsql AS $$ #variable_conflict use_variable DECLARE - a stock%ROWTYPE; + a rpr_price%ROWTYPE; n bigint; BEGIN a.price := 95; SELECT count(*) INTO n FROM ( - SELECT price FROM stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1019,7 +1019,7 @@ 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 stock + SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1032,42 +1032,42 @@ 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. (stock --- is a temp table, so it is qualified with pg_temp here.) +-- 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 stock +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.stock.price > 0 + DEFINE A AS pg_temp.rpr_price.price > 0 ); -- whole-row variant (schema.table.*): -SELECT price FROM stock +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.stock.*) IS NOT NULL + DEFINE A AS (pg_temp.rpr_price.*) IS NOT NULL ); -- 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 stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A) - DEFINE A AS (stock.*) IS NOT NULL + DEFINE A AS (rpr_price.*) IS NOT NULL ); -- 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 stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1076,7 +1076,7 @@ WINDOW w AS ( DEFINE A AS (stok.*) IS NOT NULL ); -- and the same through a row constructor: -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1089,44 +1089,44 @@ WINDOW w AS ( -- transformExpressionList(), whose star expansion binds them by RTE into -- individual column Vars, past every check. DEFINE skips it. -- ROW(schema.table.*): -SELECT price FROM stock +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.stock.*) IS NOT NULL + DEFINE A AS ROW(pg_temp.rpr_price.*) IS NOT NULL ); -- ROW(table.*): -SELECT price FROM stock +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(stock.*) IS NOT NULL + DEFINE A AS ROW(rpr_price.*) IS NOT NULL ); -- the ROW keyword is optional, so the bare constructor needs the same -- treatment: -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A) - DEFINE A AS (stock.*, 1) IS NOT NULL + DEFINE A AS (rpr_price.*, 1) IS NOT NULL ); -- redundant parentheses are not a way around it: -SELECT price FROM stock +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((stock.*)) IS NOT NULL + DEFINE A AS ROW((rpr_price.*)) IS NOT NULL ); -- a pattern variable qualifier is a separate class of rejection: -SELECT price FROM stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1136,7 +1136,7 @@ WINDOW w AS ( ); -- 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 stock +SELECT price FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1147,7 +1147,7 @@ WINDOW w AS ( -- 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 stock AS a +SELECT price FROM rpr_price AS a WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1155,28 +1155,29 @@ WINDOW w AS ( PATTERN (A) DEFINE A AS a.price > 100 ); --- Each rejection above classifies 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 stock +-- 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 stock.pric > 0 + DEFINE A AS rpr_price.pric > 0 ); -SELECT price FROM stock +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.stock.pric > 0 + DEFINE A AS pg_temp.rpr_price.pric > 0 ); -- the same typo outside a DEFINE clause, for comparison: -SELECT price FROM stock WHERE stock.pric > 0; +SELECT price FROM rpr_price WHERE rpr_price.pric > 0; -- 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 @@ -1233,7 +1234,7 @@ 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 stock +FROM rpr_price WHERE company = 'company2' AND tdate <= '2023-07-03' WINDOW w AS ( PARTITION BY company @@ -1347,7 +1348,7 @@ DROP TABLE rpr_nest_i, rpr_nest_o; -- 200 -> 150, then 110 -> 130 -> 120, which stops where 130 only ties 130. SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1362,7 +1363,7 @@ WINDOW w AS ( -- then 140, 150 up to where 90 falls short of 130. SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1374,7 +1375,7 @@ WINDOW w AS ( -- PREV(price - 50, 1): fetches (price - 50) from 1 row back SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1385,7 +1386,7 @@ WINDOW w AS ( -- NEXT(price * 2, 1): fetches (price * 2) from 1 row ahead SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1417,7 +1418,7 @@ LIMIT 3; -- A+ matches entire partition as one group; count = partition size SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1427,7 +1428,7 @@ WINDOW w AS ( -- 2-arg PREV/NEXT: negative offset SELECT company, tdate, price, first_value(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1437,7 +1438,7 @@ WINDOW w AS ( -- 2-arg PREV/NEXT: NULL offset (typed) SELECT company, tdate, price, first_value(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1447,7 +1448,7 @@ WINDOW w AS ( -- 2-arg PREV/NEXT: NULL offset (untyped) SELECT company, tdate, price, first_value(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1458,7 +1459,7 @@ WINDOW w AS ( -- 2-arg PREV/NEXT: host variable negative and NULL PREPARE test_prev_offset(int8) AS SELECT company, tdate, price, first_value(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1472,7 +1473,7 @@ DEALLOCATE test_prev_offset; -- 2-arg PREV/NEXT: host variable with expression (0 + $1) PREPARE test_prev_offset(int8) AS SELECT company, tdate, price, first_value(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1489,7 +1490,7 @@ DEALLOCATE test_prev_offset; SET plan_cache_mode = force_generic_plan; PREPARE test_prev_offset(int8) AS SELECT company, tdate, price, first_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1506,7 +1507,7 @@ RESET plan_cache_mode; -- B: price exceeds both 1-back and 2-back values SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1521,7 +1522,7 @@ WINDOW w AS ( -- A: price exceeds 1-back and is below 1-ahead (ascending interior point) SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1535,7 +1536,7 @@ WINDOW w AS ( -- 1-back and 2-back tdate text. SELECT company, tdate, tdate::text AS tdate_text, first_value(tdate::text) OVER w, last_value(tdate::text) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1550,7 +1551,7 @@ WINDOW w AS ( -- B matches when price 1-back > price 2-back (ascending pair). SELECT company, tdate, price::numeric AS nprice, first_value(price::numeric) OVER w, last_value(price::numeric) OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -1561,6 +1562,24 @@ WINDOW w AS ( B AS PREV(price::numeric, 1) > PREV(price::numeric, 2) ); +-- 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) +); +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. @@ -1582,15 +1601,15 @@ DROP TABLE rpr_typmod; -- Test data for FIRST/LAST: values cycle back so FIRST(val) = LAST(val) -- at specific positions. -CREATE TEMP TABLE rpr_nav (id int, val int); -INSERT INTO rpr_nav VALUES (1,10),(2,20),(3,30),(4,10),(5,50),(6,10); +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 WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1601,7 +1620,7 @@ FROM rpr_nav WINDOW w AS ( -- LAST(val): always equals current row's val (offset 0 default) -- Equivalent to: B AS val > 15 SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1615,7 +1634,7 @@ FROM rpr_nav WINDOW w AS ( -- id2(20!=10), id3(30!=10), id4(10=10) -> match {1,2,3,4} -- match_start=5(50): id6(10!=50) -> no match SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1628,7 +1647,7 @@ FROM rpr_nav WINDOW w AS ( -- match_start=1(10): greedy A eats all, B tries last: -- id6(10=10) -> match {1,2,3,4,5,6} SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1639,7 +1658,7 @@ FROM rpr_nav WINDOW w AS ( -- SKIP TO NEXT ROW with FIRST(val) = LAST(val): overlapping match attempts. -- Each row reports only the match that starts at it. SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP TO NEXT ROW @@ -1651,7 +1670,7 @@ FROM rpr_nav WINDOW w AS ( -- -- FIRST(val, 0) = FIRST(val): match_start row SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1660,10 +1679,11 @@ FROM rpr_nav WINDOW w AS ( ); -- 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=1(10): FIRST(val,1)=20, B needs val=20 +-- -> id2(20) match, id3(30) no -- match_start=3(30): FIRST(val,1)=10, B needs val=10 -> id4(10) match SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1673,7 +1693,7 @@ FROM rpr_nav WINDOW w AS ( -- FIRST(val, 99): offset beyond match range -> NULL, no match SELECT id, val, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1683,7 +1703,7 @@ FROM rpr_nav WINDOW w AS ( -- LAST(val, 0) = LAST(val): current row SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1695,7 +1715,7 @@ FROM rpr_nav WINDOW w AS ( -- At B evaluation on id2: LAST(val,1) = val at id1 = 10 -- B matches when previous row val < 30 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1705,7 +1725,7 @@ FROM rpr_nav WINDOW w AS ( -- LAST(val, 99): offset before match_start -> NULL SELECT id, val, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1714,7 +1734,7 @@ FROM rpr_nav WINDOW w AS ( ); -- Error: NULL offset -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -1722,7 +1742,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( ); -- Error: negative offset -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -1736,10 +1756,10 @@ SELECT prev(f), next(f), first(f), last(f) FROM rpr_names f; DROP TABLE rpr_names; -- Compound navigation: PREV(FIRST(val), M) --- rpr_nav: (1,10),(2,20),(3,30),(4,10),(5,50),(6,10) +-- 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 WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1750,7 +1770,7 @@ FROM rpr_nav WINDOW w AS ( -- NEXT(FIRST(val, 1), 1): target = match_start + 1 + 1 = match_start + 2 -- At match_start=1, B on id2: target=1+1+1=3(val=30), 30>0 -> true SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1759,12 +1779,13 @@ FROM rpr_nav WINDOW w AS ( ); -- 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). +-- 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. +-- At currentpos=3 (start id=2): target=1(val=10) -> in range -> B runs on +-- id3..id6, so the match is id2..id6. SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1776,10 +1797,10 @@ FROM rpr_nav WINDOW w AS ( -- 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. +-- B stays true through id5 (target=6); at id6 target=7 -> out of range +-- -> NULL, so the match is id1..id5. SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1789,7 +1810,7 @@ FROM rpr_nav WINDOW w AS ( -- Compound: outer offset beyond partition (PREV far back) SELECT id, val, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B+) @@ -1798,7 +1819,7 @@ FROM rpr_nav WINDOW w AS ( -- Compound: outer offset beyond partition (NEXT far forward) SELECT id, val, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B+) @@ -1807,7 +1828,7 @@ FROM rpr_nav WINDOW w AS ( -- Compound: inner offset beyond match range (FIRST offset too large) SELECT id, val, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B+) @@ -1816,7 +1837,7 @@ FROM rpr_nav WINDOW w AS ( -- Compound: inner offset beyond match range (LAST offset too large) SELECT id, val, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B+) @@ -1824,7 +1845,7 @@ FROM rpr_nav WINDOW w AS ( ); -- Compound: NULL outer offset (runtime error) -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -1832,7 +1853,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( ); -- Compound: negative outer offset (runtime error) -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -1840,27 +1861,27 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( ); -- Compound: an out-of-range inner offset must not skip validation of the outer --- one. All four arms resolve their outer offset through the same call, so each --- appears once, and the negative and the null case take two arms apiece. -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( +-- 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 ); -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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(LAST(val, 99), NULL::int8) IS NULL ); -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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 NEXT(FIRST(val, 99), NULL::int8) IS NULL ); -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -1872,7 +1893,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( -- The reach reads "runtime" here; a custom plan would fold it to 99 - 1 = 98. SET plan_cache_mode = force_generic_plan; PREPARE test_compound_illegal(int8, int8) AS -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( +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+) @@ -1919,7 +1940,7 @@ DROP TABLE rpr_nav_empty; -- Outer offset overflows int64: target position out of range -> NULL. -- Plain NEXT(val, INT64_MAX): currentpos + INT64_MAX overflows. -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( +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+) @@ -1928,7 +1949,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( -- Compound NEXT(FIRST()): outer offset overflow. Inner offset 1 forces -- inner_pos >= 1, so inner_pos + INT64_MAX overflows at every match. -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( +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+) @@ -1936,7 +1957,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( ); -- Compound NEXT(LAST()): outer offset overflow. -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -1949,7 +1970,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( -- 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 WINDOW w 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+) @@ -1958,7 +1979,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( -- 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 WINDOW w 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+) @@ -1968,7 +1989,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( -- Compound: default offsets on both sides -- PREV(FIRST(val)): inner=0 (match_start), outer=1 -> target = match_start - 1 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1978,7 +1999,7 @@ FROM rpr_nav WINDOW w AS ( -- NEXT(LAST(val)): inner=0 (currentpos), outer=1 -> target = currentpos + 1 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -1987,7 +2008,7 @@ FROM rpr_nav WINDOW w AS ( ); -- Compound: inner NULL offset (runtime error) -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -1995,7 +2016,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( ); -- Compound: inner negative offset (runtime error) -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -2003,7 +2024,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( ); -- Offset argument whose type has no implicit cast to bigint (parse error) -SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w 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+) @@ -2013,7 +2034,7 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( -- Compound + host variable offsets PREPARE test_compound_offset(int8, int8) AS SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW @@ -2026,7 +2047,7 @@ DEALLOCATE test_compound_offset; -- Compound + SKIP TO NEXT ROW: overlapping matches with PREV(FIRST()) SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt -FROM rpr_nav WINDOW w AS ( +FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP TO NEXT ROW @@ -2050,7 +2071,7 @@ FROM rpr_nav_part WINDOW w AS ( DROP TABLE rpr_nav_part; -- Reverse nesting: FIRST wrapping PREV is prohibited -SELECT id, val FROM rpr_nav WINDOW w AS ( +SELECT id, val FROM rpr_nav_cycle WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B) @@ -2058,14 +2079,14 @@ SELECT id, val FROM rpr_nav WINDOW w AS ( ); -- Reverse nesting: LAST wrapping NEXT is prohibited -SELECT id, val FROM rpr_nav WINDOW w AS ( +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 ); -DROP TABLE rpr_nav; +DROP TABLE rpr_nav_cycle; -- -- SKIP TO / Backtracking / Frame boundary @@ -2073,7 +2094,7 @@ DROP TABLE rpr_nav; -- match everything SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -2088,7 +2109,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER -- nth_value beyond reduced frame (no IGNORE NULLS) SELECT company, tdate, price, nth_value(price, 5) OVER w AS nth_5 -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -2104,7 +2125,7 @@ WINDOW w AS ( -- backtracking with reclassification of rows -- using AFTER MATCH SKIP PAST LAST ROW SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -2120,7 +2141,7 @@ SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER -- backtracking with reclassification of rows -- using AFTER MATCH SKIP TO NEXT ROW SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -2172,7 +2193,7 @@ WINDOW w AS ( -- ROWS BETWEEN CURRENT ROW AND offset FOLLOWING SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w, count(*) OVER w - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -2198,7 +2219,7 @@ SELECT company, tdate, price, sum(price) OVER w, avg(price) OVER w, count(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2220,7 +2241,7 @@ SELECT company, tdate, price, sum(price) OVER w, avg(price) OVER w, count(price) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2235,7 +2256,7 @@ DOWN AS price < PREV(price) -- row_number() within RPR reduced frame SELECT company, tdate, price, row_number() OVER w, count(*) OVER w -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -2253,20 +2274,20 @@ WINDOW w AS ( -- -- JOIN case -CREATE TEMP TABLE t1 (i int, v1 int); -CREATE TEMP TABLE t2 (j int, v2 int); -INSERT INTO t1 VALUES(1,10); -INSERT INTO t1 VALUES(1,11); -INSERT INTO t1 VALUES(1,12); -INSERT INTO t2 VALUES(2,10); -INSERT INTO t2 VALUES(2,11); -INSERT INTO t2 VALUES(2,12); +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 t1, t2 WHERE t1.v1 <= 11 AND t2.v2 <= 11; +SELECT * FROM rpr_join_left, rpr_join_right WHERE rpr_join_left.v1 <= 11 AND rpr_join_right.v2 <= 11; -SELECT *, count(*) OVER w FROM t1, t2 +SELECT *, count(*) OVER w FROM rpr_join_left, rpr_join_right WINDOW w AS ( - PARTITION BY t1.i + PARTITION BY rpr_join_left.i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A) @@ -2276,7 +2297,7 @@ WINDOW w AS ( -- WITH case WITH wstock AS ( - SELECT * FROM stock WHERE tdate < '2023-07-08' + SELECT * FROM rpr_price WHERE tdate < '2023-07-08' ) SELECT tdate, price, first_value(tdate) OVER w, @@ -2313,10 +2334,10 @@ LATERAL ( ORDER BY g.x, sub.id; -- PREV has multiple column reference -CREATE TEMP TABLE rpr1 (id INTEGER, i SERIAL, j INTEGER); -INSERT INTO rpr1(id, j) SELECT 1, g*2 FROM generate_series(1, 10) AS g; +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 rpr1 + FROM rpr_prev_multicol WINDOW w AS ( PARTITION BY id ORDER BY i @@ -2437,11 +2458,12 @@ RESET jit; -- IGNORE NULLS -- --- no NULL rows case. The result should be identical with "basic test using PREV" +-- no NULL rows case. The result should be identical with +-- "basic test using PREV" SELECT company, tdate, price, first_value(price) IGNORE NULLS OVER w, last_value(price) IGNORE NULLS OVER w, nth_value(tdate, 2) IGNORE NULLS OVER w AS nth_second - FROM stock + FROM rpr_price WINDOW w AS ( PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING @@ -2491,7 +2513,7 @@ WITH data AS ( -- nth_value beyond reduced frame with IGNORE NULLS SELECT company, tdate, price, nth_value(price, 5) IGNORE NULLS OVER w AS nth_5_in -FROM stock +FROM rpr_price WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -2541,7 +2563,8 @@ WINDOW w AS ( -- -- last_value IGNORE NULLS when the reduced frame ends with NULLs --- The search for a non-NULL value runs past the end of the reduced frame. +-- 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); @@ -2639,14 +2662,14 @@ DROP TABLE rpr_dormant; -- NULL handling -- -CREATE TEMP TABLE stock_null (company TEXT, tdate DATE, price INTEGER); -INSERT INTO stock_null VALUES ('c1', '2023-07-01', 100); -INSERT INTO stock_null VALUES ('c1', '2023-07-02', NULL); -- NULL in middle -INSERT INTO stock_null VALUES ('c1', '2023-07-03', 200); -INSERT INTO stock_null VALUES ('c1', '2023-07-04', 150); +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 stock_null +FROM rpr_stock_null WINDOW w AS ( PARTITION BY company ORDER BY tdate @@ -2809,7 +2832,8 @@ SELECT * FROM ( ) ) t WHERE cnt > 0 ORDER BY gap_rn; --- Price-volume divergence: price rising while volume declining (bearish signal) +-- 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, diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql index 9fc8f9cd0ac..e60c2c46d28 100644 --- a/src/test/regress/sql/rpr_base.sql +++ b/src/test/regress/sql/rpr_base.sql @@ -60,8 +60,7 @@ CREATE TABLE rpr_keywords ( INSERT INTO rpr_keywords VALUES (1, 10, 20, 30, 40, 45, 50, 60); SELECT id, define, initial, past, pattern, permute, seek, skip -FROM rpr_keywords -ORDER BY id; +FROM rpr_keywords; DROP TABLE rpr_keywords; @@ -70,14 +69,14 @@ DROP TABLE rpr_keywords; -- ============================================================ -- Simple column references -CREATE TABLE stock_price ( +CREATE TABLE rpr_stock_price ( dt DATE, symbol TEXT, price NUMERIC, volume INT ); -INSERT INTO stock_price VALUES +INSERT INTO rpr_stock_price VALUES ('2024-01-01', 'AAPL', 150, 1000), ('2024-01-02', 'AAPL', 155, 1200), ('2024-01-03', 'AAPL', 152, 900), @@ -86,53 +85,49 @@ INSERT INTO stock_price VALUES -- Simple column reference SELECT dt, price, COUNT(*) OVER w as cnt -FROM stock_price +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 -) -ORDER BY dt; +); -- Multiple column references SELECT dt, price, volume, COUNT(*) OVER w as cnt -FROM stock_price +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 -) -ORDER BY dt; +); -- Expression in DEFINE SELECT dt, price, COUNT(*) OVER w as cnt -FROM stock_price +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 -) -ORDER BY dt; +); -- Arithmetic and functions SELECT dt, price, volume, COUNT(*) OVER w as cnt -FROM stock_price +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 -) -ORDER BY dt; +); -DROP TABLE stock_price; +DROP TABLE rpr_stock_price; -- Pattern variables with no DEFINE entry CREATE TABLE rpr_auto (id INT, val INT); @@ -146,8 +141,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ B*) DEFINE A AS val > 15 -) -ORDER BY id; +); -- Multiple undefined variables SELECT id, val, COUNT(*) OVER w as cnt @@ -158,8 +152,7 @@ WINDOW w AS ( PATTERN (A B C) DEFINE A AS val > 0 -- B and C have no DEFINE entry, so they match every row -) -ORDER BY id; +); -- All variables defined explicitly SELECT id, val, COUNT(*) OVER w as cnt @@ -172,8 +165,7 @@ WINDOW w AS ( X AS val > 10, Y AS val > 20, Z AS val < 20 -) -ORDER BY id; +); DROP TABLE rpr_auto; @@ -215,8 +207,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (T+) DEFINE T AS flag -) -ORDER BY id; +); -- NULL::boolean SELECT id, COUNT(*) OVER w as cnt @@ -226,19 +217,18 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (N+) DEFINE N AS NULL::boolean -) -ORDER BY id; +); -- Implicit cast to boolean via custom type -CREATE TYPE truthyint AS (v int); -CREATE FUNCTION truthyint_to_bool(truthyint) RETURNS boolean AS $$ +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 (truthyint AS boolean) - WITH FUNCTION truthyint_to_bool(truthyint) +CREATE CAST (rpr_truthyint AS boolean) + WITH FUNCTION rpr_truthyint_to_bool(rpr_truthyint) AS ASSIGNMENT; -CREATE TABLE rpr_coerce (id int, val truthyint); +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 @@ -254,16 +244,16 @@ FROM (SELECT id, val, ) s ORDER BY id; DROP TABLE rpr_coerce; -DROP CAST (truthyint AS boolean); -DROP FUNCTION truthyint_to_bool(truthyint); -DROP TYPE truthyint; +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 boolish AS boolean; -CREATE TABLE rpr_domain (id int, flag boolish); +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 @@ -272,10 +262,9 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS flag -) -ORDER BY id; +); DROP TABLE rpr_domain; -DROP DOMAIN boolish; +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) @@ -288,8 +277,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (UP+) DEFINE UP AS id > PREV(val) -) -ORDER BY id; +); DROP TABLE rpr_nav; -- A non-boolean DEFINE expression is rejected @@ -331,8 +319,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (C+) DEFINE C AS CASE WHEN val1 > 10 THEN val2 > 20 ELSE false END -) -ORDER BY id; +); DROP TABLE rpr_complex; @@ -348,8 +335,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS id > 0, B AS id > 5 -- B not in pattern -) -ORDER BY id; +); DROP TABLE rpr_unused; @@ -365,8 +351,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B) DEFINE A AS v < 0, B AS 1 / (v - v) > 0 -) -ORDER BY id; +); DROP TABLE rpr_lazy; @@ -398,8 +383,8 @@ WINDOW w AS ( DROP TABLE rpr_navcoll; --- A system column in a DEFINE expression. It reaches the expression as a --- scan system attribute rather than an outer Var, and navigation still +-- 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); @@ -438,7 +423,7 @@ WINDOW w AS ( ) ORDER BY id; --- ERROR: frame must start at current row when row pattern recognition is used +-- frame must start at CURRENT ROW, not UNBOUNDED PRECEDING SELECT COUNT(*) OVER w FROM rpr_frame WINDOW w AS ( @@ -526,7 +511,7 @@ WINDOW w AS ( DEFINE A AS val > 0 ); --- ERROR: frame must start at current row when row pattern recognition is used +-- frame must start at CURRENT ROW, not offset PRECEDING SELECT COUNT(*) OVER w FROM rpr_frame WINDOW w AS ( @@ -536,7 +521,7 @@ WINDOW w AS ( DEFINE A AS val > 0 ); --- ERROR: frame must start at current row with RPR +-- frame must start at CURRENT ROW, not offset FOLLOWING SELECT COUNT(*) OVER w FROM rpr_frame WINDOW w AS ( @@ -576,8 +561,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A) DEFINE A AS val > 0 -) -ORDER BY id; +); -- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame -- and is likewise rejected (caught at execution time). @@ -589,8 +573,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A) DEFINE A AS val > 0 -) -ORDER BY id; +); -- A non-constant frame end offset is allowed; a zero value is rejected by the -- same execution-time check the literal 0 above reaches. @@ -603,8 +586,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A) DEFINE A AS val > 0 -) -ORDER BY id; +); EXECUTE rpr_end_offset(2); EXECUTE rpr_end_offset(0); DEALLOCATE rpr_end_offset; @@ -618,8 +600,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); -- Maximum offset: CURRENT ROW AND 2147483646 FOLLOWING (INT_MAX - 1) SELECT id, val, COUNT(*) OVER w as cnt @@ -630,8 +611,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); -- int64 frame-end overflow: a huge FOLLOWING offset must clamp to the -- partition end (matchStartRow + offset + 1 overflows int64; the clamp makes @@ -646,8 +626,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); -- range frame is not allowed with RPR SELECT id, val, COUNT(*) OVER w as cnt @@ -658,8 +637,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A B?) DEFINE A AS val >= 0, B AS val >= 0 -) -ORDER BY id; +); -- GROUPS frame with RPR (not permitted) SELECT id, val, COUNT(*) OVER w as cnt @@ -670,8 +648,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A B?) DEFINE A AS val >= 0, B AS val >= 0 -) -ORDER BY id; +); DROP TABLE rpr_frame; @@ -695,8 +672,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A B+) DEFINE A AS val >= 10, B AS val > 15 -) -ORDER BY id; +); -- PARTITION BY with RANGE frame SELECT id, grp, val, COUNT(*) OVER w as cnt @@ -708,8 +684,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A B?) DEFINE A AS val >= 10, B AS val >= 20 -) -ORDER BY id; +); DROP TABLE rpr_partition; @@ -732,8 +707,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ | B+ | C+) DEFINE A AS val > 35, B AS val BETWEEN 15 AND 35, C AS val < 15 -) -ORDER BY id; +); -- Grouping @@ -745,8 +719,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (((A B) C)+) DEFINE A AS val > 10, B AS val > 20, C AS val > 30 -) -ORDER BY id; +); -- Sequence @@ -763,8 +736,7 @@ WINDOW w AS ( C AS val BETWEEN 25 AND 35, D AS val BETWEEN 35 AND 45, E AS val >= 45 -) -ORDER BY id; +); -- Complex combinations @@ -776,8 +748,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A B) | (C D)) DEFINE A AS val < 20, B AS val >= 20, C AS val < 30, D AS val >= 30 -) -ORDER BY id; +); -- Alternation + sequence + grouping SELECT id, val, COUNT(*) OVER w as cnt @@ -792,8 +763,7 @@ WINDOW w AS ( DOWN AS val <= 30, FLAT AS val BETWEEN 25 AND 35, FINISH AS val > 40 -) -ORDER BY id; +); -- Nested alternation in groups SELECT id, val, COUNT(*) OVER w as cnt @@ -803,8 +773,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A | B) (C | D)) DEFINE A AS val < 15, B AS val BETWEEN 15 AND 25, C AS val BETWEEN 25 AND 35, D AS val > 35 -) -ORDER BY id; +); DROP TABLE rpr_pattern; @@ -827,8 +796,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*) DEFINE A AS val > 0 -) -ORDER BY id; +); -- + (one or more) SELECT id, val, COUNT(*) OVER w as cnt @@ -838,8 +806,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 50 -) -ORDER BY id; +); -- ? (zero or one) SELECT id, val, COUNT(*) OVER w as cnt @@ -849,8 +816,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A?) DEFINE A AS val = 50 -) -ORDER BY id; +); -- Edge case quantifiers @@ -862,8 +828,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{0} B) DEFINE A AS val > 1000, B AS val > 0 -) -ORDER BY id; +); -- {0,0} is not allowed (max must be >= 1) SELECT id, val, COUNT(*) OVER w as cnt @@ -873,8 +838,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{0,0} B) DEFINE A AS val > 1000, B AS val > 0 -) -ORDER BY id; +); -- {0,1} (equivalent to ?) SELECT id, val, COUNT(*) OVER w as cnt @@ -884,8 +848,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{0,1}) DEFINE A AS val = 50 -) -ORDER BY id; +); -- Exact quantifiers {n} @@ -897,8 +860,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{3}) DEFINE A AS val > 0 -) -ORDER BY id; +); -- Range quantifiers {n,} @@ -910,8 +872,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}) DEFINE A AS val > 40 -) -ORDER BY id; +); -- Upper bound quantifiers {,m} @@ -923,8 +884,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}) DEFINE A AS val > 0 -) -ORDER BY id; +); -- Range quantifiers {n,m} @@ -936,8 +896,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{3,7}) DEFINE A AS val > 0 -) -ORDER BY id; +); DROP TABLE rpr_quant; @@ -1010,8 +969,8 @@ WINDOW w AS ( ); -- {n}? (exactly n): min == max, so the reluctant flag is cleared and the --- plan is indistinguishable from A{2}. This pins the normalization, not --- shortest-match behaviour. +-- 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 ( @@ -1262,8 +1221,8 @@ WINDOW w AS ( DEFINE A AS val > 0 ); --- 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 +-- 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 ( @@ -1422,8 +1381,8 @@ SELECT format($$SELECT count(*) OVER w FROM (SELECT 1 i) t 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. +-- 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 @@ -1614,11 +1573,11 @@ FROM t WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A) DEFINE A AS PREV(LAST(v / 0, 1), 2) > 0); --- 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 preprocess_expression() --- documents them as mandatory, not as optimizations. Each of the three below --- reaches the executor only if those rewrites reach inside a navigation +-- 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); @@ -1696,8 +1655,7 @@ WINDOW w AS ( DEFINE A AS val > 0, B AS val > PREV(val) -) -ORDER BY id; +); -- NEXT function - reference next row in pattern SELECT id, val, COUNT(*) OVER w as cnt @@ -1709,8 +1667,7 @@ WINDOW w AS ( DEFINE A AS val < NEXT(val), B AS val > 0 -) -ORDER BY id; +); -- Combined PREV and NEXT SELECT id, val, COUNT(*) OVER w as cnt @@ -1723,8 +1680,7 @@ WINDOW w AS ( A AS val > 0, B AS val > PREV(val) AND val < NEXT(val), C AS val > PREV(val) -) -ORDER BY id; +); -- PREV function cannot be used other than in DEFINE SELECT PREV(id), id, val, COUNT(*) OVER w as cnt @@ -1736,8 +1692,7 @@ WINDOW w AS ( DEFINE A AS val > 0, B AS val > PREV(val) -) -ORDER BY id; +); -- NEXT function cannot be used other than in DEFINE SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt @@ -1749,8 +1704,7 @@ WINDOW w AS ( DEFINE A AS val > 0, B AS val > PREV(val) -) -ORDER BY id; +); -- FIRST function - reference match_start row SELECT id, val, COUNT(*) OVER w as cnt @@ -1762,8 +1716,7 @@ WINDOW w AS ( DEFINE A AS val > 0, B AS val > FIRST(val) -) -ORDER BY id; +); -- LAST function without offset - equivalent to current row's value SELECT id, val, COUNT(*) OVER w as cnt @@ -1775,8 +1728,7 @@ WINDOW w AS ( DEFINE A AS val > 0, B AS LAST(val) > PREV(val) -) -ORDER BY id; +); -- FIRST and LAST combined SELECT id, val, COUNT(*) OVER w as cnt @@ -1788,8 +1740,7 @@ WINDOW w AS ( DEFINE A AS val > 0, B AS val > FIRST(val) AND LAST(val) > PREV(val) -) -ORDER BY id; +); -- FIRST function cannot be used other than in DEFINE SELECT FIRST(id), id, val FROM rpr_nav; @@ -1799,23 +1750,24 @@ SELECT LAST(id), id, val FROM rpr_nav; DROP TABLE rpr_nav; --- Name-space: prev/next/first/last are navigation functions, not ordinary functions +-- Name-space: prev/next/first/last are navigation functions, +-- not ordinary functions CREATE SCHEMA rpr_navns; SET search_path TO rpr_navns, public; -CREATE TABLE nt (g text, id int, val int); -INSERT INTO nt VALUES ('x', 1, 100), ('x', 2, 200), ('x', 3, 150), +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 nt; -SELECT next(val) FROM nt; -SELECT prev(val, 2) FROM nt; -SELECT next(val, 2) FROM nt; -SELECT first(val) FROM nt; -SELECT last(val) FROM nt; -SELECT first(val, 1) FROM nt; +SELECT prev(val) FROM rpr_nav_rows; +SELECT next(val) FROM rpr_nav_rows; +SELECT prev(val, 2) FROM rpr_nav_rows; +SELECT next(val, 2) FROM rpr_nav_rows; +SELECT first(val) FROM rpr_nav_rows; +SELECT last(val) FROM rpr_nav_rows; +SELECT first(val, 1) FROM rpr_nav_rows; -- A schema-qualified call is also a plain (failing) function lookup -SELECT pg_catalog.prev(val) FROM nt; +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' @@ -1824,7 +1776,7 @@ SELECT next(10); -- Inside DEFINE, unqualified PREV is nav whether or not a user prev() exists SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (START UP+) @@ -1836,14 +1788,14 @@ SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id CREATE FUNCTION prev(integer) RETURNS integer LANGUAGE plpgsql VOLATILE AS 'BEGIN RETURN -999; END'; SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + 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; SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A+) @@ -1854,7 +1806,7 @@ SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id CREATE OR REPLACE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999' LANGUAGE sql VOLATILE; SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A+) @@ -1864,50 +1816,50 @@ SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id -- No OVER references the window, so flattening the subquery drops it -- before the check runs, the same way an unreferenced CTE is never planned SELECT id FROM ( - SELECT id FROM nt + 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; --- OFFSET 0 keeps the subquery, but still no OVER references the window, so --- the planner withdraws the DEFINE clause of a window it will not run and the --- check finds nothing left to reject +-- 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 nt + 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: a subquery window that does run keeps its DEFINE, so it is checked +-- 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 nt + 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 makes the subquery rel dummy, so the planner never plans it --- and nothing looks at its DEFINE -SELECT id FROM ( - SELECT id FROM nt +-- 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) OFFSET 0) sub + PATTERN (A+) DEFINE A AS random() > 0.5)) sub WHERE false; --- The volatile is in a dead CASE arm that folds away, so nothing --- volatile is left for the check to find -SELECT id FROM ( - SELECT id FROM nt - WINDOW w AS ( +-- 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)) s + ELSE val > 5 END) ORDER BY id; -- 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 +-- 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) @@ -1919,17 +1871,17 @@ DROP FUNCTION rpr_off_leak(bigint); -- A UNION ALL leaf is flattened like any other subquery, so its -- unreferenced window goes the same way SELECT id FROM ( - SELECT id FROM nt + 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 nt) s; + SELECT id FROM rpr_nav_rows) s; -- An unreferenced CTE is never planned, so nothing looks at its -- DEFINE WITH unused AS ( - SELECT id FROM nt + SELECT id FROM rpr_nav_rows WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS random() > 0.5)) @@ -1937,7 +1889,7 @@ SELECT 1; -- ERROR: referencing it plans the CTE, and the check reaches the DEFINE there WITH used AS ( - SELECT id FROM nt + SELECT id FROM rpr_nav_rows WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS random() > 0.5)) @@ -1948,23 +1900,24 @@ DROP FUNCTION prev(integer); CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999' LANGUAGE sql IMMUTABLE; SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + 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; --- (val).prev is attribute notation, so it calls the ordinary function prev(val) +-- (val).prev is attribute notation, +-- so it calls the ordinary function prev(val) -- (the IMMUTABLE user prev here), the same as the schema-qualified call below SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + 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; SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A+) @@ -1972,119 +1925,120 @@ SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id ORDER BY id; -- Zero or more than two arguments is an error, with no function fallback -SELECT count(*) OVER w FROM nt +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); -SELECT count(*) OVER w FROM nt +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); -- the error stands even when a user function of that exact arity exists CREATE FUNCTION prev(integer, integer, integer) RETURNS integer AS 'SELECT -999' LANGUAGE sql IMMUTABLE; -SELECT count(*) OVER w FROM nt +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); DROP FUNCTION prev(integer, integer, integer); -- Syntactic decoration is rejected -SELECT count(*) OVER w FROM nt +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); -SELECT count(*) OVER w FROM nt +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); -SELECT count(*) OVER w FROM nt +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); -SELECT count(*) OVER w FROM nt +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); -SELECT count(*) OVER w FROM nt +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); -SELECT count(*) OVER w FROM nt +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); -SELECT count(*) OVER w FROM nt +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); -SELECT count(*) OVER w FROM nt +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); -SELECT count(*) OVER w FROM nt +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); -- Quoting does not escape: "prev" is nav, "PREV" is an ordinary name SELECT id, val, count(*) OVER w AS cnt - FROM nt + 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; -SELECT count(*) OVER w FROM nt +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); -- A view round-trips: bare PREV stays a navigation function, and a qualified -- user prev() stays schema-qualified so it does not reparse as navigation -CREATE VIEW navns_nav AS - SELECT id, count(*) OVER w AS cnt FROM nt +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 navns_fn AS - SELECT id, count(*) OVER w AS cnt FROM nt +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('navns_nav'); -SELECT pg_get_viewdef('navns_fn'); -DROP VIEW navns_nav, navns_fn; +SELECT pg_get_viewdef('rpr_navns_nav'); +SELECT pg_get_viewdef('rpr_navns_fn'); +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 navns_fn_last AS - SELECT id, count(*) OVER w AS cnt FROM nt +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('navns_fn_last'); -DROP VIEW navns_fn_last; +SELECT pg_get_viewdef('rpr_navns_fn_last'); +DROP VIEW rpr_navns_fn_last; DROP FUNCTION rpr_navns.last(integer); --- Attribute notation is field selection only, never a function fallback +-- 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 ct (id int, p rpr_navns_pair); -INSERT INTO ct VALUES (1, (10, 20)), (2, (30, 40)); -SELECT (p).last FROM ct ORDER BY id; -SELECT count(*) OVER w FROM ct +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; +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); -SELECT count(*) OVER w FROM ct +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); -- Navigation offset must not contain a navigation operation SELECT id, val - FROM nt + FROM rpr_nav_rows WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A+) @@ -2114,8 +2068,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A B C) DEFINE A AS val > 0, B AS val > 2, C AS val > 4 -) -ORDER BY id; +); -- SKIP PAST LAST ROW @@ -2128,8 +2081,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN (A B C) DEFINE A AS val > 0, B AS val > 2, C AS val > 4 -) -ORDER BY id; +); -- Default behavior (should be SKIP PAST LAST ROW) @@ -2141,8 +2093,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B) DEFINE A AS val > 0, B AS val > 1 -) -ORDER BY id; +); -- Compare default with explicit PAST LAST ROW -- Results should be identical @@ -2186,8 +2137,7 @@ WINDOW w AS ( INITIAL PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); -- Implicit INITIAL (default) SELECT id, val, COUNT(*) OVER w as cnt @@ -2197,8 +2147,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); DROP TABLE rpr_init; @@ -2291,8 +2240,8 @@ CREATE VIEW rpr_permute_v AS SELECT pg_get_viewdef('rpr_permute_v'::regclass); -- Quoted even where no group follows: the deparser quotes the name wherever --- it appears rather than looking ahead for the "(" that would make it --- ambiguous +-- 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 ( @@ -2322,9 +2271,9 @@ DROP TABLE rpr_permute; -- ============================================================ -- Serialization/Deserialization Tests -- ============================================================ --- RPR-defining views and tables here are intentionally left in place (not --- dropped) so that pg_dump/pg_upgrade exercise the deparse-then-re-parse --- round-trip of the RPR window clause. +-- 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 @@ -2542,7 +2491,7 @@ WINDOW w AS (ORDER BY id DEFINE A AS val > 0, B AS val > 0); SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass); --- Quoted identifier round-trip: mixed case and reserved words need quoting +-- Quoted identifier round-trip: mixed-case names need quoting CREATE VIEW rpr_serial_quoted AS SELECT id, val, count(*) OVER w FROM rpr_serial @@ -2564,7 +2513,8 @@ WINDOW w AS (ORDER BY id DEFINE PERMUTE AS val > 0, A AS val > 10, B AS val > 20); SELECT pg_get_viewdef('rpr_serial_permute'::regclass); --- Inline OVER round-trip: inline window spec (no WINDOW alias) deparses inside OVER (...) +-- 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 @@ -2576,7 +2526,7 @@ SELECT pg_get_viewdef('rpr_serial_inline_over'::regclass); -- Multi-relation view: a DEFINE column is deparsed with no qualifier, so it -- must stay unambiguous across the join for the view to re-parse. This one --- is left in place like the rest of the section, which is what puts an +-- 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); @@ -2689,8 +2639,18 @@ ALTER TABLE rpr_pin_j2 ADD COLUMN price INT; SELECT pg_get_viewdef('rpr_pin_on_v'::regclass, true); SELECT * FROM rpr_pin_on_v ORDER BY id; --- 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. +-- 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); + +-- 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); @@ -3023,13 +2983,16 @@ 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. +-- 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 ROW(1)::rpr_res_ct $$; + 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 @@ -3054,6 +3017,97 @@ 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); +SELECT 'CREATE VIEW rpr_cov_inner_rt AS ' + || pg_get_viewdef('rpr_cov_inner_v'::regclass, true) \gexec +SELECT pg_get_viewdef('rpr_cov_inner_v'::regclass, true) + = pg_get_viewdef('rpr_cov_inner_rt'::regclass, true) AS round_trips; +SELECT * FROM rpr_cov_inner_v ORDER BY idp1; +SELECT * FROM rpr_cov_inner_rt ORDER BY idp1; + +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); +SELECT 'CREATE VIEW rpr_cov_sys2_rt AS ' + || pg_get_viewdef('rpr_cov_sys2_v'::regclass, true) \gexec +SELECT pg_get_viewdef('rpr_cov_sys2_v'::regclass, true) + = pg_get_viewdef('rpr_cov_sys2_rt'::regclass, true) AS round_trips; + +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); +SELECT 'CREATE VIEW rpr_cov_coldef_rt AS ' + || pg_get_viewdef('rpr_cov_coldef_v'::regclass, true) \gexec +SELECT pg_get_viewdef('rpr_cov_coldef_v'::regclass, true) + = pg_get_viewdef('rpr_cov_coldef_rt'::regclass, true) AS round_trips; +SELECT * FROM rpr_cov_coldef_v; +SELECT * FROM rpr_cov_coldef_rt; + +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); +SELECT 'CREATE VIEW rpr_cov_merge_rt AS ' + || pg_get_viewdef('rpr_cov_merge_v'::regclass, true) \gexec +SELECT pg_get_viewdef('rpr_cov_merge_v'::regclass, true) + = pg_get_viewdef('rpr_cov_merge_rt'::regclass, true) AS round_trips; +SELECT * FROM rpr_cov_merge_v ORDER BY id; +SELECT * FROM rpr_cov_merge_rt ORDER BY id; + +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 @@ -3367,11 +3421,11 @@ SELECT * FROM rpr_res_fa_rt; 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 anonymous 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 would have moved to is --- kept clear as well. +-- 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); @@ -3565,8 +3619,8 @@ SELECT pg_get_viewdef('rpr_cds_null_v'::regclass, true) SELECT * FROM rpr_cds_null_v ORDER BY idp1; SELECT * FROM rpr_cds_null_rt ORDER BY idp1; --- and the same nesting where the join above nulls nothing, which the exact --- match takes +-- 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 @@ -3679,15 +3733,6 @@ SELECT * FROM rpr_pvar_rt; DROP VIEW rpr_pvar_rt, rpr_pvar_v; DROP TABLE rpr_pvar_a, rpr_pvar_b; --- The same query written fresh is rejected, since nothing pins the name for --- it. Pinning is what lets the stored definition above 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); - -- Materialized view (if supported) @@ -3745,7 +3790,7 @@ DROP TABLE rpr_ctas_result; DROP TABLE rpr_insert_target; DROP TABLE rpr_ctas; --- Prepared statements (tests outfuncs.c / readfuncs.c) +-- 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); @@ -3759,8 +3804,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); EXECUTE rpr_prep_simple; EXECUTE rpr_prep_simple; @@ -3777,8 +3821,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 10 -) -ORDER BY id; +); EXECUTE rpr_prep_param(2); EXECUTE rpr_prep_param(3); @@ -3798,8 +3841,7 @@ WINDOW w AS ( A AS val > 5, B AS val > 15, C AS val <= 15 -) -ORDER BY id; +); EXECUTE rpr_prep_complex; EXECUTE rpr_prep_complex; @@ -3826,7 +3868,7 @@ WITH rpr_cte AS ( ) SELECT * FROM rpr_cte ORDER BY id; --- CTE with multiple references (forces node copy) +-- 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 @@ -3854,8 +3896,7 @@ FROM ( DEFINE A AS val > 10, B AS val > 20 ) ) sub -WHERE cnt > 0 -ORDER BY id; +WHERE cnt > 0; -- Nested subqueries SELECT * @@ -3872,8 +3913,7 @@ FROM ( ) ) inner_sub WHERE cnt > 0 -) outer_sub -ORDER BY id; +) outer_sub; DROP TABLE rpr_copy; @@ -4048,8 +4088,9 @@ WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTE 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'; 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. +-- 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), @@ -4096,7 +4137,7 @@ WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTE SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_struct'), E'\n')) AS line WHERE line ~ 'PATTERN'; DROP VIEW rpr_dp_struct; -- Execution semantics (deparse cannot show reluctant shortest-match). The --- rpr_glue rows -- an A-run followed by B rows -- show when the '|B' +-- 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 "+" @@ -4109,8 +4150,7 @@ FROM rpr_glue WINDOW gs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0), rs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0), gp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0), - rp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0) -ORDER BY id; + rp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0); -- 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 @@ -4118,7 +4158,8 @@ ORDER BY id; 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); 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); 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); --- the dangling operator is blamed on the element it hangs off, not on the first +-- 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); 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); 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); @@ -4214,7 +4255,8 @@ WINDOW w AS ( -- Qualified column references (NOT SUPPORTED) --- Pattern variable qualified name: not supported (valid per ISO/IEC 19075-5 6.15 / 4.16, not yet implemented) +-- 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 ( @@ -4224,7 +4266,8 @@ WINDOW w AS ( DEFINE A AS A.val > 0 ); --- PATTERN-only variable qualified name: not supported even without DEFINE entry +-- PATTERN-only variable qualified name: +-- not supported even without DEFINE entry SELECT COUNT(*) OVER w FROM rpr_err WINDOW w AS ( @@ -4244,7 +4287,8 @@ WINDOW w AS ( 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) +-- 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 ( @@ -4254,8 +4298,9 @@ WINDOW w AS ( DEFINE A AS rpr_err.val > 0 ); --- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check --- must fall through so that normal column resolution produces a sensible error. +-- 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 ( @@ -4292,7 +4337,8 @@ WINDOW w AS ( DEFINE A AS (items).amount > 10 ); --- Composite type field selection (qualified forms): the ColumnRef portion ("A.items" or +-- 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 @@ -4346,9 +4392,10 @@ WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING DROP TABLE rpr_ordrow; -- The same split by way of a pulled-up composite target, both as a plain --- subquery and as a view. +-- 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), (2, 2), (3, 3); +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 @@ -4439,8 +4486,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0, B AS val > 5, C AS val > 10 -) -ORDER BY id; +); DROP TABLE rpr_err; @@ -4457,8 +4503,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 15 -) -ORDER BY id; +); -- IS NULL in DEFINE SELECT id, val, COUNT(*) OVER w as cnt @@ -4468,8 +4513,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (N+) DEFINE N AS val IS NULL -) -ORDER BY id; +); -- IS NOT NULL in DEFINE SELECT id, val, COUNT(*) OVER w as cnt @@ -4479,8 +4523,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (NN+) DEFINE NN AS val IS NOT NULL -) -ORDER BY id; +); DROP TABLE rpr_null; @@ -4566,7 +4609,8 @@ 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); --- An unknown literal argument resolves to text; it must still reference a column +-- 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 @@ -4639,7 +4683,8 @@ 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); --- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged +-- 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 @@ -4658,7 +4703,8 @@ 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); --- Consecutive GROUP merge at the boundary: (A B){1073741823,} (A B){1073741823,} +-- 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). @@ -4746,7 +4792,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A{2}){2,3}) DEFINE A AS val > 0); -- Quantifier NO multiply: (A{2}){2,} stays as (a{2}){2,} --- outer unbounded - gaps would occur (4,6,8,... not 4,5,6,...), no optimization +-- 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 @@ -4820,14 +4867,14 @@ 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); --- (A+){2,4} -> a{2,} (outer range, unbounded child: every interval reaches INF, --- so they always touch) +-- (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); --- (A{2,3}){2,4} stays nested for the same reason, even though the counts +-- (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 @@ -4835,15 +4882,16 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A{2,3}){2,4}) DEFINE A AS val > 0); -- 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) +-- 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); -- 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) +-- 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 @@ -4875,13 +4923,13 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A A (B B)+ B B C C C) DEFINE A AS val <= 20, B AS val > 20 AND val <= 70, C AS val > 70); --- Consecutive GROUP merge with unbounded: (A+) (A+) -> a{2,} +-- 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); --- Consecutive GROUP merge finite: (A{10}){20} -> a{200} +-- 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 @@ -4914,7 +4962,7 @@ SELECT COUNT(*) OVER w FROM rpr_plan WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A B)+ A B) DEFINE A AS val <= 50, B AS val > 50); --- Multiple SUFFIX absorption with skipUntil: (A B)+ A B A B C +-- 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 @@ -4945,7 +4993,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B* (A B*)+) DEFINE A AS val <= 50, B AS val > 50); --- PREFIX merge with multiple quantifiers: A+ B* C? (A+ B* C?)+ -> (a+ b* c?){2,} +-- 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 @@ -4983,8 +5032,8 @@ 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); --- Reluctant optimization bypass: GROUP merge --- (A B)+? (A B) stays separate (greedy merges to (a b){2,}) +-- 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 @@ -5052,7 +5101,8 @@ 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); -- Reluctant preserved through ALT flatten --- (A | (B | C))+? flattens to (a | b | c)+? - inner ALT flattened, reluctant kept +-- (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 @@ -5098,7 +5148,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING DEFINE A AS val <= 50, B AS val > 50); -- Unwrap single-item ALT after dedup: (A | A)+ -> a+ --- ALT dedup reduces to single-item, then GROUP unwrap +-- 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 @@ -5185,7 +5235,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ | B)+) DEFINE A AS val <= 50, B AS val > 50); --- ALT inside unbounded GROUP: (A+ B | A B)* -> (a+# b | a b)* (first iteration absorbable) +-- 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 @@ -5234,7 +5285,8 @@ SELECT COUNT(*) OVER w FROM rpr_plan WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW PATTERN (A B+) DEFINE A AS val <= 50, B AS val > 50); --- Non-absorbable (no unbounded branch): (A | B){2,} -> (a | b){2,} (no markers) +-- 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 @@ -5294,8 +5346,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B) DEFINE A AS val <= 50, B AS val > 50 -) -ORDER BY id; +); -- Absorbable GROUP Pattern: (A B)+ C -- Pattern starts with unbounded GROUP @@ -5308,8 +5359,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B)+ C) DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 -) -ORDER BY id; +); -- Non-Absorbable: Unbounded Not at Start -- Pattern: A B+ (unbounded not at start) @@ -5322,8 +5372,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN (A B+) DEFINE A AS val <= 50, B AS val > 50 -) -ORDER BY id; +); -- ALT with Absorbable Branches -- Pattern: (A+ | B+) C - both branches absorbable @@ -5336,11 +5385,10 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ | B+) C) DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 -) -ORDER BY id; +); -- ALT with Mixed Branches --- Pattern: (A+ | B C) - only first branch absorbable +-- Pattern: (A+ | B C)+ - only first branch absorbable SELECT id, val, COUNT(*) OVER w as cnt FROM rpr_plan @@ -5350,8 +5398,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ | B C)+) DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 -) -ORDER BY id; +); -- Non-Absorbable: ALT Inside GROUP -- Pattern: (A | B){2,} - ALT inside unbounded GROUP @@ -5364,11 +5411,10 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN ((A | B){2,}) DEFINE A AS val <= 50, B AS val > 50 -) -ORDER BY id; +); --- Non-Absorbable: Nested Unbounded --- Pattern: ((A B)+ C)+ - nested GROUP structure +-- 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 @@ -5378,8 +5424,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN (((A B)+ C)+) DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 -) -ORDER BY id; +); -- Non-Absorbable: Unbounded Element Inside GROUP -- Pattern: (A B+){2,} - unbounded inside GROUP @@ -5392,8 +5437,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B+){2,}) DEFINE A AS val <= 50, B AS val > 50 -) -ORDER BY id; +); -- Runtime Conditions: SKIP TO NEXT ROW -- Absorption disabled with SKIP TO NEXT ROW @@ -5406,8 +5450,7 @@ WINDOW w AS ( AFTER MATCH SKIP TO NEXT ROW PATTERN (A+ B) DEFINE A AS val <= 50, B AS val > 50 -) -ORDER BY id; +); -- Runtime Conditions: Limited Frame -- Absorption disabled with limited frame end @@ -5420,8 +5463,7 @@ WINDOW w AS ( AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B) DEFINE A AS val <= 50, B AS val > 50 -) -ORDER BY id; +); -- ALT Non-Absorbable Branch Match: A+ B | C -- C match on the non-absorbable branch (id=2, id=5) must survive absorption of @@ -5452,10 +5494,12 @@ WINDOW w AS ( C AS 'C' = ANY(flags) ); --- Measuring a group body for absorbability. The optimizer only measures a --- body an unbounded quantifier wraps, so each pattern below puts the shape --- under test inside one. None of the three can match real rows; the point is --- that the measurement reports "not a fixed length" instead of overflowing. +-- 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 @@ -5492,7 +5536,8 @@ WINDOW w AS ( -- 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 +-- a finalized B+ match on the other branch +-- (id=1, id=6) must survive absorption WITH test_absorbable_branches AS ( SELECT * FROM (VALUES @@ -5537,8 +5582,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*) DEFINE A AS val > 1000 -- Never matches -) -ORDER BY id; +); -- All Rows Match -- Pattern where every row matches @@ -5550,8 +5594,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val >= 0 -- Always true -) -ORDER BY id; +); -- Large Quantifiers -- Pattern: A{100} (large exact quantifier) @@ -5563,8 +5606,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{100}) DEFINE A AS val > 0 -) -ORDER BY id; +); -- Pattern: A{10,20} (large range quantifier) SELECT id, val, COUNT(*) OVER w as cnt @@ -5574,8 +5616,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{10,20}) DEFINE A AS val > 0 -) -ORDER BY id; +); -- Complex Multi-Level Nesting -- Pattern: (((A B) | C)+ D)+ @@ -5588,8 +5629,7 @@ WINDOW w AS ( PATTERN ((((A B) | C)+ D)+) DEFINE A AS val <= 20, B AS val > 20 AND val <= 40, C AS val > 40 AND val <= 60, D AS val > 60 -) -ORDER BY id; +); -- Long Alternation Chain -- Pattern: A | B | C | D | E (5-way ALT) @@ -5602,8 +5642,7 @@ WINDOW w AS ( PATTERN (A | B | C | D | E) DEFINE A AS val = 10, B AS val = 30, C AS val = 50, D AS val = 70, E AS val = 90 -) -ORDER BY id; +); -- Long Sequence -- Pattern: A B C D E F G H (8-element SEQ) @@ -5617,8 +5656,7 @@ WINDOW w AS ( DEFINE A AS val >= 10, B AS val >= 20, C AS val >= 30, D AS val >= 40, E AS val >= 50, F AS val >= 60, G AS val >= 70, H AS val >= 80 -) -ORDER BY id; +); -- Interleaved Quantifiers -- Pattern: A{2} B+ C{3,5} D* E{1,} @@ -5631,8 +5669,7 @@ WINDOW w AS ( PATTERN (A{2} B+ C{3,5} D* E{1,}) DEFINE A AS val > 0, B AS val > 0, C AS val > 0, D AS val > 0, E AS val > 0 -) -ORDER BY id; +); -- ============================================================ -- Optimization Fallback Tests @@ -5662,7 +5699,8 @@ WINDOW w AS ( DEFINE A AS val > 0 ); --- Max quantifier exceeds valid range (2147483647 = INT_MAX, limit is 2147483646) +-- Max quantifier exceeds valid range +-- (2147483647 = INT_MAX, limit is 2147483646) EXPLAIN (COSTS OFF) SELECT COUNT(*) OVER w FROM rpr_fallback WINDOW w AS ( @@ -5866,8 +5904,7 @@ w2 AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (B+) DEFINE B AS val >= 40 -) -ORDER BY id; +); -- Window Function with PARTITION BY @@ -5880,8 +5917,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY category, id; +); -- Window Function with Complex ORDER BY @@ -5893,8 +5929,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY category DESC, val ASC; +); -- Named Window Reference @@ -5906,8 +5941,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); -- Inline Window Definition @@ -5918,8 +5952,7 @@ SELECT id, category, val, PATTERN (A+) DEFINE A AS val > 0 ) as cnt -FROM rpr_planner -ORDER BY id; +FROM rpr_planner; -- ============================================================ -- Subquery and CTE Tests @@ -5939,8 +5972,7 @@ SELECT * FROM ( DEFINE A AS val > 0 ) ) sub -WHERE cnt > 5 -ORDER BY id; +WHERE cnt > 5; -- RPR with Subquery in WHERE @@ -5953,8 +5985,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 50 -) -ORDER BY id; +); -- CTE with RPR @@ -6029,8 +6060,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val1 + val2 > 100 -) -ORDER BY t1.id; +); -- RPR After LEFT JOIN @@ -6043,8 +6073,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val1 > 0 -) -ORDER BY t1.id; +); -- RPR with Multiple Tables in DEFINE @@ -6058,8 +6087,7 @@ WINDOW w AS ( PATTERN (A+ B) DEFINE A AS val1 > 20, B AS val2 > 200 -) -ORDER BY t1.id; +); -- RPR After Cross Join @@ -6073,8 +6101,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val1 + val2 > 0 -) -ORDER BY t1.id, t2.id; +); -- Self-Join with RPR @@ -6088,8 +6115,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (X+) DEFINE X AS val1 < val1_next -) -ORDER BY id; +); DROP TABLE rpr_join1, rpr_join2; @@ -6238,8 +6264,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); -- CASE Expression in Target List @@ -6256,8 +6281,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); -- Subquery in Target List @@ -6270,8 +6294,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); -- Function Calls in Target List @@ -6285,8 +6308,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); -- Column Aliases and References @@ -6299,8 +6321,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY row_id; +); DROP TABLE rpr_target; @@ -6424,8 +6445,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS COUNT(*) > 0 -) -ORDER BY category; +); -- RPR with HAVING (same aggregate-in-DEFINE error) @@ -6440,8 +6460,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS COUNT(*) > 0 -) -ORDER BY category; +); -- RPR with DISTINCT @@ -6678,7 +6697,7 @@ SELECT * FROM rpr_grp_v ORDER BY category; 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 unknown, so the total row +-- 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 @@ -6783,10 +6802,10 @@ SELECT pg_get_viewdef('rpr_grp_v2'::regclass, true); SELECT * FROM rpr_grp_v2 ORDER BY category NULLS LAST; DROP VIEW rpr_grp_v2; --- A DEFINE clause may spell a GROUP BY expression. Planting stops at one --- rather than offering the columns underneath it to the grouping logic on --- their own, which is not how grouping makes them available; the target list --- entry holding the same expression is what both copies end up naming. +-- 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 @@ -6797,6 +6816,25 @@ WINDOW w AS ( DEFINE A AS val + 1 > 0) ORDER BY bumped; +-- 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; +SELECT last_value = (SELECT count(*) FROM rpr_grp) AS once_per_row +FROM rpr_grp_seq; +DROP SEQUENCE rpr_grp_seq; + -- The same for a function call SELECT upper(category) AS u, count(*) OVER w AS cnt FROM rpr_grp @@ -6841,10 +6879,11 @@ WINDOW w AS ( DEFINE A AS val + 1 > 0); -- A DEFINE clause may repeat an expression the window itself orders by, with --- no grouping in sight. Planting bare Vars 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 already in --- the input. +-- 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 ( @@ -6893,18 +6932,20 @@ WINDOW w1 AS (ORDER BY category), PATTERN (A) DEFINE A AS category IS NOT NULL); --- An unreferenced window is substituted like any other +-- 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 category IS NOT NULL); + DEFINE A AS val > 0); --- A join turns the DEFINE clause's Vars into join alias Vars. Plain --- grouping still resolves them, so the column USING merges reaches the --- pattern unharmed. +-- 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 @@ -6952,6 +6993,53 @@ WINDOW w AS ( PATTERN (A) DEFINE A AS true); +-- 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; + +-- 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); +SELECT * FROM rpr_fjcoal_v ORDER BY 1; + +-- 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; + +DROP VIEW rpr_fjcoal_v2; +DROP VIEW rpr_fjcoal_v; + DROP TABLE rpr_grp; DROP TABLE rpr_sort; @@ -7047,8 +7135,7 @@ w3 AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (C+) DEFINE C AS val > 100 -) -ORDER BY id; +); -- Deeply Nested Subqueries with RPR @@ -7067,8 +7154,7 @@ SELECT * FROM ( ) sub1 ) sub2 ) sub3 -WHERE cnt > 10 -ORDER BY id; +WHERE cnt > 10; -- Complex Expression in DEFINE Clause @@ -7081,8 +7167,7 @@ WINDOW w AS ( PATTERN (A+ B) DEFINE A AS (val % 3 = 0 OR val % 5 = 0), B AS (val * 2 > 100 AND val / 2 < 100) -) -ORDER BY id; +); -- Window with No Matching Rows @@ -7095,8 +7180,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); -- Window on Single Row @@ -7109,15 +7193,14 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+) DEFINE A AS val > 0 -) -ORDER BY id; +); DROP TABLE rpr_stress; -- ============================================================ -- Error Limit Tests -- ============================================================ --- Tests for error conditions in rpr.c +-- 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); @@ -7132,94 +7215,33 @@ WINDOW w AS ( B AS TRUE ); --- 240 variables in PATTERN and DEFINE (boundary - should succeed) -SELECT COUNT(*) OVER w FROM rpr_errors -WINDOW w AS ( - ORDER BY id - ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 - V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 - V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 - V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 - V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 - V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 - V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 - V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 - V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 - V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 - V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 - V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240) - DEFINE - V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0, - V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0, - V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0, - V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0, - V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0, - V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0, - V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0, - V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0, - V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0, - V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0, - V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0, - V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0, - V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0, - V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0, - V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0, - V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0, - V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0, - V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0, - V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0, - V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0, - V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0, - V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0, - V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0, - V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0 -); - --- ERROR: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE) -SELECT COUNT(*) OVER w FROM rpr_errors -WINDOW w AS ( - ORDER BY id - ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 - V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 - V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 - V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 - V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 - V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 - V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 - V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 - V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 - V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 - V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 - V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 - V241) - DEFINE - V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0, - V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0, - V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0, - V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0, - V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0, - V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0, - V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0, - V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0, - V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0, - V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0, - V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0, - V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0, - V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0, - V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0, - V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0, - V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0, - V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0, - V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0, - V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0, - V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0, - V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0, - V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0, - V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0, - V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0 -); +-- 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 +SELECT format($$SELECT COUNT(*) OVER w FROM rpr_errors + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (%s) DEFINE %s)$$, + (SELECT string_agg('V' || i, ' ' ORDER BY i) + FROM generate_series(1, 240) i), + (SELECT string_agg('V' || i || ' AS val > 0', ', ' ORDER BY i) + FROM generate_series(1, 240) i)) \gexec +SELECT format($$SELECT COUNT(*) OVER w FROM rpr_errors + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (%s) DEFINE %s)$$, + (SELECT string_agg('V' || i, ' ' ORDER BY i) + FROM generate_series(1, 241) i), + (SELECT string_agg('V' || i || ' AS val > 0', ', ' ORDER BY i) + FROM generate_series(1, 240) i)) \gexec +\set ECHO all -- Pattern nesting-depth boundary: 254 levels are accepted, 255 rejected. -- Reluctant quantifiers are not subject to quantifier multiplication, so the diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql index dd9c74f5474..f19150969cb 100644 --- a/src/test/regress/sql/rpr_explain.sql +++ b/src/test/regress/sql/rpr_explain.sql @@ -39,7 +39,8 @@ -- Nav Mark Lookback/Lookahead (tuplestore trim) -- ============================================================ --- Filter function to normalize platform-dependent memory values (not NFA statistics). +-- 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. @@ -206,7 +207,7 @@ WINDOW w AS ( );'); -- Sequential alternations at the same depth --- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)" +-- 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) @@ -532,7 +533,7 @@ WINDOW w AS ( );'); -- Early termination: first ALT branch (A) reaches FIN immediately, --- pruning second branch (A B+) before it can accumulate B repetitions. +-- 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) @@ -650,7 +651,8 @@ WINDOW w AS ( );'); -- Bare unbounded quantifier: A+ absorbs redundant contexts --- min=1 commits no match until the run ends, so newer contexts absorb in-progress +-- 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) @@ -673,7 +675,8 @@ WINDOW w AS ( );'); -- Bare min=0 quantifier: A* is skipped, not absorbed --- min=0 commits an empty match at creation, so SKIP (not absorption) removes them +-- 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) @@ -901,8 +904,8 @@ WINDOW w AS ( -- Absorption preserved when DEFINE uses only LAST without offset -- LAST(v) is match_start-independent (always currentpos), so absorption --- remains active. Compare: absorbed count should be >0, like the --- PREV-only case above. +-- 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) @@ -1072,8 +1075,8 @@ WINDOW w AS ( );'); -- 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) +-- 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']), @@ -1364,7 +1367,8 @@ WINDOW w AS ( )'); -- JSON format with skipped context statistics --- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped +-- 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) @@ -1605,7 +1609,8 @@ WINDOW w AS ( DEFINE A AS FALSE );'); --- (A?){2,3}: min=2 (ISO/IEC 19075-5 7.2.8 STR06 = STRE STRE) -> 3 length-0 matches +-- (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) @@ -2399,7 +2404,8 @@ WINDOW w AS ( );'); -- Nested ALT at start of branch inside outer ALT --- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element +-- 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) @@ -2823,8 +2829,10 @@ WINDOW w AS ( D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 );'); --- 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 +-- 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) @@ -2847,7 +2855,7 @@ WINDOW w AS ( );'); -- Three-deep stack whose innermost branch is a quantified group: the group's --- skip-target jump must not be mistaken for a branch separator at any depth +-- 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 @@ -2943,7 +2951,8 @@ WINDOW w AS ( -- 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 +-- 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) @@ -3437,9 +3446,11 @@ WINDOW w AS ( -- ============================================================ -- Nav Mark Lookback/Lookahead Tests --- Verifies planner-computed navigation offsets for tuplestore trim. --- Lookback: how far back from currentpos (PREV, LAST, compound PREV_LAST/NEXT_LAST). --- Lookahead: how far forward from match_start (FIRST, compound PREV_FIRST/NEXT_FIRST). +-- 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 @@ -3582,7 +3593,8 @@ WINDOW w AS ( DEFINE A AS LAST(v) > PREV(v) ); --- Compound PREV(FIRST(val, 1), 2): lookback from match_start, firstOffset = 1-2 = -1 +-- 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 ( @@ -3773,8 +3785,8 @@ WINDOW w AS ( ); -- Compound PREV(LAST(val, $1), $2): parameter lookback overflow -> retain all --- EXPLAIN shows "runtime" (plan-level); EXPLAIN ANALYZE shows "retain all" --- (executor-resolved). +-- 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) @@ -3825,7 +3837,8 @@ DEALLOCATE p_first_runtime; -- PREV(v) + PREV(v, $1): the implicit lookback of 1 has to count even when the -- explicit offset resolves to 0, or PREV(v) would fail with "cannot fetch row --- before mark". A generic plan settles the reach per scan instead of at init. +-- 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 @@ -3949,8 +3962,9 @@ 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 +-- 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); diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql index 36c8a6a0fe8..6e51eb2d26d 100644 --- a/src/test/regress/sql/rpr_integration.sql +++ b/src/test/regress/sql/rpr_integration.sql @@ -15,7 +15,7 @@ -- 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) +-- 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 @@ -62,7 +62,8 @@ INSERT INTO rpr_integ VALUES -- PRECEDING, breaking RPR's required ROWS BETWEEN CURRENT ROW AND -- UNBOUNDED FOLLOWING. --- Non-RPR baseline: the planner rewrites the frame to ROWS UNBOUNDED PRECEDING. +-- 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 @@ -126,8 +127,7 @@ SELECT * FROM ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B+) DEFINE B AS val > PREV(val)) -) t WHERE cnt > 0 -ORDER BY id; +) t WHERE cnt > 0; -- ============================================================ -- A3. Window dedup prevention (RPR vs non-RPR) @@ -135,11 +135,11 @@ ORDER BY id; -- Verify that PostgreSQL does not merge an RPR window with a non-RPR -- window even when both share the same ORDER BY and frame -- specification. RPR pattern matching produces results that are --- semantically different from a plain frame-based aggregate, so the --- two windows must remain as separate WindowAgg nodes. Inline window --- specs are used throughout this section because only inline windows --- are subject to the dedup path; distinct named windows are always --- kept separate regardless of equivalence. +-- 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 @@ -174,8 +174,7 @@ SELECT DEFINE B AS val > PREV(val)) AS rpr_cnt, count(*) OVER (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS normal_cnt -FROM rpr_integ -ORDER BY id; +FROM rpr_integ; -- 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. @@ -194,8 +193,8 @@ WINDOW w1 AS ( ); -- The two windows above start from the same frame. These two do not: --- they converge only after frame optimization rewrites the non-RPR one, --- and the RPR window is preserved, so they still must not be merged. +-- 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. @@ -216,14 +215,17 @@ WINDOW EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed; -- ============================================================ --- A4. Window dedup prevention (same PATTERN, different DEFINE) +-- 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 have different DEFINE --- conditions. Even though the ORDER BY, frame, and PATTERN coincide, --- the differing DEFINE expressions classify rows differently and --- must therefore yield two separate WindowAgg nodes. Inline specs --- are used here because dedup only applies to inline windows. +-- 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 @@ -267,8 +269,41 @@ SELECT ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B+) DEFINE B AS val < PREV(val)) AS cnt_down -FROM rpr_integ -ORDER BY id; +FROM rpr_integ; + +-- 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; + +-- 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; -- ============================================================ -- A5. Unused output removal around an RPR window @@ -335,8 +370,11 @@ SELECT count(*), sum(c) FROM ( ) t; -- "val" is a non-resjunk subquery output that the outer query never reads, so --- remove_unused_subquery_outputs() would replace it with NULL and DEFINE would --- then compare NULLs. The guard in allpaths.c keeps it. +-- 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 @@ -375,9 +413,9 @@ WINDOW w AS (ORDER BY id PATTERN (A B+) DEFINE B AS val > PREV(val)); --- The same retention has to survive join removal: nulling "uv" would leave --- rpr_integ_u referenced by nothing, the LEFT JOIN would be dropped, and the --- DEFINE Var would then point at a relation no longer in the plan. +-- 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; @@ -401,8 +439,9 @@ SELECT id, c FROM ( ) s ORDER BY id; -- A flattened subquery output that an outer join makes nullable reaches the --- DEFINE clause as a PlaceHolderVar rather than a Var. The parser's targetlist --- entry is rewritten the same way, so the expression still 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 @@ -428,10 +467,10 @@ WINDOW w AS (ORDER BY t.id DEFINE B AS uv1 > PREV(uv1)); -- The same shape with the window dead: nothing reads count(*) OVER w, so its --- entry goes, w goes with it, and "uv" is no longer held by a DEFINE clause --- that will run. That was rpr_integ_u's last reference, so join removal takes --- the LEFT JOIN too and the scan is left alone. Retaining "uv" here on the --- strength of a window that will not run would keep the join alive for nothing. +-- 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 @@ -479,9 +518,11 @@ SELECT id, c1 FROM ( 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. Its DEFINE must not keep "val" alive --- for a window that never runs: the subquery output for val becomes a null Const. +-- 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 @@ -493,12 +534,12 @@ SELECT c FROM ( DEFINE B AS val > PREV(val)) ) t; --- Here w2 does have a window function, but the outer query does not read it, so --- this call replaces that entry with a null Const and w2 goes inactive as well. --- Which windows are active therefore has to be read after that substitution: --- read before it, w2 still looks active and "val" is retained for a window that --- will not run. Both null Consts on the WindowAgg's Output line are the --- assertion. +-- 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 @@ -511,11 +552,9 @@ SELECT c FROM ( ) t; -- The same shape with the window function one level down, inside an --- expression. The live set is read off the entries that survive, so a --- window function nested in one of them is seen and one in an entry about to --- be replaced is not; reading it from the entries' top-level nodes instead --- would report w2 live here and hold "val" for a window that goes inactive --- anyway. This plan matching the one above is the assertion. +-- 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 @@ -542,7 +581,8 @@ 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 w2 keeps its own. +-- 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 @@ -559,9 +599,9 @@ SELECT c2 FROM ( -- 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 its DEFINE column is retained. The pass that settles the window --- function entries therefore has to apply every condition the loop after it --- applies, not just the one about the upper query. +-- 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 @@ -589,10 +629,10 @@ SELECT sum(c) FROM ( -- 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. +-- 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 @@ -612,33 +652,34 @@ SELECT sum(c) FROM ( DEFINE B AS r IS NOT NULL) ) t; --- The walk that decides which windows are still live runs on a targetlist --- subquery_planner() has not preprocessed yet, so a SubLink is still a SubLink --- there. OFFSET 0 keeps the subquery unflattened, which is what puts --- remove_unused_subquery_outputs() on the path at all. +-- 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, (SELECT 1) AS s, count(*) OVER w AS c + 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; +) t WHERE m; --- 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 walk reads it --- there; a window function written inside the sub-select itself would count --- against that query's own window clauses and must not be read here. SELECT count(*) FROM ( - SELECT id, (count(*) OVER w) IN (SELECT 1) AS m + 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; +) t WHERE m; -- ============================================================ -- A6. Inverse transition bypass @@ -714,7 +755,7 @@ DROP FUNCTION rpr_logging_minvfunc(text, anyelement); -- cost_windowagg() must account for DEFINE expression evaluation cost. -- Verify RPR WindowAgg cost > non-RPR WindowAgg cost. -CREATE FUNCTION get_windowagg_cost(query text) RETURNS numeric AS $$ +CREATE FUNCTION rpr_get_windowagg_cost(query text) RETURNS numeric AS $$ DECLARE plan json; cost numeric; @@ -725,17 +766,17 @@ BEGIN END; $$ LANGUAGE plpgsql; -SELECT get_windowagg_cost( +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))') > - get_windowagg_cost( + 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; -DROP FUNCTION get_windowagg_cost(text); +DROP FUNCTION rpr_get_windowagg_cost(text); -- ============================================================ -- A8. Subquery flattening prevention @@ -762,13 +803,12 @@ WHERE cnt > 0; -- ============================================================ -- Verify that DEFINE expressions are not propagated into the -- targetlist of any upper WindowAgg node. Only the column references --- consumed by DEFINE should be passed up; the full DEFINE expression --- is meaningful only inside the RPR WindowAgg that owns it. --- EXPLAIN VERBOSE is therefore expected to show a clean targetlist on --- the outer WindowAgg, with no DEFINE-derived expression leaking in. --- Note: columns referenced by DEFINE (e.g., "val") may appear as --- resjunk entries in upper WindowAgg targetlists -- but that is harmless. --- The claim here is limited to the full DEFINE boolean expression. +-- 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, @@ -1044,8 +1084,8 @@ SET plan_cache_mode = force_generic_plan; EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1); EXECUTE rpr_prev(1); --- Negative runtime nav offset under the generic plan: init clamps it to 0 for --- trim sizing, but the per-row navigation rejects the negative offset. +-- 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); RESET plan_cache_mode; @@ -1147,15 +1187,29 @@ ORDER BY o.id, r.id; -- A lateral outer reference can share varno and varattno with a DEFINE-only -- column: here o.b and y are both attribute 2 at their own query levels. --- Only varlevelsup separates them, so the window input target has to take y --- even though a Var with the same varno and varattno is present. +-- 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. -SELECT * +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; + +SELECT o.a, s.c FROM rpr_lat_o o, LATERAL ( SELECT o.b AS lat, count(*) OVER w AS c @@ -1167,8 +1221,20 @@ LATERAL ( ) s; -- Control: the outer reference is o.a, attribute 1, which cannot be mistaken --- for y. The counts must match the query above. -SELECT * +-- 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; + +SELECT o.a, s.c FROM rpr_lat_o o, LATERAL ( SELECT o.a AS lat, count(*) OVER w AS c @@ -1253,8 +1319,8 @@ DROP INDEX rpr_integ_id_idx; -- B9. RPR + Volatile function in DEFINE -- ============================================================ -- Volatile functions in DEFINE are rejected in the planner. Under --- RPR's NFA engine the same row's DEFINE predicate may be evaluated --- multiple times (backtracking, PREV/NEXT navigation), so a volatile +-- 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. @@ -1333,7 +1399,8 @@ CREATE TABLE rpr_over2 (c int); INSERT INTO rpr_over1 VALUES (1),(2),(3); INSERT INTO rpr_over2 VALUES (1),(2),(3); --- Plan: only the DEFINE column survives in the subquery output. +-- 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 @@ -1343,13 +1410,13 @@ SELECT cnt FROM ( ) s; DROP TABLE rpr_over1, rpr_over2; --- A DEFINE clause can hold a Var, or a PlaceHolderVar, of an outer query --- level by the time this pruning runs, 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. Reading the clause has to pass those by. --- Each query below prunes an output, and is followed by the same query --- reading that output, which prunes nothing and so never meets them. +-- 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); @@ -1367,8 +1434,9 @@ GROUP BY d.k ORDER BY 1; 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; --- the same, in a clause that does read it: the column has to be held for the --- window even though nothing above the subquery reads it. +-- 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 @@ -1403,10 +1471,11 @@ 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. +-- (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 $$ @@ -1420,7 +1489,7 @@ $$ LANGUAGE sql STABLE; 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; --- Each outer row yields its own offset (9, 8, 7), not one frozen value. +-- 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; @@ -1445,9 +1514,9 @@ SELECT g.n, max(s) AS m FROM (VALUES (0), (1), (2)) g(n), LATERAL rpr_srf_first( GROUP BY g.n ORDER BY g.n; DROP FUNCTION rpr_srf_first(int); --- A compound navigation's OUTER offset must be re-resolved per scan --- as well. The last offset overflows int64, so that scan's navigation --- has no target row at all. +-- 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 @@ -1493,10 +1562,11 @@ DROP TABLE rpr_hcache_thr, rpr_hcache_stock; -- ============================================================ -- B14. RPR + Multiple window definitions -- ============================================================ --- A DEFINE-only column and a later window's sort key both become junk --- targetlist entries. Each draws its resno from p_next_resno, which is what --- keeps the two distinct: a targetlist that gives one resno to two entries is --- not a valid Query, and the parser is the only place that can prevent it. +-- 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 @@ -1516,8 +1586,7 @@ WINDOW w1 AS (ORDER BY id w2 AS (ORDER BY grp) ORDER BY id; --- Control: the opposite declaration order draws the sort key first, so the two --- never compete for a resno. It must return the same rows as the first query +-- 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) diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql index e6d1d9a11ab..e7e3f5429e1 100644 --- a/src/test/regress/sql/rpr_nfa.sql +++ b/src/test/regress/sql/rpr_nfa.sql @@ -160,7 +160,13 @@ WINDOW w AS ( -- 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']), @@ -177,13 +183,15 @@ FROM test_absorbable WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS 'A' = ANY(flags) ); -- 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']), @@ -200,7 +208,7 @@ FROM test_mixed_absorption WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+) | B) DEFINE A AS 'A' = ANY(flags), @@ -208,6 +216,8 @@ WINDOW w AS ( ); -- 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']), @@ -224,7 +234,7 @@ FROM test_state_coverage WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (A{2,} B) DEFINE A AS 'A' = ANY(flags), @@ -232,8 +242,9 @@ WINDOW w AS ( ); -- Reluctant pattern (A+?) - not absorbable --- Compare with greedy A+ above: reluctant excluded from absorption. --- Each context produces minimum match independently. +-- 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']), @@ -250,13 +261,15 @@ FROM test_reluctant_absorption WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+?) DEFINE A AS 'A' = ANY(flags) ); -- 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']), @@ -271,7 +284,7 @@ FROM test_absorb_suffix WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B) DEFINE A AS 'A' = ANY(flags), @@ -279,6 +292,8 @@ WINDOW w AS ( ); -- 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']), @@ -293,7 +308,7 @@ FROM test_absorb_alt WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (B+ C | B+ D) DEFINE B AS 'B' = ANY(flags), @@ -302,6 +317,8 @@ WINDOW w AS ( ); -- 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']), @@ -316,7 +333,7 @@ FROM test_no_absorb WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (A B+) DEFINE A AS 'A' = ANY(flags), @@ -324,6 +341,8 @@ WINDOW w AS ( ); -- 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']), @@ -340,7 +359,7 @@ FROM test_absorb_group WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B) (A B)+) DEFINE A AS 'A' = ANY(flags), @@ -349,9 +368,9 @@ WINDOW w AS ( -- 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. +-- 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']), @@ -546,6 +565,8 @@ WINDOW w AS ( ); -- 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']), @@ -560,7 +581,7 @@ FROM test_multi_unbounded WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B+) DEFINE A AS 'A' = ANY(flags), @@ -672,7 +693,8 @@ WINDOW w AS ( -- Reluctant context lifecycle (A+? B with SKIP TO NEXT ROW) -- A+? exits early but if B not available, falls back to loop. --- Contexts not absorbed (reluctant), so multiple survive. +-- 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']), @@ -889,13 +911,14 @@ WINDOW w AS ( -- 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. +-- the engine must prefer the fast-forward (exit) path when the body +-- prefers the empty match, and suppress longer matches once exit reaches +-- FIN, mirroring the sibling min<=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 @@ -912,8 +935,49 @@ WINDOW gg AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATT rr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+?) DEFINE A AS isa), rr2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??){2,}?) DEFINE A AS isa), ca AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??) DEFINE A AS isa), - cs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?) DEFINE A AS isa) -ORDER BY id; + cs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?) DEFINE A AS isa); + +-- 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); + +-- 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); -- Doubly-nested reluctant nullable group: (((A??){2,}?){2,}?). Reluctant -- quantifiers disable optimizer flattening, so both levels survive and the @@ -927,8 +991,7 @@ WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (((A??){2,}?){2,}?) DEFINE A AS isa -) -ORDER BY id; +); -- 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 @@ -1333,7 +1396,7 @@ WINDOW w AS ( J AS 'J' = ANY(flags) ); --- Reduced frame map reallocation (> 1024 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 @@ -1359,6 +1422,25 @@ FROM ( -- 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 @@ -1429,9 +1511,11 @@ WINDOW w AS ( B AS 'B' = ANY(flags) ); --- Reluctant not absorbed (A+? with SKIP TO NEXT ROW) --- Compare with greedy A+ below: reluctant is not absorbable, --- so all contexts survive independently. +-- 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']), @@ -1448,13 +1532,42 @@ FROM test_reluctant_stats WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+?) DEFINE A AS 'A' = ANY(flags) -); +)$$); + +-- 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) +)$$); --- Absorbed contexts +-- 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']), @@ -1475,15 +1588,66 @@ WINDOW w AS ( PATTERN (A+) DEFINE A AS 'A' = ANY(flags) +)$$); + +-- 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) ); +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) +)$$); --- Skipped contexts (SKIP TO NEXT ROW) +-- 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']), - (3, ARRAY['A']), - (4, ARRAY['B']) -- Completes match starting at row 1 + (2, ARRAY['A', 'B']), + (3, ARRAY['A', 'B', 'C']), + (4, ARRAY['C']) ) AS t(id, flags) ) SELECT id, flags, @@ -1494,11 +1658,37 @@ WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP TO NEXT ROW - PATTERN (A+ B) + PATTERN (A B C) DEFINE A AS 'A' = ANY(flags), - B AS 'B' = ANY(flags) + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) ); +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) +)$$); + +DROP FUNCTION rpr_nfa_counters(text); -- ============================================================ -- Quantifier Runtime Behavior @@ -1944,7 +2134,7 @@ WINDOW w AS ( ); -- Reluctant nullable: A*? (prefers 0 matches) --- A*? always takes skip path (0 iterations preferred) +-- 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']), @@ -2903,8 +3093,8 @@ WINDOW w AS ( B AS 'B' = ANY(flags) ); --- Nested END->END between min/max --- Inner group (A B){1,3} exits between min/max -> outer END count++ +-- ((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']), @@ -2932,8 +3122,9 @@ WINDOW w AS ( B AS 'B' = ANY(flags) ); --- Nested reluctant group ((A B)+?) with following element C --- Inner group exits after minimum 1 iteration +-- 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']), @@ -2980,13 +3171,10 @@ WINDOW w AS ( B AS 'B' = ANY(flags) ); --- Nested END->END fast-forward --- When an inner group has a nullable body and count < min, the --- fast-forward path exits through the outer END, incrementing --- the outer group's count. --- Pattern: ((A?){2,3}){2,3} -- nested groups, neither collapses --- because the optimizer cannot safely multiply non-exact quantifiers. --- Data has no A rows, forcing all-empty iterations via fast-forward. +-- 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']), @@ -3145,7 +3333,8 @@ WINDOW w AS ( -- 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). +-- 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']), @@ -3503,7 +3692,8 @@ WINDOW w AS ( -- INITIAL Mode (Runtime) -- ============================================================ --- Explicit INITIAL (after AFTER MATCH SKIP, per the grammar); same as the default +-- Explicit INITIAL (after AFTER MATCH SKIP, per the grammar); +-- same as the default WITH test_initial_mode AS ( SELECT * FROM (VALUES (1, ARRAY['_']), -- Unmatched @@ -3787,7 +3977,8 @@ WINDOW w AS ( -- Partition end with absorbable pattern -- SKIP PAST LAST ROW + unbounded frame + all rows match A --- Triggers absorb in !rowExists path at partition boundary. +-- 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']), @@ -3845,6 +4036,9 @@ WINDOW w AS ( -- ============================================================ -- 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']), @@ -3861,7 +4055,7 @@ FROM test_partial_absorbable WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+) B) DEFINE A AS 'A' = ANY(flags), @@ -3869,6 +4063,9 @@ WINDOW w AS ( ); -- 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']), @@ -3886,7 +4083,7 @@ FROM test_dynamic_flags WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+) | B) DEFINE A AS 'A' = ANY(flags), @@ -3895,7 +4092,7 @@ WINDOW w AS ( -- Non-absorbable context during absorption -- Pattern (A B)+ C: A,B in absorbable group, C is not. --- When END exits to C, the cloned context becomes non-absorbable. +-- When END exits to C, the cloned state becomes non-absorbable. WITH test_non_absorbable AS ( SELECT * FROM (VALUES (1, ARRAY['A']), @@ -3982,7 +4179,8 @@ WINDOW w AS ( -- 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). +-- 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 @@ -4009,7 +4207,8 @@ WINDOW w AS ( -- 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). +-- 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 @@ -4035,7 +4234,8 @@ WINDOW w AS ( ); -- Reluctant branch in ALT not absorbable: (A+?) | B --- A+? is reluctant so not absorbable. Compare with greedy (A+) | B above. +-- 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']), @@ -4052,7 +4252,7 @@ FROM test_reluctant_alt_absorption WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - AFTER MATCH SKIP TO NEXT ROW + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+?) | B) DEFINE A AS 'A' = ANY(flags), @@ -4063,13 +4263,14 @@ WINDOW w AS ( -- Zero-Consumption Cycle Detection -- ============================================================ --- Cycle prevention at count > 0: (A*)* inner skip cycles at count=3 +-- (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']) -- Inner A* matches 0, cycles at count=3 + (4, ARRAY['B']) -- A* stops here ) AS t(id, flags) ) SELECT id, flags, @@ -4453,7 +4654,7 @@ WINDOW w AS ( ); -- (A B C | A B): the first alternative is the longer one and it fits, so --- length and written order agree. Compare with the reverse below. +-- length and written order agree. WITH test_alt_shared_prefix_long_first AS ( SELECT * FROM (VALUES (1, ARRAY['A']), @@ -4775,15 +4976,18 @@ WINDOW w AS ( ); -- ------------------------------------------------------------ --- 7.2.6 Anchors (not yet implemented - syntax error expected) +-- 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: not yet supported +-- ^ 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); --- $ anchor: not yet supported +-- $ 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); @@ -4792,13 +4996,15 @@ WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -- 7.2.8 Infinite repetitions of empty matches -- (Perl lower-bound stopping rule) -- ------------------------------------------------------------ --- Standard examples from 7.2.8: --- (A?){0,3}: allowed strings include STR00=(), STR01=(A), STR02=(empty), --- STR03=(AA), STR04=(A,empty), STR07=(AAA), STR08=(AA,empty) --- (A?){1,3}: same as {0,3} but STR00 excluded (min=1 not met) --- (A?){2,3}: STR03-06 (len 2) and STR07,08,11,12 (len 3) are valid --- STR06=(STRE,STRE) IS valid because non-final STRE at --- position 1 fills the lower bound +-- 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 @@ -4871,8 +5077,9 @@ WINDOW w AS ( A AS 'A' = ANY(flags) ); --- (A?){2,3}: min=2, nullable inner. Per ISO/IEC 19075-5 7.2.8 STR06 = (STRE STRE) --- is valid: two empty iterations satisfy min=2. +-- (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']), @@ -4915,10 +5122,10 @@ WINDOW w AS ( A AS 'A' = ANY(flags) ); --- (A? | B){3}: an empty iteration below min fills the lower bound (STR06), --- and it must outrank the later branch. Row 2 is B only, so A? derives empty --- there; repeating that derivation fills the remaining iterations and the --- match ends at row 1. Taking branch B instead would consume rows 2-3. +-- (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']), @@ -4940,8 +5147,8 @@ WINDOW w AS ( B AS 'B' = ANY(flags) ); --- The same pattern unrolled. Consecutive identical alternations are merged --- into the rolled form above, so the two must agree. +-- 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']), @@ -5093,8 +5300,7 @@ WINDOW g AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING DEFINE A AS 'A' = ANY(flags)), rr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW PATTERN (((A??){2}?)) - DEFINE A AS 'A' = ANY(flags)) -ORDER BY id; + DEFINE A AS 'A' = ANY(flags)); -- (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 @@ -5329,9 +5535,10 @@ WINDOW w AS ( C AS 'C' = ANY(flags) ); --- (A? | B){3} C over the same rows: with an exact bound the two empty --- iterations sit below min, so the loop must continue; the third takes B --- and the match is rows 1-2. Contrast with the {2,3} case above. +-- (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']), diff --git a/src/test/regress/sql/window.sql b/src/test/regress/sql/window.sql index 8e6f92d94c7..1a4971a6083 100644 --- a/src/test/regress/sql/window.sql +++ b/src/test/regress/sql/window.sql @@ -235,6 +235,14 @@ SELECT last_value(unique1) over (ORDER BY four rows between current row and 2 fo unique1, four FROM tenk1 WHERE unique1 < 10; +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; + +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; + SELECT sum(unique1) over (rows between 2 preceding and 1 preceding), unique1, four FROM tenk1 WHERE unique1 < 10; -- 2.54.0 (Apple Git-157)