Skip to content

Commit c14d129

Browse files
antiguruclaude
andcommitted
compute: Emit FlatMap output as the columnar edge
Flip FlatMap's output to build into a `ConsolidatingColumnBuilder`, returning a `CollectionEdge::Columnar`. The pre-migration ok builder was already a consolidating one, so within-batch folding is preserved. Output rows are freshly built by the mfp, so the owned give into staging is a move, not a new alloc. The err collection stays `Vec`. The input side and fuel machinery are unchanged. Adds a within-batch duplicate-fold test and updates the arms-agree and fuel tests to decode the columnar capture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 21f51b2 commit c14d129

1 file changed

Lines changed: 112 additions & 17 deletions

File tree

src/compute/src/render/flat_map.rs

Lines changed: 112 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use mz_expr::{Eval, MfpPlan};
1818
use mz_repr::{DatumVec, RowArena, SharedRow};
1919
use mz_repr::{Diff, Row, RowRef, Timestamp};
2020
use mz_timely_util::columnar::Column;
21+
use mz_timely_util::columnar::consolidate::ConsolidatingColumnBuilder;
2122
use mz_timely_util::operator::StreamExt;
2223
use timely::Container;
2324
use timely::container::DrainContainer;
@@ -76,15 +77,19 @@ impl<'scope, T: crate::render::RenderTimestamp> Context<'scope, T> {
7677
};
7778

7879
use differential_dataflow::AsCollection;
79-
let ok_collection = oks.as_collection();
80+
let ok_collection = CollectionEdge::Columnar(oks.as_collection());
8081
let new_err_collection = errs.as_collection();
8182
let err_collection = err_collection.concat(new_err_collection);
82-
CollectionBundle::from_collections(ok_collection, err_collection)
83+
CollectionBundle::from_edge(ok_collection, err_collection)
8384
}
8485
}
8586

8687
/// Output ok-session container builder for [`flat_map_stage`].
87-
type FlatMapOk<T> = ConsolidatingContainerBuilder<Vec<(Row, T, Diff)>>;
88+
///
89+
/// Consolidating like the err builder, but emits `Column<(Row, T, Diff)>` so
90+
/// the FlatMap output travels as the columnar edge. Output rows are freshly
91+
/// built by the mfp, so the owned give into staging is a move, not a new alloc.
92+
type FlatMapOk<T> = ConsolidatingColumnBuilder<Row, T, Diff>;
8893
/// Output err-session container builder for [`flat_map_stage`].
8994
type FlatMapErr<T> = ConsolidatingContainerBuilder<Vec<(DataflowErrorSer, T, Diff)>>;
9095

@@ -137,7 +142,7 @@ fn flat_map_stage<'scope, T, C>(
137142
until: Antichain<Timestamp>,
138143
budget: usize,
139144
) -> (
140-
Stream<'scope, T, Vec<(Row, T, Diff)>>,
145+
Stream<'scope, T, Column<(Row, T, Diff)>>,
141146
Stream<'scope, T, Vec<(DataflowErrorSer, T, Diff)>>,
142147
)
143148
where
@@ -329,7 +334,7 @@ mod tests {
329334
use differential_dataflow::input::Input;
330335
use mz_expr::MapFilterProject;
331336
use mz_repr::{Datum, ReprScalarType};
332-
use timely::dataflow::operators::Inspect;
337+
use timely::dataflow::operators::InspectCore;
333338
use timely::dataflow::operators::capture::{Capture, Extract};
334339

335340
use super::*;
@@ -370,7 +375,13 @@ mod tests {
370375
let scope = stream.scope();
371376
let (oks, _errs) =
372377
flat_map_stage(stream, scope, exprs, func, mfp, Antichain::new(), budget);
373-
oks.inspect(move |_| *sink.borrow_mut() += 1);
378+
// The columnar output ships whole `Column`s, so count records
379+
// through the container rather than per raw element.
380+
oks.inspect_container(move |event| {
381+
if let Ok((_time, data)) = event {
382+
*sink.borrow_mut() += data.borrow().into_index_iter().count();
383+
}
384+
});
374385
input
375386
});
376387

@@ -479,21 +490,105 @@ mod tests {
479490
})
480491
});
481492

