Skip to content

Commit d5f9095

Browse files
antiguruclaude
andcommitted
compute: Re-encode temporal-bucketing output as the columnar edge
Temporal bucketing (`maybe_apply_temporal_bucketing`) is `Vec`-internal: it consumes and produces a `StreamVec`. The four sites that drive it previously decoded the edge with `into_vec` and then re-wrapped the `Vec` result as `CollectionEdge::Vec`, leaving the last `Vec` producer that fed a Union's `concat_many`. Re-encode each bucketed result with `vec_to_columnar` so the output edge is columnar, matching every other producer after the producer wave. The bucketer stays on `Vec` internally; only its output boundary changes. This is a non-consolidating leaf encode, since the bucketer output is not consolidated and the prior `CollectionEdge::Vec` wrap was non-consolidating. With every producer now columnar, a consolidating Union that mixes a bucketed input with a `Direct` input feeds `concat_many` two columnar edges instead of a mixed pair. Adds runtime coverage to `temporal_bucketing.slt` under `enable_compute_temporal_bucketing`: bucketed Reduce, bucketed TopK, and an `EXCEPT ALL` whose Union carries `[TemporalBucketing, Direct]`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7fecbce commit d5f9095

4 files changed

Lines changed: 157 additions & 39 deletions

File tree

