Skip to main content

mz_compute/render/
columnar.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Columnar dataflow edge support.
11//!
12//! Defines [`ColCollection`], the columnar collection that dataflow edges between Plan
13//! nodes carry. Every producer emits this representation.
14//!
15//! Within a Plan node, operators may freely materialize `Vec` collections. Only
16//! the collection edge format is constrained. A node that produces a row-based
17//! collection re-encodes it to the columnar edge at its output leaf via
18//! [`vec_to_columnar`]. A node that must consume rows decodes at its input leaf
19//! via [`columnar_to_vec`]. Both are named operators (`VecToColumnar`,
20//! `ColumnarToVec`), so those leaf seams stay visible in dataflow
21//! introspection.
22
23use columnar::{Borrow, Columnar, Container, Index, Len, Push};
24use differential_dataflow::dynamic::pointstamp::{PointStamp, PointStampSummary};
25use differential_dataflow::{AsCollection, Collection, VecCollection};
26use mz_repr::{DatumVec, DatumVecBorrow, Diff, Row};
27use mz_timely_util::columnar::Column;
28use mz_timely_util::columnar::builder::ColumnBuilder;
29use mz_timely_util::columnar::chunk::{AccountedChunkChainBatcher, ComputeSpill};
30use mz_timely_util::columnar::columnar_consolidate_exchange;
31use mz_timely_util::operator::consolidate_pact;
32use timely::ContainerBuilder;
33use timely::container::{CapacityContainerBuilder, NoopBuilder};
34use timely::dataflow::channels::pact::{ExchangeCore, Pipeline};
35use timely::dataflow::operators::CapabilitySet;
36use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
37use timely::dataflow::operators::generic::{Operator, OutputBuilder};
38use timely::dataflow::{Scope, Stream, StreamVec};
39use timely::order::Product;
40use timely::progress::Antichain;
41
42use crate::render::RenderTimestamp;
43use crate::render::context::{ECB, Session};
44use crate::render::errors::DataflowErrorSer;
45
46/// A columnar collection of `(D, T, R)` updates traveling on a compute
47/// dataflow edge.
48///
49/// Mirrors differential's [`VecCollection<'scope, T, D, R>`]; the underlying
50/// container is [`Column<(D, T, R)>`] instead of `Vec<(D, T, R)>`.
51pub type ColumnarCollection<'scope, T, D, R> = Collection<'scope, T, Column<(D, T, R)>>;
52
53/// A columnar collection of `(Row, Diff)` updates, which is what a dataflow edge between
54/// Plan nodes carries.
55pub type ColCollection<'scope, T> = ColumnarCollection<'scope, T, Row, Diff>;
56
57/// Concatenates a collection of columnar edges.
58pub fn concat_many<'scope, T, I>(scope: Scope<'scope, T>, edges: I) -> ColCollection<'scope, T>
59where
60    T: RenderTimestamp,
61    I: IntoIterator<Item = ColCollection<'scope, T>>,
62{
63    let cols: Vec<_> = edges.into_iter().collect();
64    differential_dataflow::collection::concatenate(scope, cols)
65}
66
67/// Applies `logic` to each record in `edge`, exposing the record as a borrowed
68/// [`DatumVecBorrow`] and giving it ok and err output sessions.
69///
70/// `name` is the rendered operator's name. `max_demand` bounds the number of columns decoded
71/// per row. Pass `usize::MAX` to decode all columns.
72///
73/// This is the canonical entry point for "decoding consumers" (operators that
74/// read [`mz_repr::Datum`]s from each row anyway). It iterates the columnar
75/// batch directly without going through an owned [`Row`].
76pub fn flat_map_datums<'scope, T, DCB, L>(
77    edge: ColCollection<'scope, T>,
78    name: &str,
79    max_demand: usize,
80    mut logic: L,
81) -> (
82    Stream<'scope, T, DCB::Container>,
83    StreamVec<'scope, T, (DataflowErrorSer, T, Diff)>,
84)
85where
86    T: RenderTimestamp,
87    DCB: ContainerBuilder,
88    L: for<'a> FnMut(
89            &'a mut DatumVecBorrow<'_>,
90            T,
91            Diff,
92            &mut Session<T, DCB>,
93            &mut Session<T, ECB<T>>,
94        ) -> usize
95        + 'static,
96{
97    let scope = edge.inner.scope();
98    let mut builder = OperatorBuilder::new(name.to_string(), scope);
99    let (ok_output, ok_stream) = builder.new_output();
100    let mut ok_output = OutputBuilder::<_, DCB>::from(ok_output);
101    let (err_output, err_stream) = builder.new_output();
102    let mut err_output = OutputBuilder::<_, ECB<T>>::from(err_output);
103    let mut input = builder.new_input(edge.inner, Pipeline);
104    builder.build(move |_capabilities| {
105        let mut datums = DatumVec::new();
106        move |_frontiers| {
107            let mut ok_output = ok_output.activate();
108            let mut err_output = err_output.activate();
109            input.for_each(|time, data| {
110                // Retain the input capability to derive a capability for each
111                // output. The `Session` type alias is fixed to `CapabilitySet<T>`.
112                let ok_cap = time.retain_stamp(0);
113                let err_cap = time.retain_stamp(1);
114                let mut ok_session = ok_output.session_with_builder(&ok_cap);
115                let mut err_session = err_output.session_with_builder(&err_cap);
116                // Rows are read from the borrowed column, never materialized as
117                // owned `Row`s.
118                for (v, t, d) in data.borrow().into_index_iter() {
119                    logic(
120                        &mut datums.borrow_with_limit(v, max_demand),
121                        Columnar::into_owned(t),
122                        Columnar::into_owned(d),
123                        &mut ok_session,
124                        &mut err_session,
125                    );
126                }
127            });
128        }
129    });
130    (ok_stream, err_stream)
131}
132
133/// Negates the diff of every record in `column`, rebuilding only the diff column.
134///
135/// A `Typed` input hands its row and time columns over untouched, so only the
136/// diffs are rebuilt, which is 8 bytes per record. A serialized input keeps all
137/// three columns in one buffer, so its rows and times are copied in bulk.
138///
139/// Negation stays checked: `Neg for Overflowing` runs `overflowing_neg` and
140/// reports overflow, which `-Diff::MIN` triggers.
141///
142/// TODO: Negate a `Typed` input's diffs in place rather than rebuilding them.
143/// This cannot go through `columnar::IndexMut`: `Overflows` stores the raw
144/// integer and materializes `Overflowing` on read, so there is no wrapper in
145/// memory to borrow mutably, and handing out `&mut` to the raw integer would let
146/// writes bypass the checked arithmetic. It needs a checked bulk operation on the
147/// container instead, keeping the overflow check inside.
148fn negate_column<T>(column: Column<(Row, T, Diff)>) -> Column<(Row, T, Diff)>
149where
150    T: RenderTimestamp,
151{
152    /// Collects the negation of every diff in `diffs` into a fresh column.
153    fn negated_diffs<'a, D>(diffs: &'a D) -> <Diff as Columnar>::Container
154    where
155        D: Len + Index<Ref = Diff> + 'a,
156    {
157        let mut negated = <Diff as Columnar>::Container::default();
158        for index in 0..diffs.len() {
159            negated.push(-diffs.get(index));
160        }
161        negated
162    }
163
164    match column {
165        Column::Typed((rows, times, diffs)) => {
166            let negated = negated_diffs(&diffs.borrow());
167            Column::Typed((rows, times, negated))
168        }
169        column => {
170            let view = column.borrow();
171            let len = view.len();
172            let mut negated = <(Row, T, Diff) as Columnar>::Container::default();
173            let (rows, times, diffs) = &mut negated;
174            rows.extend_from_self(view.0, 0..len);
175            times.extend_from_self(view.1, 0..len);
176            *diffs = negated_diffs(&view.2);
177            Column::Typed(negated)
178        }
179    }
180}
181
182/// The timestamp of a scope that carries iteration coordinates.
183pub type RecTimestamp = Product<mz_repr::Timestamp, PointStamp<u64>>;
184
185/// Truncates every time in `column` to `level - 1` iteration coordinates.
186///
187/// A `Typed` input hands its row and diff columns over untouched, because truncation only
188/// rewrites times. A serialized input keeps all three columns in one buffer, so its rows
189/// and diffs are copied in bulk rather than per record.
190fn truncate_times(
191    column: Column<(Row, RecTimestamp, Diff)>,
192    level: usize,
193) -> Column<(Row, RecTimestamp, Diff)> {
194    /// Collects the truncation of every time in `times` into a fresh column.
195    fn truncated<'a, C>(times: C, level: usize) -> <RecTimestamp as Columnar>::Container
196    where
197        C: Len + Index<Ref = columnar::Ref<'a, RecTimestamp>> + 'a,
198    {
199        let mut truncated = <RecTimestamp as Columnar>::Container::default();
200        let mut time = RecTimestamp::default();
201        for reference in times.into_index_iter() {
202            time.copy_from(reference);
203            let mut coordinates = std::mem::take(&mut time.inner).into_inner();
204            coordinates.truncate(level - 1);
205            time.inner = PointStamp::new(coordinates);
206            truncated.push(&time);
207        }
208        truncated
209    }
210
211    match column {
212        Column::Typed((rows, times, diffs)) => {
213            let times = truncated(times.borrow(), level);
214            Column::Typed((rows, times, diffs))
215        }
216        column => {
217            let view = column.borrow();
218            let len = view.len();
219            let mut out = <(Row, RecTimestamp, Diff) as Columnar>::Container::default();
220            let (rows, times, diffs) = &mut out;
221            rows.extend_from_self(view.0, 0..len);
222            *times = truncated(view.1, level);
223            diffs.extend_from_self(view.2, 0..len);
224            Column::Typed(out)
225        }
226    }
227}
228
229/// Leaves a dynamically created scope that has `level` iteration coordinates.
230///
231/// The columnar counterpart of differential's `leave_dynamic`, which it offers only for
232/// `Vec` collections. Keeping the recursive binding's result on the edge is what lets the
233/// feedback loop run without a decode and a re-encode per iteration.
234pub fn columnar_leave_dynamic<'scope>(
235    collection: ColumnarCollection<'scope, RecTimestamp, Row, Diff>,
236    level: usize,
237) -> ColumnarCollection<'scope, RecTimestamp, Row, Diff> {
238    let scope = collection.inner.scope();
239    let mut builder = OperatorBuilder::new("ColumnarLeaveDynamic".to_string(), scope);
240    let (output, stream) = builder.new_output();
241    let mut output =
242        OutputBuilder::<_, NoopBuilder<Column<(Row, RecTimestamp, Diff)>>>::from(output);
243    // The connection summary tells the scope that this operator drops all but `level - 1`
244    // coordinates, so a downstream frontier is not held back by the iteration it leaves.
245    let summary = Product {
246        outer: Default::default(),
247        inner: PointStampSummary {
248            retain: Some(level - 1),
249            actions: Vec::new(),
250        },
251    };
252    let mut input = builder.new_input_connection(
253        collection.inner,
254        Pipeline,
255        [(0, Antichain::from_elem(summary))],
256    );
257
258    builder.build(move |_capability| {
259        move |_frontier| {
260            let mut output = output.activate();
261            input.for_each(|cap, data| {
262                // The iteration coordinates make the scope partially ordered, so a message
263                // can carry several capabilities and none of them dominates the rest. Each
264                // one holds for the records it covers, so each one is truncated and kept.
265                let caps: CapabilitySet<_> = cap
266                    .stamp()
267                    .iter()
268                    .map(|time| {
269                        let mut time = time.clone();
270                        let mut coordinates = std::mem::take(&mut time.inner).into_inner();
271                        coordinates.truncate(level - 1);
272                        time.inner = PointStamp::new(coordinates);
273                        cap.delayed(&time, 0)
274                    })
275                    .collect();
276                let mut truncated = truncate_times(std::mem::take(data), level);
277                output
278                    .session_with_builder(&caps)
279                    .give_container(&mut truncated);
280            });
281        }
282    });
283
284    stream.as_collection()
285}
286
287/// Negates the diff of every record in a [`ColumnarCollection`].
288pub fn columnar_negate<'scope, T>(
289    collection: ColumnarCollection<'scope, T, Row, Diff>,
290) -> ColumnarCollection<'scope, T, Row, Diff>
291where
292    T: RenderTimestamp,
293{
294    collection
295        .inner
296        .unary::<NoopBuilder<Column<(Row, T, Diff)>>, _, _, _>(
297            Pipeline,
298            "ColumnarNegate",
299            |_cap, _info| {
300                move |input, output| {
301                    input.for_each(|time, data| {
302                        let mut negated = negate_column(std::mem::take(data));
303                        output
304                            .session_with_builder(&time)
305                            .give_container(&mut negated);
306                    });
307                }
308            },
309        )
310        .as_collection()
311}
312
313/// Consolidates a [`ColumnarCollection`] natively, without a row round-trip.
314///
315/// A [`ChunkChunker`] sorts and consolidates the input columns and an
316/// [`AccountedChunkChainBatcher`] merges them, both holding their data in columnar form, so
317/// nothing outside the exchange pact visits a record or materializes an owned [`Row`].
318/// The batcher's chains are chunks, so the process buffer pool spills them while the
319/// chunk spill gate is set, bounding what a consolidation holds resident.
320///
321/// [`ChunkChunker`]: mz_timely_util::columnar::chunk::ChunkChunker
322///
323/// Uses [`consolidate_pact`] rather than `mz_arrange_core`: a consolidate emits a
324/// consolidated collection, so building and reading back a maintained trace would be
325/// wasted work.
326pub fn columnar_consolidate<'scope, T>(
327    collection: ColumnarCollection<'scope, T, Row, Diff>,
328    name: &str,
329) -> ColumnarCollection<'scope, T, Row, Diff>
330where
331    T: RenderTimestamp,
332{
333    // TODO: This pact re-serializes every record into a per-destination `ColumnBuilder`,
334    // the one remaining full re-encode on this path. Bulk routing needs contiguous ranges
335    // of records sharing a destination, which a per-record hash cannot identify.
336    let exchange = ExchangeCore::<ColumnBuilder<_>, _>::new_core(
337        columnar_consolidate_exchange::<Row, T, Diff>,
338    );
339    let consolidated =
340        consolidate_pact::<AccountedChunkChainBatcher<Row, T, Diff, ComputeSpill>, _, _>(
341            collection.inner,
342            exchange,
343            name,
344            AccountedChunkChainBatcher::new,
345        );
346
347    // Flatten the sealed chain into one container per chunk, loading a spilled body
348    // and moving a resident one, visiting no record either way.
349    //
350    // TODO: This ships a whole sealed snapshot in one activation, an un-fueled burst
351    // hazard on large consolidations. `consolidate_named`'s unpack does the same, so a
352    // fuel fix has to cover both.
353    consolidated
354        .unary::<NoopBuilder<Column<(Row, T, Diff)>>, _, _, _>(
355            Pipeline,
356            &format!("Flatten {name}"),
357            |_cap, _info| {
358                move |input, output| {
359                    input.for_each(|time, data| {
360                        let mut session = output.session_with_builder(&time);
361                        for chunk in data.drain(..).flatten() {
362                            let mut column = Column::from(chunk.into_body());
363                            session.give_container(&mut column);
364                        }
365                    });
366                }
367            },
368        )
369        .as_collection()
370}
371
372/// Repacks a row-based collection into columnar batches.
373///
374/// The leaf encode described in the module docs, named `VecToColumnar` in a rendered
375/// dataflow. Repacking copies row bytes and allocates no per-record `Row`.
376pub fn vec_to_columnar<'scope, T>(
377    collection: VecCollection<'scope, T, Row, Diff>,
378) -> ColumnarCollection<'scope, T, Row, Diff>
379where
380    T: RenderTimestamp,
381{
382    collection
383        .inner
384        .unary::<ColumnBuilder<(Row, T, Diff)>, _, _, _>(
385            Pipeline,
386            "VecToColumnar",
387            |_cap, _info| {
388                move |input, output| {
389                    input.for_each(|time, data| {
390                        let mut session = output.session_with_builder(&time);
391                        for (v, t, d) in data.drain(..) {
392                            session.give((&v, &t, &d));
393                        }
394                    });
395                }
396            },
397        )
398        .as_collection()
399}
400
401/// Decodes columnar batches into a row-based collection.
402///
403/// The leaf decode described in the module docs, named `ColumnarToVec` in a rendered
404/// dataflow. It allocates an owned [`Row`] per record, which is why it stays at those
405/// boundaries.
406pub fn columnar_to_vec<'scope, T>(
407    collection: ColumnarCollection<'scope, T, Row, Diff>,
408) -> VecCollection<'scope, T, Row, Diff>
409where
410    T: RenderTimestamp,
411{
412    collection
413        .inner
414        .unary::<CapacityContainerBuilder<Vec<(Row, T, Diff)>>, _, _, _>(
415            Pipeline,
416            "ColumnarToVec",
417            |_cap, _info| {
418                move |input, output| {
419                    input.for_each(|time, data| {
420                        let mut session = output.session(&time);
421                        for (v, t, d) in data.borrow().into_index_iter() {
422                            session.give((
423                                Columnar::into_owned(v),
424                                Columnar::into_owned(t),
425                                Columnar::into_owned(d),
426                            ));
427                        }
428                    });
429                }
430            },
431        )
432        .as_collection()
433}
434
435#[cfg(test)]
436mod tests {
437    use differential_dataflow::input::Input;
438    use mz_ore::cast::CastFrom;
439    use mz_repr::{Datum, Timestamp};
440    use timely::dataflow::operators::Capture;
441    use timely::dataflow::operators::capture::{Event, Extract};
442
443    use super::*;
444
445    type RowBuilder = CapacityContainerBuilder<Vec<(Row, Timestamp, Diff)>>;
446    type CapturedRows = std::sync::mpsc::Receiver<Event<Timestamp, Vec<(Row, Timestamp, Diff)>>>;
447
448    fn extract_sorted(captured: CapturedRows) -> Vec<(Row, Timestamp, Diff)> {
449        let mut updates: Vec<_> = captured
450            .extract()
451            .into_iter()
452            .flat_map(|(_, data)| data)
453            .collect();
454        updates.sort();
455        updates
456    }
457
458    fn test_rows() -> Vec<Row> {
459        vec![
460            Row::pack_slice(&[Datum::Int32(42), Datum::String("hello")]),
461            Row::pack_slice(&[Datum::Int64(100), Datum::Null]),
462            Row::pack_slice(&[Datum::True, Datum::False, Datum::Null]),
463            Row::default(),
464        ]
465    }
466
467    #[mz_ore::test]
468    fn round_trip_through_columnar() {
469        let rows = test_rows();
470        let expected: Vec<_> = {
471            let mut updates: Vec<_> = rows
472                .iter()
473                .enumerate()
474                .map(|(i, r)| (r.clone(), Timestamp::from(u64::cast_from(i / 2)), Diff::ONE))
475                .collect();
476            updates.sort();
477            updates
478        };
479        let captured = timely::execute_directly(move |worker| {
480            worker.dataflow::<Timestamp, _, _>(|scope| {
481                let (mut input, collection) = scope.new_collection();
482                let captured = columnar_to_vec(vec_to_columnar(collection)).inner.capture();
483                for (i, row) in rows.into_iter().enumerate() {
484                    input.advance_to(Timestamp::from(u64::cast_from(i / 2)));
485                    input.update(row, Diff::ONE);
486                }
487                input.advance_to(Timestamp::from(2_u64));
488                input.flush();
489                captured
490            })
491        });
492        assert_eq!(extract_sorted(captured), expected);
493    }
494
495    #[mz_ore::test]
496    fn columnar_negate_flips_diffs() {
497        let rows = test_rows();
498        let expected: Vec<_> = {
499            let mut updates: Vec<_> = rows
500                .iter()
501                .map(|r| (r.clone(), Timestamp::from(0_u64), -Diff::ONE))
502                .collect();
503            updates.sort();
504            updates
505        };
506        let captured = timely::execute_directly(move |worker| {
507            worker.dataflow::<Timestamp, _, _>(|scope| {
508                let (mut input, collection) = scope.new_collection();
509                let edge = columnar_negate(vec_to_columnar(collection));
510                let captured = columnar_to_vec(edge).inner.capture();
511                for row in rows {
512                    input.update(row, Diff::ONE);
513                }
514                input.advance_to(Timestamp::from(1_u64));
515                input.flush();
516                captured
517            })
518        });
519        assert_eq!(extract_sorted(captured), expected);
520    }
521
522    #[mz_ore::test]
523    fn concat_many_concatenates_columnar() {
524        let rows = test_rows();
525        let expected: Vec<_> = {
526            let mut updates: Vec<_> = rows
527                .iter()
528                .map(|r| (r.clone(), Timestamp::from(0_u64), Diff::ONE))
529                .collect();
530            // The first row arrives on both inputs.
531            updates.push((rows[0].clone(), Timestamp::from(0_u64), Diff::ONE));
532            updates.sort();
533            updates
534        };
535        let captured = timely::execute_directly(move |worker| {
536            worker.dataflow::<Timestamp, _, _>(|scope| {
537                let (mut input1, collection1) = scope.new_collection();
538                let (mut input2, collection2) = scope.new_collection();
539                let edge = concat_many(
540                    scope,
541                    [vec_to_columnar(collection1), vec_to_columnar(collection2)],
542                );
543                let captured = columnar_to_vec(edge).inner.capture();
544                let (first, rest) = rows.split_first().unwrap();
545                input1.update(first.clone(), Diff::ONE);
546                input2.update(first.clone(), Diff::ONE);
547                for row in rest {
548                    input1.update(row.clone(), Diff::ONE);
549                }
550                for input in [&mut input1, &mut input2] {
551                    input.advance_to(Timestamp::from(1_u64));
552                    input.flush();
553                }
554                captured
555            })
556        });
557        assert_eq!(extract_sorted(captured), expected);
558    }
559
560    #[mz_ore::test]
561    fn flat_map_datums_arms_agree() {
562        // Project the first datum of each row, exercising `max_demand`.
563        let rows = test_rows();
564        let captured = timely::execute_directly(move |worker| {
565            worker.dataflow::<Timestamp, _, _>(|scope| {
566                let (mut input, collection) = scope.new_collection();
567                let (oks, _errs) = flat_map_datums::<_, RowBuilder, _>(
568                    vec_to_columnar(collection),
569                    "test",
570                    1,
571                    |datums, t, d, ok_session, _err_session| {
572                        ok_session.give((Row::pack(datums.iter()), t, d));
573                        1
574                    },
575                );
576                let captured = oks.capture();
577                for row in rows {
578                    input.update(row, Diff::ONE);
579                }
580                input.advance_to(Timestamp::from(1_u64));
581                input.flush();
582                captured
583            })
584        });
585        let updates = extract_sorted(captured);
586        assert!(!updates.is_empty());
587        // Each output row retains at most the first datum of its input.
588        assert!(updates.iter().all(|(r, _, _)| r.iter().count() <= 1));
589    }
590
591    #[mz_ore::test]
592    fn columnar_consolidate_accumulates_and_cancels() {
593        let row1 = Row::pack_slice(&[Datum::Int32(1)]);
594        let row2 = Row::pack_slice(&[Datum::Int32(2)]);
595        let row3 = Row::pack_slice(&[Datum::Int32(3)]);
596        // `row1` accumulates at t=0 and again at t=1, kept apart by time. `row2` cancels
597        // at t=0 and `row3` at t=1, so neither reaches the output.
598        let expected = vec![
599            (row1.clone(), Timestamp::from(0_u64), Diff::from(2)),
600            (row1.clone(), Timestamp::from(1_u64), Diff::ONE),
601        ];
602
603        let captured = timely::execute_directly(move |worker| {
604            worker.dataflow::<Timestamp, _, _>(|scope| {
605                let (mut input, collection) = scope.new_collection();
606                let edge = columnar_consolidate(vec_to_columnar(collection), "Test");
607                let captured = columnar_to_vec(edge).inner.capture();
608                // t=0: row1 accumulates (+1, +1), row2 cancels (+1, -1).
609                input.advance_to(Timestamp::from(0_u64));
610                input.update(row1.clone(), Diff::ONE);
611                input.update(row1.clone(), Diff::ONE);
612                input.update(row2.clone(), Diff::ONE);
613                input.update(row2, -Diff::ONE);
614                // t=1: row1 survives (+1), row3 cancels (+1, -1).
615                input.advance_to(Timestamp::from(1_u64));
616                input.update(row1, Diff::ONE);
617                input.update(row3.clone(), Diff::ONE);
618                input.update(row3, -Diff::ONE);
619                input.advance_to(Timestamp::from(2_u64));
620                input.flush();
621                captured
622            })
623        });
624        assert_eq!(extract_sorted(captured), expected);
625    }
626
627    /// Consolidation over a payload big enough to spill: the chunks the batcher
628    /// commits land in the pool, and flattening loads them back whole.
629    #[mz_ore::test]
630    #[cfg_attr(miri, ignore)] // the pool's mmap and madvise calls are unsupported under miri
631    fn columnar_consolidate_spills_and_round_trips() {
632        use mz_ore::pool::Pool;
633        use mz_timely_util::columnar::chunk::set_spill_override;
634
635        // Enough bytes for a committed chunk to clear the pool's 64 KiB spill
636        // floor. Every row is distinct, so consolidation cancels nothing and the
637        // whole payload has to survive the round trip through the pool.
638        let rows: Vec<Row> = (0..40_000i64)
639            .map(|i| Row::pack_slice(&[Datum::Int64(i), Datum::String("a repeated string value")]))
640            .collect();
641        let expected = rows.len();
642
643        let pool = Pool::new().expect("pool creation");
644        // The override is per-thread and `execute_directly` runs the worker on
645        // this one, so it covers the dataflow below and nothing else.
646        set_spill_override(Some(pool.clone()));
647        let captured = timely::execute_directly(move |worker| {
648            worker.dataflow::<Timestamp, _, _>(|scope| {
649                let (mut input, collection) = scope.new_collection();
650                let edge = columnar_consolidate(vec_to_columnar(collection), "Test");
651                let captured = columnar_to_vec(edge).inner.capture();
652                input.advance_to(Timestamp::from(0_u64));
653                for row in rows {
654                    input.update(row, Diff::ONE);
655                }
656                input.advance_to(Timestamp::from(1_u64));
657                input.flush();
658                captured
659            })
660        });
661        set_spill_override(None);
662
663        assert!(
664            pool.stats().inserts > 0,
665            "the payload should have reached the pool"
666        );
667        let updates = extract_sorted(captured);
668        assert_eq!(updates.len(), expected);
669        assert!(
670            updates
671                .iter()
672                .all(|(_, time, diff)| *time == Timestamp::from(0_u64) && *diff == Diff::ONE),
673            "every row survives at its own time and multiplicity"
674        );
675    }
676}