482-
let extract_sorted = |captured: std::sync::mpsc::Receiver<_>| {
483-
let mut updates: Vec<(Row, Timestamp, Diff)> = captured
484-
.extract()
485-
.into_iter()
486-
.flat_map(|(_, data)| data)
487-
.collect();
488-
updates.sort();
489-
updates
490-
};
491-
let vec_updates = extract_sorted(vec_captured);
493+
let vec_updates = extract_sorted_columns(vec_captured);
492494
assert!(!vec_updates.is_empty());
493495
assert!(
494496
vec_updates.iter().any(|(_, _, d)| *d < Diff::ZERO),
495497
"the retraction must survive as a negative diff"
496498
);
497-
assert_eq!(vec_updates, extract_sorted(col_captured));
499+
assert_eq!(vec_updates, extract_sorted_columns(col_captured));
500+
}
501+
502+
/// Decodes a capture of the columnar FlatMap output into sorted owned
503+
/// `(row, time, diff)` updates.
504+
fn extract_sorted_columns(
505+
captured: std::sync::mpsc::Receiver<
506+
timely::dataflow::operators::capture::Event<Timestamp, Column<(Row, Timestamp, Diff)>>,
507+
>,
508+
) -> Vec<(Row, Timestamp, Diff)> {
509+
let mut updates: Vec<(Row, Timestamp, Diff)> = captured
510+
.extract()
511+
.into_iter()
512+
.flat_map(|(_, col)| {
513+
col.borrow()
514+
.into_index_iter()
515+
.map(|(v, t, d)| {
516+
(
517+
Columnar::into_owned(v),
518+
Columnar::into_owned(t),
519+
Columnar::into_owned(d),
520+
)
521+
})
522+
.collect::<Vec<_>>()
523+
})
524+
.collect();
525+
updates.sort();
526+
updates
527+
}
528+
529+
#[mz_ore::test]
530+
fn flat_map_output_consolidates_within_batch() {
531+
// Two distinct input rows whose table-function expansions overlap once
532+
// the mfp projects away the differing `stop` column. The overlapping
533+
// output rows land at the same time in one batch, so the consolidating
534+
// columnar output builder must fold them into summed diffs.
535+
let captured = timely::execute_directly(move |worker| {
536+
worker.dataflow::<Timestamp, _, _>(|scope| {
537+
let (mut input, collection) = scope.new_collection();
538+
let exprs = vec![
539+
LirScalarExpr::column(0),
540+
LirScalarExpr::column(1),
541+
LirScalarExpr::literal_ok(Datum::Int64(1), ReprScalarType::Int64),
542+
];
543+
let func = TableFunc::GenerateSeriesInt64;
544+
// Project to only the generated value (column 2), collapsing the
545+
// two input rows' distinct (start, stop) prefixes.
546+
let mfp = MapFilterProject::<LirScalarExpr>::new(3)
547+
.project(vec![2])
548+
.into_plan()
549+
.expect("project mfp");
550+
let stream = collection.inner;
551+
let scope = stream.scope();
552+
let (oks, _errs) = flat_map_stage(
553+
stream,
554+
scope,
555+
exprs,
556+
func,
557+
mfp,
558+
Antichain::new(),
559+
usize::MAX,
560+
);
561+
let captured = oks.capture();
562+
// Both rows at t=0: generate_series(1, 2) -> {1, 2},
563+
// generate_series(1, 3) -> {1, 2, 3}. Generated 1 and 2 appear on
564+
// both, so they must fold to a diff of two.
565+
input.advance_to(Timestamp::from(0_u64));
566+
input.update(input_row(2), Diff::ONE);
567+
input.update(input_row(3), Diff::ONE);
568+
input.advance_to(Timestamp::from(1_u64));
569+
input.flush();
570+
captured
571+
})
572+
});
573+
574+
let updates = extract_sorted_columns(captured);
575+
let expected = vec![
576+
(
577+
Row::pack_slice(&[Datum::Int64(1)]),
578+
Timestamp::from(0_u64),
579+
Diff::from(2),
580+
),
581+
(
582+
Row::pack_slice(&[Datum::Int64(2)]),
583+
Timestamp::from(0_u64),
584+
Diff::from(2),
585+
),
586+
(
587+
Row::pack_slice(&[Datum::Int64(3)]),
588+
Timestamp::from(0_u64),
589+
Diff::ONE,
590+
),
591+
];
592+
assert_eq!(updates, expected);
498593
}
499594
}

0 commit comments

Comments
 (0)