From 0e2dc9211c0ad74884ddee77d068b695273a5d1b Mon Sep 17 00:00:00 2001 From: Henson Choi Date: Wed, 8 Jul 2026 09:27:10 +0900 Subject: [PATCH] Fix context absorption discarding matches on non-absorbable branches Row pattern context absorption decided whether a context could be absorbed from its in-progress states alone. When a context recorded a match on a non-absorbable alternation branch (for example C in PATTERN (A{5,} | C)), that match moved to matchedState and left the in-progress set, so the context was judged fully absorbable. An older context whose absorbable run dominated it then freed the whole context, including the recorded match, which produced a wrong result. Bring the recorded match into the absorbability decision: a context that has already recorded a match is not absorbable, since absorption frees the context and its match and no absorbing context can reproduce it. count-dominance only guarantees that a context's future (in-progress) matches are a subset of an older context's; it says nothing about a match already committed. Add regression coverage in rpr_base (match ranges) and rpr_explain (absorption statistics) for alternations with one and with both branches absorbable, plus a bare A+ / A* pair contrasting a context absorbed while in-progress against a min=0 context that commits an empty match at creation and is instead eliminated by SKIP. Adjust the (A{2,})* statistics the fix shifts from absorbed to skipped. --- src/backend/executor/README.rpr | 8 + src/backend/executor/execRPR.c | 7 + src/test/regress/expected/rpr_base.out | 80 +++++++++ src/test/regress/expected/rpr_explain.out | 194 +++++++++++++++++++++- src/test/regress/sql/rpr_base.sql | 61 +++++++ src/test/regress/sql/rpr_explain.sql | 112 +++++++++++++ 6 files changed, 460 insertions(+), 2 deletions(-) diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr index d4c53fd3f63..956d69b7549 100644 --- a/src/backend/executor/README.rpr +++ b/src/backend/executor/README.rpr @@ -794,6 +794,14 @@ Worked example for PATTERN (A+) over 3 rows (each matches A): Total active contexts stays at O(1) instead of growing with N. +The monotonicity argument covers only a context's future (in-progress) +matches, not a match it has already recorded (matchedState) -- e.g., one +on a non-absorbable branch, which an absorbing context cannot reproduce. +Absorption therefore excludes any context holding a recorded match (see +nfa_update_absorption_flags()). This costs no efficiency: SKIP PAST LAST +ROW still prunes such redundant contexts once the covering match is +recorded (nfa_add_matched_state()). + VIII-3. Absorption Conditions Planner-time prerequisites (all must hold for absorption to be enabled): diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c index e0d9637b3e5..043d5a18f5b 100644 --- a/src/backend/executor/execRPR.c +++ b/src/backend/executor/execRPR.c @@ -596,6 +596,13 @@ nfa_update_absorption_flags(RPRNFAContext *ctx) allAbsorbable = false; } + /* + * A recorded match makes this context non-absorbable: absorption would + * free the match, which no absorbing context can reproduce. + */ + if (ctx->matchedState != NULL) + allAbsorbable = false; + ctx->hasAbsorbableState = hasAbsorbable; ctx->allStatesAbsorbable = allAbsorbable; } diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out index eb9f43ca2c2..85a1ad8727d 100644 --- a/src/test/regress/expected/rpr_base.out +++ b/src/test/regress/expected/rpr_base.out @@ -6082,6 +6082,86 @@ ORDER BY id; 10 | 100 | 0 (10 rows) +-- ALT Non-Absorbable Branch Match: A+ B | C +-- C match on the non-absorbable branch (id=2, id=5) must survive absorption of +-- the dominating A+ run, which never completes a match (B is never present) +WITH test_nonabsorbable_branch AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A', 'C']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A', 'C']), + (6, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nonabsorbable_branch +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B | C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | | + 2 | {A,C} | 2 | 2 + 3 | {A} | | + 4 | {A} | | + 5 | {A,C} | 5 | 5 + 6 | {A} | | +(6 rows) + +-- ALT Both Branches Absorbable: A+ C | B+ +-- A+ C never completes (C absent) so its A+ run keeps expanding and dominates; +-- a finalized B+ match on the other branch (id=1, id=6) must survive absorption +WITH test_absorbable_branches AS ( + SELECT * FROM (VALUES + (1, ARRAY['A', 'B']), + (2, ARRAY['A', 'B']), + (3, ARRAY['A', 'B']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['A', 'B']), + (7, ARRAY['A', 'B']), + (8, ARRAY['A', 'B']), + (9, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_absorbable_branches +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ C | B+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 3 + 2 | {A,B} | | + 3 | {A,B} | | + 4 | {A} | | + 5 | {A} | | + 6 | {A,B} | 6 | 8 + 7 | {A,B} | | + 8 | {A,B} | | + 9 | {A} | | +(9 rows) + -- ============================================================ -- Edge Case Tests -- ============================================================ diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out index d8343c9accc..11fad2b80ba 100644 --- a/src/test/regress/expected/rpr_explain.out +++ b/src/test/regress/expected/rpr_explain.out @@ -816,10 +816,10 @@ WINDOW w AS ( Pattern: (a{2,}")* Nav Mark Lookback: 0 Storage: Memory Maximum Storage: NkB - NFA States: 5 peak, 18 total, 0 merged + NFA States: 6 peak, 18 total, 0 merged NFA Contexts: 3 peak, 7 total, 2 pruned NFA: 2 matched (len 2/2/2.0), 0 mismatched - NFA: 2 absorbed (len 1/1/1.0), 0 skipped + NFA: 0 absorbed, 2 skipped (len 1/1/1.0) -> Function Scan on generate_series s (actual rows=6.00 loops=1) (10 rows) @@ -866,6 +866,88 @@ WINDOW w AS ( -> Function Scan on generate_series s (actual rows=50.00 loops=1) (10 rows) +-- Bare unbounded quantifier: A+ absorbs redundant contexts +-- min=1 commits no match until the run ends, so newer contexts absorb in-progress +CREATE VIEW rpr_ev_ctx_absorb_plus AS +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v > 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_plus'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------- + PATTERN (a+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v > 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=10.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 3 peak, 21 total, 0 merged + NFA Contexts: 2 peak, 11 total, 0 pruned + NFA: 1 matched (len 10/10/10.0), 0 mismatched + NFA: 9 absorbed (len 1/1/1.0), 0 skipped + -> Function Scan on generate_series s (actual rows=10.00 loops=1) +(10 rows) + +-- Bare min=0 quantifier: A* is skipped, not absorbed +-- min=0 commits an empty match at creation, so SKIP (not absorption) removes them +CREATE VIEW rpr_ev_ctx_absorb_star AS +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A*) + DEFINE A AS v > 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_star'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------- + PATTERN (a*) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A*) + DEFINE A AS v > 0 +);'); + rpr_explain_filter +---------------------------------------------------------------------- + WindowAgg (actual rows=10.00 loops=1) + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a*" + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 5 peak, 32 total, 0 merged + NFA Contexts: 2 peak, 11 total, 0 pruned + NFA: 1 matched (len 10/10/10.0), 0 mismatched + NFA: 0 absorbed, 9 skipped (len 1/1/1.0) + -> Function Scan on generate_series s (actual rows=10.00 loops=1) +(10 rows) + -- No absorption - bounded quantifier CREATE VIEW rpr_ev_ctx_no_absorb AS SELECT count(*) OVER w @@ -1293,6 +1375,114 @@ WINDOW w AS ( -> Function Scan on generate_series s (actual rows=50.00 loops=1) (11 rows) +-- Alternation, non-absorbable branch match survives absorption: A+ B | C +-- The dominating A+ run absorbs redundant contexts, but the recorded C matches +-- are not absorbable, so they survive (2 matched, not 0) +CREATE VIEW rpr_ev_ctx_absorb_alt_nonabsorb AS +WITH d(id, flags) AS ( + VALUES (1, ARRAY['A']), (2, ARRAY['A', 'C']), (3, ARRAY['A']), + (4, ARRAY['A']), (5, ARRAY['A', 'C']), (6, ARRAY['A'])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B | C) + DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags), C AS 'C' = ANY(flags) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_alt_nonabsorb'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +----------------------- + PATTERN (a+ b | c) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +WITH d(id, flags) AS ( + VALUES (1, ARRAY[''A'']), (2, ARRAY[''A'', ''C'']), (3, ARRAY[''A'']), + (4, ARRAY[''A'']), (5, ARRAY[''A'', ''C'']), (6, ARRAY[''A''])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B | C) + DEFINE A AS ''A'' = ANY(flags), B AS ''B'' = ANY(flags), C AS ''C'' = ANY(flags) +);'); + rpr_explain_filter +----------------------------------------------------------------------------------------------- + WindowAgg (actual rows=6.00 loops=1) + Window: w AS (ORDER BY "*VALUES*".column1 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+" b | c) + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 11 peak, 34 total, 0 merged + NFA Contexts: 4 peak, 7 total, 0 pruned + NFA: 2 matched (len 1/1/1.0), 1 mismatched (len 6/6/6.0) + NFA: 3 absorbed (len 1/1/1.0), 0 skipped + -> Sort (actual rows=6.00 loops=1) + Sort Key: "*VALUES*".column1 + Sort Method: quicksort Memory: NkB + -> Values Scan on "*VALUES*" (actual rows=6.00 loops=1) +(13 rows) + +-- Alternation, both branches absorbable: A+ C | B+ +-- A+ C never completes (C absent) so its A+ run absorbs redundant contexts; the +-- finalized B+ matches on the other branch survive (2 matched, not 0) +CREATE VIEW rpr_ev_ctx_absorb_alt_both AS +WITH d(id, flags) AS ( + VALUES (1, ARRAY['A', 'B']), (2, ARRAY['A', 'B']), (3, ARRAY['A', 'B']), + (4, ARRAY['A']), (5, ARRAY['A']), (6, ARRAY['A', 'B']), + (7, ARRAY['A', 'B']), (8, ARRAY['A', 'B']), (9, ARRAY['A'])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ C | B+) + DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags), C AS 'C' = ANY(flags) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_alt_both'), E'\n')) AS line WHERE line ~ 'PATTERN'; + line +------------------------ + PATTERN (a+ c | b+) +(1 row) + +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +WITH d(id, flags) AS ( + VALUES (1, ARRAY[''A'', ''B'']), (2, ARRAY[''A'', ''B'']), (3, ARRAY[''A'', ''B'']), + (4, ARRAY[''A'']), (5, ARRAY[''A'']), (6, ARRAY[''A'', ''B'']), + (7, ARRAY[''A'', ''B'']), (8, ARRAY[''A'', ''B'']), (9, ARRAY[''A''])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ C | B+) + DEFINE A AS ''A'' = ANY(flags), B AS ''B'' = ANY(flags), C AS ''C'' = ANY(flags) +);'); + rpr_explain_filter +----------------------------------------------------------------------------------------------- + WindowAgg (actual rows=9.00 loops=1) + Window: w AS (ORDER BY "*VALUES*".column1 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a+" c | b+") + Nav Mark Lookback: 0 + Storage: Memory Maximum Storage: NkB + NFA States: 10 peak, 49 total, 0 merged + NFA Contexts: 3 peak, 10 total, 0 pruned + NFA: 2 matched (len 3/3/3.0), 0 mismatched + NFA: 7 absorbed (len 1/1/1.0), 0 skipped + -> Sort (actual rows=9.00 loops=1) + Sort Key: "*VALUES*".column1 + Sort Method: quicksort Memory: NkB + -> Values Scan on "*VALUES*" (actual rows=9.00 loops=1) +(13 rows) + -- ============================================================ -- Match Length Statistics Tests -- ============================================================ diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql index 61a5bcf0b8c..3de44c509e8 100644 --- a/src/test/regress/sql/rpr_base.sql +++ b/src/test/regress/sql/rpr_base.sql @@ -3557,6 +3557,67 @@ WINDOW w AS ( ) 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 +-- the dominating A+ run, which never completes a match (B is never present) + +WITH test_nonabsorbable_branch AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A', 'C']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A', 'C']), + (6, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nonabsorbable_branch +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B | C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- ALT Both Branches Absorbable: A+ C | B+ +-- A+ C never completes (C absent) so its A+ run keeps expanding and dominates; +-- a finalized B+ match on the other branch (id=1, id=6) must survive absorption + +WITH test_absorbable_branches AS ( + SELECT * FROM (VALUES + (1, ARRAY['A', 'B']), + (2, ARRAY['A', 'B']), + (3, ARRAY['A', 'B']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['A', 'B']), + (7, ARRAY['A', 'B']), + (8, ARRAY['A', 'B']), + (9, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_absorbable_branches +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ C | B+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + -- ============================================================ -- Edge Case Tests -- ============================================================ diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql index 3cd94af6bb8..d300530216a 100644 --- a/src/test/regress/sql/rpr_explain.sql +++ b/src/test/regress/sql/rpr_explain.sql @@ -548,6 +548,52 @@ WINDOW w AS ( DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 );'); +-- Bare unbounded quantifier: A+ absorbs redundant contexts +-- min=1 commits no match until the run ends, so newer contexts absorb in-progress +CREATE VIEW rpr_ev_ctx_absorb_plus AS +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v > 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_plus'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v > 0 +);'); + +-- Bare min=0 quantifier: A* is skipped, not absorbed +-- min=0 commits an empty match at creation, so SKIP (not absorption) removes them +CREATE VIEW rpr_ev_ctx_absorb_star AS +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A*) + DEFINE A AS v > 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_star'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A*) + DEFINE A AS v > 0 +);'); + -- No absorption - bounded quantifier CREATE VIEW rpr_ev_ctx_no_absorb AS SELECT count(*) OVER w @@ -798,6 +844,72 @@ WINDOW w AS ( DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND PREV(FIRST(v), 1) IS NOT NULL );'); +-- Alternation, non-absorbable branch match survives absorption: A+ B | C +-- The dominating A+ run absorbs redundant contexts, but the recorded C matches +-- are not absorbable, so they survive (2 matched, not 0) +CREATE VIEW rpr_ev_ctx_absorb_alt_nonabsorb AS +WITH d(id, flags) AS ( + VALUES (1, ARRAY['A']), (2, ARRAY['A', 'C']), (3, ARRAY['A']), + (4, ARRAY['A']), (5, ARRAY['A', 'C']), (6, ARRAY['A'])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B | C) + DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags), C AS 'C' = ANY(flags) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_alt_nonabsorb'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +WITH d(id, flags) AS ( + VALUES (1, ARRAY[''A'']), (2, ARRAY[''A'', ''C'']), (3, ARRAY[''A'']), + (4, ARRAY[''A'']), (5, ARRAY[''A'', ''C'']), (6, ARRAY[''A''])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B | C) + DEFINE A AS ''A'' = ANY(flags), B AS ''B'' = ANY(flags), C AS ''C'' = ANY(flags) +);'); + +-- Alternation, both branches absorbable: A+ C | B+ +-- A+ C never completes (C absent) so its A+ run absorbs redundant contexts; the +-- finalized B+ matches on the other branch survive (2 matched, not 0) +CREATE VIEW rpr_ev_ctx_absorb_alt_both AS +WITH d(id, flags) AS ( + VALUES (1, ARRAY['A', 'B']), (2, ARRAY['A', 'B']), (3, ARRAY['A', 'B']), + (4, ARRAY['A']), (5, ARRAY['A']), (6, ARRAY['A', 'B']), + (7, ARRAY['A', 'B']), (8, ARRAY['A', 'B']), (9, ARRAY['A'])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ C | B+) + DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags), C AS 'C' = ANY(flags) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_alt_both'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +WITH d(id, flags) AS ( + VALUES (1, ARRAY[''A'', ''B'']), (2, ARRAY[''A'', ''B'']), (3, ARRAY[''A'', ''B'']), + (4, ARRAY[''A'']), (5, ARRAY[''A'']), (6, ARRAY[''A'', ''B'']), + (7, ARRAY[''A'', ''B'']), (8, ARRAY[''A'', ''B'']), (9, ARRAY[''A''])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ C | B+) + DEFINE A AS ''A'' = ANY(flags), B AS ''B'' = ANY(flags), C AS ''C'' = ANY(flags) +);'); + -- ============================================================ -- Match Length Statistics Tests -- ============================================================