‎src/compute/src/render.rs‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,11 +1378,17 @@ impl<'scope, T: RenderTimestamp + MaybeBucketByTime> Context<'scope, T> {
13781378
.get(&self.config_set)
13791379
.try_into()
13801380
.expect("must fit");
1381+
// Temporal bucketing (node C8) is `Vec`-internal: decode
1382+
// the edge into it, then re-encode the `Vec` result to
1383+
// columnar so this Union input stays columnar and
1384+
// `concat_many` sees no mixed variants.
13811385
let os = os.into_vec();
1382-
CollectionEdge::Vec(T::maybe_apply_temporal_bucketing(
1383-
os.inner,
1384-
self.as_of_frontier.clone(),
1385-
summary,
1386+
CollectionEdge::Columnar(vec_to_columnar(
1387+
T::maybe_apply_temporal_bucketing(
1388+
os.inner,
1389+
self.as_of_frontier.clone(),
1390+
summary,
1391+
),
13861392
))
13871393
} else {
13881394
os

‎src/compute/src/render/context.rs‎

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ use timely::progress::{Antichain, Timestamp};
5151

5252
use crate::compute_state::ComputeState;
5353
use crate::extensions::arrange::{KeyCollection, MzArrange, MzArrangeCore};
54-
use crate::render::columnar::CollectionEdge;
54+
use crate::render::columnar::{CollectionEdge, vec_to_columnar};
5555
use crate::render::errors::{DataflowErrorSer, ErrorLogger};
5656
use crate::render::{LinearJoinSpec, MaybeBucketByTime, RenderTimestamp};
5757
use crate::typedefs::{
@@ -1083,14 +1083,15 @@ impl<'scope, T: RenderTimestamp> CollectionBundle<'scope, T> {
10831083
.try_into()
10841084
.expect("must fit");
10851085
bucketed = true;
1086-
// Temporal bucketing (node C8) still consumes and produces a
1087-
// `Vec` edge, so decode here. This leaf decode is the sanctioned
1088-
// seam until the bucketing operator learns the columnar form.
1089-
CollectionEdge::Vec(T::maybe_apply_temporal_bucketing(
1086+
// Temporal bucketing (node C8) is `Vec`-internal: it consumes and
1087+
// produces a `Vec` stream. Decode the edge into it, then re-encode
1088+
// the `Vec` result to columnar at the boundary so the bucketed
1089+
// output edge stays columnar like every other producer.
1090+
CollectionEdge::Columnar(vec_to_columnar(T::maybe_apply_temporal_bucketing(
10901091
oks.into_vec().inner,
10911092
as_of.clone(),
10921093
summary,
1093-
))
1094+
)))
10941095
} else {
10951096
oks
10961097
};
@@ -1114,27 +1115,26 @@ impl<'scope, T: RenderTimestamp> CollectionBundle<'scope, T> {
11141115
} else {
11151116
strategy
11161117
};
1117-
let oks = if matches!(effective_strategy, ArrangementStrategy::TemporalBucketing)
1118-
&& ENABLE_COMPUTE_TEMPORAL_BUCKETING.get(config_set)
1119-
{
1120-
let summary: mz_repr::Timestamp = TEMPORAL_BUCKETING_SUMMARY
1121-
.get(config_set)
1122-
.try_into()
1123-
.expect("must fit");
1124-
bucketed = true;
1125-
// Temporal bucketing (node C8) still consumes and produces a
1126-
// `Vec` edge, so decode here. This leaf decode is the
1127-
// sanctioned seam until the bucketing operator learns the
1128-
// columnar form.
1129-
let oks = oks.into_vec();
1130-
CollectionEdge::Vec(T::maybe_apply_temporal_bucketing(
1131-
oks.inner,
1132-
as_of.clone(),
1133-
summary,
1134-
))
1135-
} else {
1136-
oks
1137-
};
1118+
let oks =
1119+
if matches!(effective_strategy, ArrangementStrategy::TemporalBucketing)
1120+
&& ENABLE_COMPUTE_TEMPORAL_BUCKETING.get(config_set)
1121+
{
1122+
let summary: mz_repr::Timestamp = TEMPORAL_BUCKETING_SUMMARY
1123+
.get(config_set)
1124+
.try_into()
1125+
.expect("must fit");
1126+
bucketed = true;
1127+
// Temporal bucketing (node C8) is `Vec`-internal: it consumes
1128+
// and produces a `Vec` stream. Decode the edge into it, then
1129+
// re-encode the `Vec` result to columnar at the boundary so the
1130+
// bucketed output edge stays columnar like every other producer.
1131+
let oks = oks.into_vec();
1132+
CollectionEdge::Columnar(vec_to_columnar(
1133+
T::maybe_apply_temporal_bucketing(oks.inner, as_of.clone(), summary),
1134+
))
1135+
} else {
1136+
oks
1137+
};
11381138
let use_paged_path = ENABLE_COLUMN_PAGED_BATCHER.get(config_set);
11391139
let (oks, errs_keyed, passthrough) = Self::arrange_collection(
11401140
&name,

‎src/compute/src/render/top_k.rs‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
4949
use crate::extensions::arrange::{ArrangementSize, KeyCollection, MzArrange};
5050
use crate::extensions::reduce::{ClearContainer, MzReduce};
5151
use crate::render::Pairer;
52-
use crate::render::columnar::CollectionEdge;
52+
use crate::render::columnar::{CollectionEdge, vec_to_columnar};
5353
use crate::render::context::{ArrangementFlavor, CollectionBundle, Context};
5454
use crate::render::errors::DataflowErrorSer;
5555
use crate::render::errors::MaybeValidatingRow;
@@ -110,11 +110,12 @@ impl<'scope, T: crate::render::RenderTimestamp + crate::render::MaybeBucketByTim
110110
`mz_now()` has been const-folded and no temporal bucketing is set",
111111
);
112112
}
113-
// Temporal bucketing consumes and produces a `Vec` stream, so decode the
114-
// edge here. This is the sanctioned leaf decode (node C8 analog). It only
115-
// fires under `ENABLE_COMPUTE_TEMPORAL_BUCKETING` and the `TemporalBucketing`
116-
// strategy, both off on the common path, so the columnar edge otherwise
117-
// flows straight through.
113+
// Temporal bucketing is `Vec`-internal: it consumes and produces a `Vec`
114+
// stream. Decode the edge into it, then re-encode the `Vec` result to
115+
// columnar at the boundary so the bucketed input edge stays columnar. It
116+
// only fires under `ENABLE_COMPUTE_TEMPORAL_BUCKETING` and the
117+
// `TemporalBucketing` strategy, both off on the common path, so the
118+
// columnar edge otherwise flows straight through.
118119
let ok_input = if matches!(
119120
temporal_bucketing_strategy,
120121
ArrangementStrategy::TemporalBucketing
@@ -124,11 +125,11 @@ impl<'scope, T: crate::render::RenderTimestamp + crate::render::MaybeBucketByTim
124125
.get(&self.config_set)
125126
.try_into()
126127
.expect("must fit");
127-
CollectionEdge::Vec(T::maybe_apply_temporal_bucketing(
128+
CollectionEdge::Columnar(vec_to_columnar(T::maybe_apply_temporal_bucketing(
128129
ok_input.into_vec().inner,
129130
self.as_of_frontier.clone(),
130131
summary,
131-
))
132+
)))
132133
} else {
133134
ok_input
134135
};

‎test/sqllogictest/temporal_bucketing.slt‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,114 @@ materialize.public.mv_except_distinct_buckets_at_reduce:
276276
Target cluster: quickstart
277277

278278
EOF
279+
280+
# -----------------------------------------------------------------------------
281+
# Runtime tests (node Ta). With `enable_compute_temporal_bucketing` on, the
282+
# bucketed dataflow edges are re-encoded to the columnar representation instead
283+
# of re-wrapping `Vec`. These tests turn the flag on and assert the bucketed
284+
# dataflows still produce the correct logical results, and that a consolidating
285+
# `Union` concatenating a bucketed input with a `Direct` input yields the right
286+
# output (the mixed `concat_many` case, now columnar on both legs).
287+
# -----------------------------------------------------------------------------
288+
289+
simple conn=mz_system,user=mz_system
290+
ALTER SYSTEM SET enable_compute_temporal_bucketing = true
291+
----
292+
COMPLETE 0
293+
294+
statement ok
295+
CREATE TABLE rt_events (k INT NOT NULL, event_time TIMESTAMP NOT NULL)
296+
297+
# Far-future event times so the temporal predicates hold regardless of the
298+
# wall clock at query time, keeping the results deterministic.
299+
statement ok
300+
INSERT INTO rt_events VALUES
301+
(1, '2999-01-01 00:00:00'),
302+
(1, '2999-01-02 00:00:00'),
303+
(2, '2999-01-03 00:00:00'),
304+
(3, '2999-01-04 00:00:00')
305+
306+
statement ok
307+
CREATE TABLE rt_other (k INT NOT NULL)
308+
309+
statement ok
310+
INSERT INTO rt_other VALUES (2), (99)
311+
312+
# Bucketed Reduce: temporal filter above a GROUP BY. Hits the arrangement
313+
# re-encode in `context.rs`.
314+
statement ok
315+
CREATE MATERIALIZED VIEW rt_reduce AS
316+
SELECT k, count(*)
317+
FROM rt_events
318+
WHERE event_time + INTERVAL '45 day' > mz_now()
319+
GROUP BY k
320+
321+
query II rowsort
322+
SELECT * FROM rt_reduce
323+
----
324+
1 2
325+
2 1
326+
3 1
327+
328+
# Bucketed TopK: temporal filter under ORDER BY ... LIMIT. Hits the re-encode
329+
# in `top_k.rs`.
330+
statement ok
331+
CREATE MATERIALIZED VIEW rt_topk AS
332+
SELECT k
333+
FROM rt_events
334+
WHERE event_time + INTERVAL '45 day' > mz_now()
335+
ORDER BY event_time
336+
LIMIT 3
337+
338+
query I rowsort
339+
SELECT * FROM rt_topk
340+
----
341+
1
342+
1
343+
2
344+
345+
# Mixed Union: `EXCEPT ALL` of a temporal-filtered leg (bucketed) against a
346+
# plain relation (Direct) lowers to a consolidating `Union` with per-input
347+
# strategies `[TemporalBucketing, Direct]`. After node Ta both legs reach
348+
# `concat_many` as columnar edges.
349+
query T multiline
350+
EXPLAIN PHYSICAL PLAN AS VERBOSE TEXT FOR
351+
CREATE MATERIALIZED VIEW rt_union AS
352+
SELECT k FROM rt_events WHERE event_time + INTERVAL '45 day' > mz_now()
353+
EXCEPT ALL
354+
SELECT k FROM rt_other
355+
----
356+
materialize.public.rt_union:
357+
Threshold::Basic ensure_arrangement={ key=[#0], permutation=id, thinning=() }
358+
ArrangeBy
359+
raw=false
360+
arrangements[0]={ key=[#0], permutation=id, thinning=() }
361+
Union consolidate_output=true
362+
temporal_bucketing_strategies=[TemporalBucketing, Direct]
363+
Get::Collection materialize.public.rt_events
364+
raw=true
365+
Negate
366+
Get::PassArrangements materialize.public.rt_other
367+
raw=true
368+
369+
Source materialize.public.rt_events
370+
project=(#0)
371+
filter=((mz_now() < timestamp_to_mz_timestamp((#1{event_time} + 45 days))))
372+
Source materialize.public.rt_other
373+
374+
Target cluster: quickstart
375+
376+
EOF
377+
378+
statement ok
379+
CREATE MATERIALIZED VIEW rt_union AS
380+
SELECT k FROM rt_events WHERE event_time + INTERVAL '45 day' > mz_now()
381+
EXCEPT ALL
382+
SELECT k FROM rt_other
383+
384+
query I rowsort
385+
SELECT * FROM rt_union
386+
----
387+
1
388+
1
389+
3

0 commit comments

Comments
 (0)