diff --git a/.unreleased/pr_10175 b/.unreleased/pr_10175 new file mode 100644 index 00000000000..b047c93e5d0 --- /dev/null +++ b/.unreleased/pr_10175 @@ -0,0 +1,2 @@ +Fixes: #10071 Prune the real-time branch of hierarchical continuous aggregates at any nesting depth +Thanks: @viniciusrsouza for reporting the issue diff --git a/src/nodes/chunk_append/exec.c b/src/nodes/chunk_append/exec.c index 3056a95cae6..5894c1f9fca 100644 --- a/src/nodes/chunk_append/exec.c +++ b/src/nodes/chunk_append/exec.c @@ -1001,7 +1001,7 @@ ts_constify_restrictinfos(PlannerInfo *root, List *restrictinfos) * transformations again. This might allow us to exclude chunks * based on a parameterized time_bucket expression. */ - Expr *additional_clause = ts_transform_time_bucket_comparison(constified); + Expr *additional_clause = ts_transform_nested_time_bucket_comparison(constified); if (additional_clause != NULL) { /* diff --git a/src/planner/expand_hypertable.c b/src/planner/expand_hypertable.c index 83e262affd9..1dd76856f14 100644 --- a/src/planner/expand_hypertable.c +++ b/src/planner/expand_hypertable.c @@ -731,6 +731,32 @@ ts_transform_time_bucket_comparison(Expr *node) return &op->xpr; } +/* + * Fully unwrap a (possibly nested) time_bucket() comparison by applying the + * single-level transform repeatedly until the bound reaches the raw column. + * + * ts_transform_time_bucket_comparison() only strips the outermost time_bucket(). + * In hierarchical continuous aggregates the time_bucket() input column can be + * itself another time_bucket() call. + * + * Used both during hypertable expansion and from the chunk-append executor's + * runtime constification. + */ +Expr * +ts_transform_nested_time_bucket_comparison(Expr *qual) +{ + Expr *nested = ts_transform_time_bucket_comparison(qual); + Expr *transformed = NULL; + + while (nested != NULL) + { + transformed = nested; + nested = ts_transform_time_bucket_comparison(transformed); + } + + return transformed; +} + /* * Since baserestrictinfo is not yet set by the planner, we have to derive * it ourselves. It's safe for us to miss some restrict info clauses (this @@ -784,7 +810,7 @@ process_quals(Node *quals, CollectQualCtx *ctx, bool is_outer_join) * check for time_bucket comparisons * time_bucket(Const, time_colum) > Const */ - Expr *transformed = ts_transform_time_bucket_comparison(qual); + Expr *transformed = ts_transform_nested_time_bucket_comparison(qual); if (transformed != NULL) { /* @@ -835,7 +861,7 @@ timebucket_annotate(Node *quals, CollectQualCtx *ctx) * check for time_bucket comparisons * time_bucket(Const, time_colum) > Const */ - Expr *transformed = ts_transform_time_bucket_comparison(qual); + Expr *transformed = ts_transform_nested_time_bucket_comparison(qual); if (transformed != NULL) { /* @@ -1054,7 +1080,7 @@ get_simplified_restrictions(PlannerInfo *root, List *restrictions) * check for time_bucket comparisons * time_bucket(Const, time_colum) > Const */ - Expr *transformed = ts_transform_time_bucket_comparison(qual); + Expr *transformed = ts_transform_nested_time_bucket_comparison(qual); if (transformed != NULL) { /* diff --git a/src/planner/planner.h b/src/planner/planner.h index 30c893850b7..02556cd9408 100644 --- a/src/planner/planner.h +++ b/src/planner/planner.h @@ -97,6 +97,7 @@ extern void ts_plan_expand_hypertable_chunks(Hypertable *ht, PlannerInfo *root, bool include_osm); extern void ts_plan_expand_timebucket_annotate(PlannerInfo *root, RelOptInfo *rel); extern Expr *ts_transform_time_bucket_comparison(Expr *); +extern Expr *ts_transform_nested_time_bucket_comparison(Expr *); extern Node *ts_constify_now(PlannerInfo *root, List *rtable, Node *node); extern void ts_planner_constraint_cleanup(PlannerInfo *root, RelOptInfo *rel); extern Node *ts_add_space_constraints(PlannerInfo *root, List *rtable, Node *node); diff --git a/tsl/test/expected/cagg_hierarchical_realtime-16.out b/tsl/test/expected/cagg_hierarchical_realtime-16.out new file mode 100644 index 00000000000..5bcb4670528 --- /dev/null +++ b/tsl/test/expected/cagg_hierarchical_realtime-16.out @@ -0,0 +1,130 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. +-- Regression test for #10071. +-- +-- A real-time continuous aggregate (timescaledb.materialized_only = false) that +-- is three or more levels deep in a hierarchy (a cagg on a cagg on a cagg) must +-- prune its real-time branch when refreshed for a window entirely below the +-- watermark, exactly as a two-level cagg does. A refresh reads the cagg's +-- internal partial view, so EXPLAINing a below-watermark SELECT on that partial +-- view reproduces the refresh scan. Before the fix the real-time branch was +-- pruned only up to two levels; at three or more it re-scanned the whole +-- un-materialized tail of the base hypertable and discarded every row. +SET timezone TO PST8PDT; +SET max_parallel_workers_per_gather TO 0; +SET client_min_messages TO WARNING; +CREATE TABLE conditions (ts timestamptz NOT NULL, value double precision NOT NULL); +SELECT create_hypertable('conditions', by_range('ts', INTERVAL '7 days')); + create_hypertable +------------------- + (1,t) + +INSERT INTO conditions +SELECT ts, 1.0 +FROM generate_series(TIMESTAMPTZ '2026-01-01', TIMESTAMPTZ '2026-04-29', INTERVAL '1 hour') AS ts; +-- Level 1: 6-hour buckets on the raw hypertable +CREATE MATERIALIZED VIEW cagg_l1 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('6 hours', ts) AS ts, count(*) AS cnt, sum(value) AS sum_value +FROM conditions GROUP BY 1 WITH NO DATA; +-- Level 2: 1-day buckets on cagg_l1 (raw -> 6h -> 1day) +CREATE MATERIALIZED VIEW cagg_l2 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('1 day', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l1 GROUP BY 1 WITH NO DATA; +-- Level 3: 7-day buckets on cagg_l2 (raw -> 6h -> 1day -> 7day) +CREATE MATERIALIZED VIEW cagg_l3 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('7 days', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l2 GROUP BY 1 WITH NO DATA; +-- Level 4: 28-day buckets on cagg_l3 (raw -> 6h -> 1day -> 7day -> 28day) +CREATE MATERIALIZED VIEW cagg_l4 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('28 days', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l3 GROUP BY 1 WITH NO DATA; +-- Materialize everything up to 2026-04-01, leaving an un-materialized tail. +CALL refresh_continuous_aggregate('cagg_l1', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l2', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l3', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l4', NULL, TIMESTAMPTZ '2026-04-01'); +-- A refresh reads the cagg's internal partial view, so EXPLAINing a +-- below-watermark SELECT on that partial view reproduces the refresh scan. Look +-- up each partial view name. +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l2 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l2' \gset +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l3 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l3' \gset +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l4 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l4' \gset +\set PREFIX 'EXPLAIN (COSTS OFF, TIMING OFF, SUMMARY OFF)' +-- For a window entirely below the watermark the real-time branch must be pruned +-- at every depth: the plan should fold it to "One-Time Filter: false" and touch +-- only the materialized hypertable, never the base-hypertable (_hyper_1_*) +-- chunks. +-- 2 levels (control: pruned before and after the fix) +:PREFIX SELECT * FROM :pv_l2 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; +--- QUERY PLAN --- + GroupAggregate + Group Key: (time_bucket('@ 1 day'::interval, _hyper_2_19_chunk.ts)) + -> Sort + Sort Key: (time_bucket('@ 1 day'::interval, _hyper_2_19_chunk.ts)) + -> Result + -> Append + -> Index Scan using _hyper_2_19_chunk__materialized_hypertable_2_ts_idx on _hyper_2_19_chunk + Index Cond: ((ts < 'Tue Mar 31 23:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Wed Feb 11 00:00:00 2026 PST'::timestamp with time zone)) + Filter: ((time_bucket('@ 1 day'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 1 day'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> HashAggregate + Group Key: time_bucket('@ 6 hours'::interval, ts) + -> Result + One-Time Filter: false + +-- 3 levels (the bug: real-time branch must be pruned) +:PREFIX SELECT * FROM :pv_l3 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; +--- QUERY PLAN --- + GroupAggregate + Group Key: (time_bucket('@ 7 days'::interval, _hyper_3_21_chunk.ts)) + -> Sort + Sort Key: (time_bucket('@ 7 days'::interval, _hyper_3_21_chunk.ts)) + -> Result + -> Append + -> Index Scan using _hyper_3_21_chunk__materialized_hypertable_3_ts_idx on _hyper_3_21_chunk + Index Cond: ((ts < 'Tue Mar 31 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Feb 17 00:00:00 2026 PST'::timestamp with time zone)) + Filter: ((time_bucket('@ 7 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 7 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> HashAggregate + Group Key: time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, ts))) + -> Result + -> HashAggregate + Group Key: time_bucket('@ 6 hours'::interval, ts) + -> Result + One-Time Filter: false + +-- 4 levels +:PREFIX SELECT * FROM :pv_l4 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; +--- QUERY PLAN --- + GroupAggregate + Group Key: (time_bucket('@ 28 days'::interval, _materialized_hypertable_4.ts)) + -> Sort + Sort Key: (time_bucket('@ 28 days'::interval, _materialized_hypertable_4.ts)) + -> Result + -> Append + -> Append + -> Index Scan using _hyper_4_22_chunk__materialized_hypertable_4_ts_idx on _hyper_4_22_chunk + Index Cond: ((ts < 'Sun Mar 29 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Mar 10 01:00:00 2026 PDT'::timestamp with time zone)) + Filter: ((time_bucket('@ 28 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 28 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> Index Scan using _hyper_4_23_chunk__materialized_hypertable_4_ts_idx on _hyper_4_23_chunk + Index Cond: ((ts < 'Sun Mar 29 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Mar 10 01:00:00 2026 PDT'::timestamp with time zone)) + Filter: ((time_bucket('@ 28 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 28 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> HashAggregate + Group Key: time_bucket('@ 7 days'::interval, (time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, ts))))) + -> Result + -> HashAggregate + Group Key: time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, ts))) + -> Result + -> HashAggregate + Group Key: time_bucket('@ 6 hours'::interval, ts) + -> Result + One-Time Filter: false + +DROP TABLE conditions CASCADE; +RESET client_min_messages; diff --git a/tsl/test/expected/cagg_hierarchical_realtime-17.out b/tsl/test/expected/cagg_hierarchical_realtime-17.out new file mode 100644 index 00000000000..5bcb4670528 --- /dev/null +++ b/tsl/test/expected/cagg_hierarchical_realtime-17.out @@ -0,0 +1,130 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. +-- Regression test for #10071. +-- +-- A real-time continuous aggregate (timescaledb.materialized_only = false) that +-- is three or more levels deep in a hierarchy (a cagg on a cagg on a cagg) must +-- prune its real-time branch when refreshed for a window entirely below the +-- watermark, exactly as a two-level cagg does. A refresh reads the cagg's +-- internal partial view, so EXPLAINing a below-watermark SELECT on that partial +-- view reproduces the refresh scan. Before the fix the real-time branch was +-- pruned only up to two levels; at three or more it re-scanned the whole +-- un-materialized tail of the base hypertable and discarded every row. +SET timezone TO PST8PDT; +SET max_parallel_workers_per_gather TO 0; +SET client_min_messages TO WARNING; +CREATE TABLE conditions (ts timestamptz NOT NULL, value double precision NOT NULL); +SELECT create_hypertable('conditions', by_range('ts', INTERVAL '7 days')); + create_hypertable +------------------- + (1,t) + +INSERT INTO conditions +SELECT ts, 1.0 +FROM generate_series(TIMESTAMPTZ '2026-01-01', TIMESTAMPTZ '2026-04-29', INTERVAL '1 hour') AS ts; +-- Level 1: 6-hour buckets on the raw hypertable +CREATE MATERIALIZED VIEW cagg_l1 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('6 hours', ts) AS ts, count(*) AS cnt, sum(value) AS sum_value +FROM conditions GROUP BY 1 WITH NO DATA; +-- Level 2: 1-day buckets on cagg_l1 (raw -> 6h -> 1day) +CREATE MATERIALIZED VIEW cagg_l2 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('1 day', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l1 GROUP BY 1 WITH NO DATA; +-- Level 3: 7-day buckets on cagg_l2 (raw -> 6h -> 1day -> 7day) +CREATE MATERIALIZED VIEW cagg_l3 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('7 days', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l2 GROUP BY 1 WITH NO DATA; +-- Level 4: 28-day buckets on cagg_l3 (raw -> 6h -> 1day -> 7day -> 28day) +CREATE MATERIALIZED VIEW cagg_l4 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('28 days', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l3 GROUP BY 1 WITH NO DATA; +-- Materialize everything up to 2026-04-01, leaving an un-materialized tail. +CALL refresh_continuous_aggregate('cagg_l1', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l2', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l3', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l4', NULL, TIMESTAMPTZ '2026-04-01'); +-- A refresh reads the cagg's internal partial view, so EXPLAINing a +-- below-watermark SELECT on that partial view reproduces the refresh scan. Look +-- up each partial view name. +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l2 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l2' \gset +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l3 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l3' \gset +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l4 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l4' \gset +\set PREFIX 'EXPLAIN (COSTS OFF, TIMING OFF, SUMMARY OFF)' +-- For a window entirely below the watermark the real-time branch must be pruned +-- at every depth: the plan should fold it to "One-Time Filter: false" and touch +-- only the materialized hypertable, never the base-hypertable (_hyper_1_*) +-- chunks. +-- 2 levels (control: pruned before and after the fix) +:PREFIX SELECT * FROM :pv_l2 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; +--- QUERY PLAN --- + GroupAggregate + Group Key: (time_bucket('@ 1 day'::interval, _hyper_2_19_chunk.ts)) + -> Sort + Sort Key: (time_bucket('@ 1 day'::interval, _hyper_2_19_chunk.ts)) + -> Result + -> Append + -> Index Scan using _hyper_2_19_chunk__materialized_hypertable_2_ts_idx on _hyper_2_19_chunk + Index Cond: ((ts < 'Tue Mar 31 23:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Wed Feb 11 00:00:00 2026 PST'::timestamp with time zone)) + Filter: ((time_bucket('@ 1 day'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 1 day'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> HashAggregate + Group Key: time_bucket('@ 6 hours'::interval, ts) + -> Result + One-Time Filter: false + +-- 3 levels (the bug: real-time branch must be pruned) +:PREFIX SELECT * FROM :pv_l3 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; +--- QUERY PLAN --- + GroupAggregate + Group Key: (time_bucket('@ 7 days'::interval, _hyper_3_21_chunk.ts)) + -> Sort + Sort Key: (time_bucket('@ 7 days'::interval, _hyper_3_21_chunk.ts)) + -> Result + -> Append + -> Index Scan using _hyper_3_21_chunk__materialized_hypertable_3_ts_idx on _hyper_3_21_chunk + Index Cond: ((ts < 'Tue Mar 31 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Feb 17 00:00:00 2026 PST'::timestamp with time zone)) + Filter: ((time_bucket('@ 7 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 7 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> HashAggregate + Group Key: time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, ts))) + -> Result + -> HashAggregate + Group Key: time_bucket('@ 6 hours'::interval, ts) + -> Result + One-Time Filter: false + +-- 4 levels +:PREFIX SELECT * FROM :pv_l4 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; +--- QUERY PLAN --- + GroupAggregate + Group Key: (time_bucket('@ 28 days'::interval, _materialized_hypertable_4.ts)) + -> Sort + Sort Key: (time_bucket('@ 28 days'::interval, _materialized_hypertable_4.ts)) + -> Result + -> Append + -> Append + -> Index Scan using _hyper_4_22_chunk__materialized_hypertable_4_ts_idx on _hyper_4_22_chunk + Index Cond: ((ts < 'Sun Mar 29 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Mar 10 01:00:00 2026 PDT'::timestamp with time zone)) + Filter: ((time_bucket('@ 28 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 28 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> Index Scan using _hyper_4_23_chunk__materialized_hypertable_4_ts_idx on _hyper_4_23_chunk + Index Cond: ((ts < 'Sun Mar 29 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Mar 10 01:00:00 2026 PDT'::timestamp with time zone)) + Filter: ((time_bucket('@ 28 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 28 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> HashAggregate + Group Key: time_bucket('@ 7 days'::interval, (time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, ts))))) + -> Result + -> HashAggregate + Group Key: time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, ts))) + -> Result + -> HashAggregate + Group Key: time_bucket('@ 6 hours'::interval, ts) + -> Result + One-Time Filter: false + +DROP TABLE conditions CASCADE; +RESET client_min_messages; diff --git a/tsl/test/expected/cagg_hierarchical_realtime-18.out b/tsl/test/expected/cagg_hierarchical_realtime-18.out new file mode 100644 index 00000000000..5bcb4670528 --- /dev/null +++ b/tsl/test/expected/cagg_hierarchical_realtime-18.out @@ -0,0 +1,130 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. +-- Regression test for #10071. +-- +-- A real-time continuous aggregate (timescaledb.materialized_only = false) that +-- is three or more levels deep in a hierarchy (a cagg on a cagg on a cagg) must +-- prune its real-time branch when refreshed for a window entirely below the +-- watermark, exactly as a two-level cagg does. A refresh reads the cagg's +-- internal partial view, so EXPLAINing a below-watermark SELECT on that partial +-- view reproduces the refresh scan. Before the fix the real-time branch was +-- pruned only up to two levels; at three or more it re-scanned the whole +-- un-materialized tail of the base hypertable and discarded every row. +SET timezone TO PST8PDT; +SET max_parallel_workers_per_gather TO 0; +SET client_min_messages TO WARNING; +CREATE TABLE conditions (ts timestamptz NOT NULL, value double precision NOT NULL); +SELECT create_hypertable('conditions', by_range('ts', INTERVAL '7 days')); + create_hypertable +------------------- + (1,t) + +INSERT INTO conditions +SELECT ts, 1.0 +FROM generate_series(TIMESTAMPTZ '2026-01-01', TIMESTAMPTZ '2026-04-29', INTERVAL '1 hour') AS ts; +-- Level 1: 6-hour buckets on the raw hypertable +CREATE MATERIALIZED VIEW cagg_l1 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('6 hours', ts) AS ts, count(*) AS cnt, sum(value) AS sum_value +FROM conditions GROUP BY 1 WITH NO DATA; +-- Level 2: 1-day buckets on cagg_l1 (raw -> 6h -> 1day) +CREATE MATERIALIZED VIEW cagg_l2 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('1 day', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l1 GROUP BY 1 WITH NO DATA; +-- Level 3: 7-day buckets on cagg_l2 (raw -> 6h -> 1day -> 7day) +CREATE MATERIALIZED VIEW cagg_l3 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('7 days', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l2 GROUP BY 1 WITH NO DATA; +-- Level 4: 28-day buckets on cagg_l3 (raw -> 6h -> 1day -> 7day -> 28day) +CREATE MATERIALIZED VIEW cagg_l4 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('28 days', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l3 GROUP BY 1 WITH NO DATA; +-- Materialize everything up to 2026-04-01, leaving an un-materialized tail. +CALL refresh_continuous_aggregate('cagg_l1', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l2', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l3', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l4', NULL, TIMESTAMPTZ '2026-04-01'); +-- A refresh reads the cagg's internal partial view, so EXPLAINing a +-- below-watermark SELECT on that partial view reproduces the refresh scan. Look +-- up each partial view name. +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l2 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l2' \gset +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l3 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l3' \gset +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l4 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l4' \gset +\set PREFIX 'EXPLAIN (COSTS OFF, TIMING OFF, SUMMARY OFF)' +-- For a window entirely below the watermark the real-time branch must be pruned +-- at every depth: the plan should fold it to "One-Time Filter: false" and touch +-- only the materialized hypertable, never the base-hypertable (_hyper_1_*) +-- chunks. +-- 2 levels (control: pruned before and after the fix) +:PREFIX SELECT * FROM :pv_l2 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; +--- QUERY PLAN --- + GroupAggregate + Group Key: (time_bucket('@ 1 day'::interval, _hyper_2_19_chunk.ts)) + -> Sort + Sort Key: (time_bucket('@ 1 day'::interval, _hyper_2_19_chunk.ts)) + -> Result + -> Append + -> Index Scan using _hyper_2_19_chunk__materialized_hypertable_2_ts_idx on _hyper_2_19_chunk + Index Cond: ((ts < 'Tue Mar 31 23:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Wed Feb 11 00:00:00 2026 PST'::timestamp with time zone)) + Filter: ((time_bucket('@ 1 day'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 1 day'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> HashAggregate + Group Key: time_bucket('@ 6 hours'::interval, ts) + -> Result + One-Time Filter: false + +-- 3 levels (the bug: real-time branch must be pruned) +:PREFIX SELECT * FROM :pv_l3 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; +--- QUERY PLAN --- + GroupAggregate + Group Key: (time_bucket('@ 7 days'::interval, _hyper_3_21_chunk.ts)) + -> Sort + Sort Key: (time_bucket('@ 7 days'::interval, _hyper_3_21_chunk.ts)) + -> Result + -> Append + -> Index Scan using _hyper_3_21_chunk__materialized_hypertable_3_ts_idx on _hyper_3_21_chunk + Index Cond: ((ts < 'Tue Mar 31 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Feb 17 00:00:00 2026 PST'::timestamp with time zone)) + Filter: ((time_bucket('@ 7 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 7 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> HashAggregate + Group Key: time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, ts))) + -> Result + -> HashAggregate + Group Key: time_bucket('@ 6 hours'::interval, ts) + -> Result + One-Time Filter: false + +-- 4 levels +:PREFIX SELECT * FROM :pv_l4 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; +--- QUERY PLAN --- + GroupAggregate + Group Key: (time_bucket('@ 28 days'::interval, _materialized_hypertable_4.ts)) + -> Sort + Sort Key: (time_bucket('@ 28 days'::interval, _materialized_hypertable_4.ts)) + -> Result + -> Append + -> Append + -> Index Scan using _hyper_4_22_chunk__materialized_hypertable_4_ts_idx on _hyper_4_22_chunk + Index Cond: ((ts < 'Sun Mar 29 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Mar 10 01:00:00 2026 PDT'::timestamp with time zone)) + Filter: ((time_bucket('@ 28 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 28 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> Index Scan using _hyper_4_23_chunk__materialized_hypertable_4_ts_idx on _hyper_4_23_chunk + Index Cond: ((ts < 'Sun Mar 29 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Mar 10 01:00:00 2026 PDT'::timestamp with time zone)) + Filter: ((time_bucket('@ 28 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 28 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> HashAggregate + Group Key: time_bucket('@ 7 days'::interval, (time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, ts))))) + -> Result + -> HashAggregate + Group Key: time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, ts))) + -> Result + -> HashAggregate + Group Key: time_bucket('@ 6 hours'::interval, ts) + -> Result + One-Time Filter: false + +DROP TABLE conditions CASCADE; +RESET client_min_messages; diff --git a/tsl/test/expected/cagg_hierarchical_realtime-19.out b/tsl/test/expected/cagg_hierarchical_realtime-19.out new file mode 100644 index 00000000000..22218d0b2dd --- /dev/null +++ b/tsl/test/expected/cagg_hierarchical_realtime-19.out @@ -0,0 +1,130 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. +-- Regression test for #10071. +-- +-- A real-time continuous aggregate (timescaledb.materialized_only = false) that +-- is three or more levels deep in a hierarchy (a cagg on a cagg on a cagg) must +-- prune its real-time branch when refreshed for a window entirely below the +-- watermark, exactly as a two-level cagg does. A refresh reads the cagg's +-- internal partial view, so EXPLAINing a below-watermark SELECT on that partial +-- view reproduces the refresh scan. Before the fix the real-time branch was +-- pruned only up to two levels; at three or more it re-scanned the whole +-- un-materialized tail of the base hypertable and discarded every row. +SET timezone TO PST8PDT; +SET max_parallel_workers_per_gather TO 0; +SET client_min_messages TO WARNING; +CREATE TABLE conditions (ts timestamptz NOT NULL, value double precision NOT NULL); +SELECT create_hypertable('conditions', by_range('ts', INTERVAL '7 days')); + create_hypertable +------------------- + (1,t) + +INSERT INTO conditions +SELECT ts, 1.0 +FROM generate_series(TIMESTAMPTZ '2026-01-01', TIMESTAMPTZ '2026-04-29', INTERVAL '1 hour') AS ts; +-- Level 1: 6-hour buckets on the raw hypertable +CREATE MATERIALIZED VIEW cagg_l1 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('6 hours', ts) AS ts, count(*) AS cnt, sum(value) AS sum_value +FROM conditions GROUP BY 1 WITH NO DATA; +-- Level 2: 1-day buckets on cagg_l1 (raw -> 6h -> 1day) +CREATE MATERIALIZED VIEW cagg_l2 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('1 day', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l1 GROUP BY 1 WITH NO DATA; +-- Level 3: 7-day buckets on cagg_l2 (raw -> 6h -> 1day -> 7day) +CREATE MATERIALIZED VIEW cagg_l3 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('7 days', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l2 GROUP BY 1 WITH NO DATA; +-- Level 4: 28-day buckets on cagg_l3 (raw -> 6h -> 1day -> 7day -> 28day) +CREATE MATERIALIZED VIEW cagg_l4 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('28 days', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l3 GROUP BY 1 WITH NO DATA; +-- Materialize everything up to 2026-04-01, leaving an un-materialized tail. +CALL refresh_continuous_aggregate('cagg_l1', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l2', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l3', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l4', NULL, TIMESTAMPTZ '2026-04-01'); +-- A refresh reads the cagg's internal partial view, so EXPLAINing a +-- below-watermark SELECT on that partial view reproduces the refresh scan. Look +-- up each partial view name. +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l2 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l2' \gset +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l3 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l3' \gset +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l4 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l4' \gset +\set PREFIX 'EXPLAIN (COSTS OFF, TIMING OFF, SUMMARY OFF)' +-- For a window entirely below the watermark the real-time branch must be pruned +-- at every depth: the plan should fold it to "One-Time Filter: false" and touch +-- only the materialized hypertable, never the base-hypertable (_hyper_1_*) +-- chunks. +-- 2 levels (control: pruned before and after the fix) +:PREFIX SELECT * FROM :pv_l2 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; +--- QUERY PLAN --- + GroupAggregate + Group Key: (time_bucket('@ 1 day'::interval, _hyper_2_19_chunk.ts)) + -> Sort + Sort Key: (time_bucket('@ 1 day'::interval, _hyper_2_19_chunk.ts)) + -> Result + -> Append + -> Index Scan using _hyper_2_19_chunk__materialized_hypertable_2_ts_idx on _hyper_2_19_chunk + Index Cond: ((ts < 'Tue Mar 31 23:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Wed Feb 11 00:00:00 2026 PST'::timestamp with time zone)) + Filter: ((time_bucket('@ 1 day'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 1 day'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> HashAggregate + Group Key: time_bucket('@ 6 hours'::interval, conditions.ts) + -> Result + One-Time Filter: false + +-- 3 levels (the bug: real-time branch must be pruned) +:PREFIX SELECT * FROM :pv_l3 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; +--- QUERY PLAN --- + GroupAggregate + Group Key: (time_bucket('@ 7 days'::interval, _hyper_3_21_chunk.ts)) + -> Sort + Sort Key: (time_bucket('@ 7 days'::interval, _hyper_3_21_chunk.ts)) + -> Result + -> Append + -> Index Scan using _hyper_3_21_chunk__materialized_hypertable_3_ts_idx on _hyper_3_21_chunk + Index Cond: ((ts < 'Tue Mar 31 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Feb 17 00:00:00 2026 PST'::timestamp with time zone)) + Filter: ((time_bucket('@ 7 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 7 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> HashAggregate + Group Key: time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, conditions.ts))) + -> Result + -> HashAggregate + Group Key: time_bucket('@ 6 hours'::interval, conditions.ts) + -> Result + One-Time Filter: false + +-- 4 levels +:PREFIX SELECT * FROM :pv_l4 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; +--- QUERY PLAN --- + GroupAggregate + Group Key: (time_bucket('@ 28 days'::interval, _materialized_hypertable_4.ts)) + -> Sort + Sort Key: (time_bucket('@ 28 days'::interval, _materialized_hypertable_4.ts)) + -> Result + -> Append + -> Append + -> Index Scan using _hyper_4_22_chunk__materialized_hypertable_4_ts_idx on _hyper_4_22_chunk + Index Cond: ((ts < 'Sun Mar 29 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Mar 10 01:00:00 2026 PDT'::timestamp with time zone)) + Filter: ((time_bucket('@ 28 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 28 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> Index Scan using _hyper_4_23_chunk__materialized_hypertable_4_ts_idx on _hyper_4_23_chunk + Index Cond: ((ts < 'Sun Mar 29 17:00:00 2026 PDT'::timestamp with time zone) AND (ts >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (ts < 'Tue Mar 10 01:00:00 2026 PDT'::timestamp with time zone)) + Filter: ((time_bucket('@ 28 days'::interval, ts) >= 'Tue Jan 13 00:00:00 2026 PST'::timestamp with time zone) AND (time_bucket('@ 28 days'::interval, ts) < 'Tue Feb 10 00:00:00 2026 PST'::timestamp with time zone)) + -> HashAggregate + Group Key: time_bucket('@ 7 days'::interval, (time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, conditions.ts))))) + -> Result + -> HashAggregate + Group Key: time_bucket('@ 1 day'::interval, (time_bucket('@ 6 hours'::interval, conditions.ts))) + -> Result + -> HashAggregate + Group Key: time_bucket('@ 6 hours'::interval, conditions.ts) + -> Result + One-Time Filter: false + +DROP TABLE conditions CASCADE; +RESET client_min_messages; diff --git a/tsl/test/sql/CMakeLists.txt b/tsl/test/sql/CMakeLists.txt index 045d75cf3bd..2ef7621f2b1 100644 --- a/tsl/test/sql/CMakeLists.txt +++ b/tsl/test/sql/CMakeLists.txt @@ -95,6 +95,7 @@ set(TEST_FILES vectorized_aggregation.sql) set(TEST_TEMPLATES + cagg_hierarchical_realtime.sql.in cagg_permissions.sql.in cagg_planning.sql.in cagg_rewrites.sql.in diff --git a/tsl/test/sql/cagg_hierarchical_realtime.sql.in b/tsl/test/sql/cagg_hierarchical_realtime.sql.in new file mode 100644 index 00000000000..055a214d147 --- /dev/null +++ b/tsl/test/sql/cagg_hierarchical_realtime.sql.in @@ -0,0 +1,84 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. + +-- Regression test for #10071. +-- +-- A real-time continuous aggregate (timescaledb.materialized_only = false) that +-- is three or more levels deep in a hierarchy (a cagg on a cagg on a cagg) must +-- prune its real-time branch when refreshed for a window entirely below the +-- watermark, exactly as a two-level cagg does. A refresh reads the cagg's +-- internal partial view, so EXPLAINing a below-watermark SELECT on that partial +-- view reproduces the refresh scan. Before the fix the real-time branch was +-- pruned only up to two levels; at three or more it re-scanned the whole +-- un-materialized tail of the base hypertable and discarded every row. + +SET timezone TO PST8PDT; +SET max_parallel_workers_per_gather TO 0; +SET client_min_messages TO WARNING; + +CREATE TABLE conditions (ts timestamptz NOT NULL, value double precision NOT NULL); +SELECT create_hypertable('conditions', by_range('ts', INTERVAL '7 days')); + +INSERT INTO conditions +SELECT ts, 1.0 +FROM generate_series(TIMESTAMPTZ '2026-01-01', TIMESTAMPTZ '2026-04-29', INTERVAL '1 hour') AS ts; + +-- Level 1: 6-hour buckets on the raw hypertable +CREATE MATERIALIZED VIEW cagg_l1 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('6 hours', ts) AS ts, count(*) AS cnt, sum(value) AS sum_value +FROM conditions GROUP BY 1 WITH NO DATA; + +-- Level 2: 1-day buckets on cagg_l1 (raw -> 6h -> 1day) +CREATE MATERIALIZED VIEW cagg_l2 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('1 day', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l1 GROUP BY 1 WITH NO DATA; + +-- Level 3: 7-day buckets on cagg_l2 (raw -> 6h -> 1day -> 7day) +CREATE MATERIALIZED VIEW cagg_l3 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('7 days', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l2 GROUP BY 1 WITH NO DATA; + +-- Level 4: 28-day buckets on cagg_l3 (raw -> 6h -> 1day -> 7day -> 28day) +CREATE MATERIALIZED VIEW cagg_l4 + WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS +SELECT time_bucket('28 days', ts) AS ts, sum(cnt) AS cnt, sum(sum_value) AS sum_value +FROM cagg_l3 GROUP BY 1 WITH NO DATA; + +-- Materialize everything up to 2026-04-01, leaving an un-materialized tail. +CALL refresh_continuous_aggregate('cagg_l1', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l2', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l3', NULL, TIMESTAMPTZ '2026-04-01'); +CALL refresh_continuous_aggregate('cagg_l4', NULL, TIMESTAMPTZ '2026-04-01'); + +-- A refresh reads the cagg's internal partial view, so EXPLAINing a +-- below-watermark SELECT on that partial view reproduces the refresh scan. Look +-- up each partial view name. +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l2 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l2' \gset +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l3 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l3' \gset +SELECT format('%I.%I', partial_view_schema, partial_view_name) AS pv_l4 +FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'cagg_l4' \gset + +\set PREFIX 'EXPLAIN (COSTS OFF, TIMING OFF, SUMMARY OFF)' + +-- For a window entirely below the watermark the real-time branch must be pruned +-- at every depth: the plan should fold it to "One-Time Filter: false" and touch +-- only the materialized hypertable, never the base-hypertable (_hyper_1_*) +-- chunks. + +-- 2 levels (control: pruned before and after the fix) +:PREFIX SELECT * FROM :pv_l2 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; + +-- 3 levels (the bug: real-time branch must be pruned) +:PREFIX SELECT * FROM :pv_l3 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; + +-- 4 levels +:PREFIX SELECT * FROM :pv_l4 WHERE ts >= TIMESTAMPTZ '2026-01-13' AND ts < TIMESTAMPTZ '2026-02-10'; + +DROP TABLE conditions CASCADE; +RESET client_min_messages;