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 [`CollectionEdge`], the columnar batch representation that dataflow
13//! edges between Plan 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::{AsCollection, Collection, VecCollection};
25use mz_repr::{DatumVec, DatumVecBorrow, Diff, Row};
26use mz_timely_util::columnar::Column;
27use mz_timely_util::columnar::batcher::ColumnChunker;
28use mz_timely_util::columnar::builder::ColumnBuilder;
29use mz_timely_util::columnar::columnar_consolidate_exchange;
30use mz_timely_util::columnar::merge_batcher::ColumnMergeBatcher;
31use mz_timely_util::operator::consolidate_pact;
32use timely::ContainerBuilder;
33use timely::container::CapacityContainerBuilder;
34use timely::dataflow::channels::pact::{ExchangeCore, Pipeline};
35use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
36use timely::dataflow::operators::generic::{Operator, OutputBuilder};
37use timely::dataflow::{Scope, Stream, StreamVec};
38
39use crate::render::RenderTimestamp;
40use crate::render::context::{ECB, Session};
41use crate::render::errors::DataflowErrorSer;
42
43/// A columnar collection of `(D, T, R)` updates traveling on a compute
44/// dataflow edge.
45///
46/// Mirrors differential's [`VecCollection<'scope, T, D, R>`]; the underlying
47/// container is [`Column<(D, T, R)>`] instead of `Vec<(D, T, R)>`.
48pub type ColumnarCollection<'scope, T, D, R> = Collection<'scope, T, Column<(D, T, R)>>;
49
50/// A dataflow edge between Plan nodes: a columnar collection of `(Row, Diff)` updates.
51pub type CollectionEdge<'scope, T> = ColumnarCollection<'scope, T, Row, Diff>;
52
53/// Concatenates a collection of columnar edges.
54pub fn concat_many<'scope, T, I>(scope: Scope<'scope, T>, edges: I) -> CollectionEdge<'scope, T>
55where
56    T: RenderTimestamp,
57    I: IntoIterator<Item = CollectionEdge<'scope, T>>,
58{
59    let cols: Vec<_> = edges.into_iter().collect();
60    differential_dataflow::collection::concatenate(scope, cols)
61}
62
63/// Applies `logic` to each record in `edge`, exposing the record as a borrowed
64/// [`DatumVecBorrow`] and giving it ok and err output sessions.
65///
66/// `max_demand` bounds the number of columns decoded per row. Pass `usize::MAX`
67/// to decode all columns.
68///
69/// This is the canonical entry point for "decoding consumers" (operators that
70/// read [`mz_repr::Datum`]s from each row anyway). It iterates the columnar
71/// batch directly without going through an owned [`Row`].
72pub fn flat_map_datums<'scope, T, DCB, L>(
73    edge: CollectionEdge<'scope, T>,
74    max_demand: usize,
75    mut logic: L,
76) -> (
77    Stream<'scope, T, DCB::Container>,
78    StreamVec<'scope, T, (DataflowErrorSer, T, Diff)>,
79)
80where
81    T: RenderTimestamp,
82    DCB: ContainerBuilder,
83    L: for<'a> FnMut(
84            &'a mut DatumVecBorrow<'_>,
85            T,
86            Diff,
87            &mut Session<T, DCB>,
88            &mut Session<T, ECB<T>>,
89        ) -> usize
90        + 'static,
91{
92    let scope = edge.inner.scope();
93    let mut builder = OperatorBuilder::new("CollectionFlatMap".to_string(), scope);
94    let (ok_output, ok_stream) = builder.new_output();
95    let mut ok_output = OutputBuilder::<_, DCB>::from(ok_output);
96    let (err_output, err_stream) = builder.new_output();
97    let mut err_output = OutputBuilder::<_, ECB<T>>::from(err_output);
98    let mut input = builder.new_input(edge.inner, Pipeline);
99    builder.build(move |_capabilities| {
100        let mut datums = DatumVec::new();
101        move |_frontiers| {
102            let mut ok_output = ok_output.activate();
103            let mut err_output = err_output.activate();
104            input.for_each(|time, data| {
105                // Retain the input capability to derive a `Capability` for each
106                // output. The `Session` type alias is fixed to `Capability<T>`.
107                let ok_cap = time.retain(0);
108                let err_cap = time.retain(1);
109                let mut ok_session = ok_output.session_with_builder(&ok_cap);
110                let mut err_session = err_output.session_with_builder(&err_cap);
111                // Rows are read from the borrowed column, never materialized as
112                // owned `Row`s.
113                for (v, t, d) in data.borrow().into_index_iter() {
114                    logic(
115                        &mut datums.borrow_with_limit(v, max_demand),
116                        Columnar::into_owned(t),
117                        Columnar::into_owned(d),
118                        &mut ok_session,
119                        &mut err_session,
120                    );
121                }
122            });
123        }
124    });
125    (ok_stream, err_stream)
126}
127
128/// Negates the diff of every record in `column`, rebuilding only the diff column.
129///
130/// A `Typed` input hands its row and time columns over untouched, so only the
131/// diffs are rebuilt, which is 8 bytes per record. A serialized input keeps all
132/// three columns in one buffer, so its rows and times are copied in bulk.
133///
134/// Negation stays checked: `Neg for Overflowing` runs `overflowing_neg` and
135/// reports overflow, which `-Diff::MIN` triggers.
136///
137/// TODO: Negate a `Typed` input's diffs in place rather than rebuilding them.
138/// This cannot go through `columnar::IndexMut`: `Overflows` stores the raw
139/// integer and materializes `Overflowing` on read, so there is no wrapper in
140/// memory to borrow mutably, and handing out `&mut` to the raw integer would let
141/// writes bypass the checked arithmetic. It needs a checked bulk operation on the
142/// container instead, keeping the overflow check inside.
143fn negate_column<T>(column: Column<(Row, T, Diff)>) -> Column<(Row, T, Diff)>
144where
145    T: RenderTimestamp,
146{
147    /// Collects the negation of every diff in `diffs` into a fresh column.
148    fn negated_diffs<'a, D>(diffs: &'a D) -> <Diff as Columnar>::Container
149    where
150        D: Len + Index<Ref = Diff> + 'a,
151    {
152        let mut negated = <Diff as Columnar>::Container::default();
153        for index in 0..diffs.len() {
154            negated.push(-diffs.get(index));
155        }
156        negated
157    }
158
159    match column {
160        Column::Typed((rows, times, diffs)) => {
161            let negated = negated_diffs(&diffs.borrow());
162            Column::Typed((rows, times, negated))
163        }
164        column => {
165            let view = column.borrow();
166            let len = view.len();
167            let mut negated = <(Row, T, Diff) as Columnar>::Container::default();
168            let (rows, times, diffs) = &mut negated;
169            rows.extend_from_self(view.0, 0..len);
170            times.extend_from_self(view.1, 0..len);
171            *diffs = negated_diffs(&view.2);
172            Column::Typed(negated)
173        }
174    }
175}
176
177/// Negates the diff of every record in a [`ColumnarCollection`].
178pub fn columnar_negate<'scope, T>(
179    collection: ColumnarCollection<'scope, T, Row, Diff>,
180) -> ColumnarCollection<'scope, T, Row, Diff>
181where
182    T: RenderTimestamp,
183{
184    collection
185        .inner
186        .unary::<CapacityContainerBuilder<Column<(Row, T, Diff)>>, _, _, _>(
187            Pipeline,
188            "ColumnarNegate",
189            |_cap, _info| {
190                move |input, output| {
191                    input.for_each(|time, data| {
192                        let mut negated = negate_column(std::mem::take(data));
193                        output.session(&time).give_container(&mut negated);
194                    });
195                }
196            },
197        )
198        .as_collection()
199}
200
201/// Consolidates a [`ColumnarCollection`] natively, without a row round-trip.
202///
203/// A [`ColumnChunker`] sorts and consolidates the input columns and a
204/// [`ColumnMergeBatcher`] merges them, both holding their data in [`Column`], so nothing
205/// outside the exchange pact visits a record or materializes an owned [`Row`].
206///
207/// Uses [`consolidate_pact`] rather than `mz_arrange_core`: a consolidate emits a
208/// consolidated collection, so building and reading back a maintained trace would be
209/// wasted work.
210pub fn columnar_consolidate<'scope, T>(
211    collection: ColumnarCollection<'scope, T, Row, Diff>,
212    name: &str,
213) -> ColumnarCollection<'scope, T, Row, Diff>
214where
215    T: RenderTimestamp,
216{
217    // TODO: This pact re-serializes every record into a per-destination `ColumnBuilder`,
218    // the one remaining full re-encode on this path. Bulk routing needs contiguous ranges
219    // of records sharing a destination, which a per-record hash cannot identify.
220    let exchange = ExchangeCore::<ColumnBuilder<_>, _>::new_core(
221        columnar_consolidate_exchange::<Row, T, Diff>,
222    );
223    let consolidated = consolidate_pact::<
224        ColumnChunker<(Row, T, Diff)>,
225        ColumnMergeBatcher<Row, T, Diff>,
226        _,
227        _,
228    >(collection.inner, exchange, name);
229
230    // Flatten the sealed chain into one container per chunk, moving containers and
231    // visiting no record.
232    //
233    // TODO: This ships a whole sealed snapshot in one activation, an un-fueled burst
234    // hazard on large consolidations. `consolidate_named`'s unpack does the same, so a
235    // fuel fix has to cover both.
236    consolidated
237        .unary::<CapacityContainerBuilder<Column<(Row, T, Diff)>>, _, _, _>(
238            Pipeline,
239            &format!("Flatten {name}"),
240            |_cap, _info| {
241                move |input, output| {
242                    input.for_each(|time, data| {
243                        let mut session = output.session(&time);
244                        for mut chunk in data.drain(..).flatten() {
245                            session.give_container(&mut chunk);
246                        }
247                    });
248                }
249            },
250        )
251        .as_collection()
252}
253
254/// Repacks a row-based collection into columnar batches.
255///
256/// The leaf encode described in the module docs, named `VecToColumnar` in a rendered
257/// dataflow. Repacking copies row bytes and allocates no per-record `Row`.
258pub fn vec_to_columnar<'scope, T>(
259    collection: VecCollection<'scope, T, Row, Diff>,
260) -> ColumnarCollection<'scope, T, Row, Diff>
261where
262    T: RenderTimestamp,
263{
264    collection
265        .inner
266        .unary::<ColumnBuilder<(Row, T, Diff)>, _, _, _>(
267            Pipeline,
268            "VecToColumnar",
269            |_cap, _info| {
270                move |input, output| {
271                    input.for_each(|time, data| {
272                        let mut session = output.session_with_builder(&time);
273                        for (v, t, d) in data.drain(..) {
274                            session.give((&v, &t, &d));
275                        }
276                    });
277                }
278            },
279        )
280        .as_collection()
281}
282
283/// Decodes columnar batches into a row-based collection.
284///
285/// The leaf decode described in the module docs, named `ColumnarToVec` in a rendered
286/// dataflow. It allocates an owned [`Row`] per record, which is why it stays at those
287/// boundaries.
288pub fn columnar_to_vec<'scope, T>(
289    collection: ColumnarCollection<'scope, T, Row, Diff>,
290) -> VecCollection<'scope, T, Row, Diff>
291where
292    T: RenderTimestamp,
293{
294    collection
295        .inner
296        .unary::<CapacityContainerBuilder<Vec<(Row, T, Diff)>>, _, _, _>(
297            Pipeline,
298            "ColumnarToVec",
299            |_cap, _info| {
300                move |input, output| {
301                    input.for_each(|time, data| {
302                        let mut session = output.session(&time);
303                        for (v, t, d) in data.borrow().into_index_iter() {
304                            session.give((
305                                Columnar::into_owned(v),
306                                Columnar::into_owned(t),
307                                Columnar::into_owned(d),
308                            ));
309                        }
310                    });
311                }
312            },
313        )
314        .as_collection()
315}
316
317#[cfg(test)]
318mod tests {
319    use differential_dataflow::input::Input;
320    use mz_ore::cast::CastFrom;
321    use mz_repr::{Datum, Timestamp};
322    use timely::dataflow::operators::Capture;
323    use timely::dataflow::operators::capture::{Event, Extract};
324
325    use super::*;
326
327    type RowBuilder = CapacityContainerBuilder<Vec<(Row, Timestamp, Diff)>>;
328    type CapturedRows = std::sync::mpsc::Receiver<Event<Timestamp, Vec<(Row, Timestamp, Diff)>>>;
329
330    fn extract_sorted(captured: CapturedRows) -> Vec<(Row, Timestamp, Diff)> {
331        let mut updates: Vec<_> = captured
332            .extract()
333            .into_iter()
334            .flat_map(|(_, data)| data)
335            .collect();
336        updates.sort();
337        updates
338    }
339
340    fn test_rows() -> Vec<Row> {
341        vec![
342            Row::pack_slice(&[Datum::Int32(42), Datum::String("hello")]),
343            Row::pack_slice(&[Datum::Int64(100), Datum::Null]),
344            Row::pack_slice(&[Datum::True, Datum::False, Datum::Null]),
345            Row::default(),
346        ]
347    }
348
349    #[mz_ore::test]
350    fn round_trip_through_columnar() {
351        let rows = test_rows();
352        let expected: Vec<_> = {
353            let mut updates: Vec<_> = rows
354                .iter()
355                .enumerate()
356                .map(|(i, r)| (r.clone(), Timestamp::from(u64::cast_from(i / 2)), Diff::ONE))
357                .collect();
358            updates.sort();
359            updates
360        };
361        let captured = timely::execute_directly(move |worker| {
362            worker.dataflow::<Timestamp, _, _>(|scope| {
363                let (mut input, collection) = scope.new_collection();
364                let captured = columnar_to_vec(vec_to_columnar(collection)).inner.capture();
365                for (i, row) in rows.into_iter().enumerate() {
366                    input.advance_to(Timestamp::from(u64::cast_from(i / 2)));
367                    input.update(row, Diff::ONE);
368                }
369                input.advance_to(Timestamp::from(2_u64));
370                input.flush();
371                captured
372            })
373        });
374        assert_eq!(extract_sorted(captured), expected);
375    }
376
377    #[mz_ore::test]
378    fn columnar_negate_flips_diffs() {
379        let rows = test_rows();
380        let expected: Vec<_> = {
381            let mut updates: Vec<_> = rows
382                .iter()
383                .map(|r| (r.clone(), Timestamp::from(0_u64), -Diff::ONE))
384                .collect();
385            updates.sort();
386            updates
387        };
388        let captured = timely::execute_directly(move |worker| {
389            worker.dataflow::<Timestamp, _, _>(|scope| {
390                let (mut input, collection) = scope.new_collection();
391                let edge = columnar_negate(vec_to_columnar(collection));
392                let captured = columnar_to_vec(edge).inner.capture();
393                for row in rows {
394                    input.update(row, Diff::ONE);
395                }
396                input.advance_to(Timestamp::from(1_u64));
397                input.flush();
398                captured
399            })
400        });
401        assert_eq!(extract_sorted(captured), expected);
402    }
403
404    #[mz_ore::test]
405    fn concat_many_concatenates_columnar() {
406        let rows = test_rows();
407        let expected: Vec<_> = {
408            let mut updates: Vec<_> = rows
409                .iter()
410                .map(|r| (r.clone(), Timestamp::from(0_u64), Diff::ONE))
411                .collect();
412            // The first row arrives on both inputs.
413            updates.push((rows[0].clone(), Timestamp::from(0_u64), Diff::ONE));
414            updates.sort();
415            updates
416        };
417        let captured = timely::execute_directly(move |worker| {
418            worker.dataflow::<Timestamp, _, _>(|scope| {
419                let (mut input1, collection1) = scope.new_collection();
420                let (mut input2, collection2) = scope.new_collection();
421                let edge = concat_many(
422                    scope,
423                    [vec_to_columnar(collection1), vec_to_columnar(collection2)],
424                );
425                let captured = columnar_to_vec(edge).inner.capture();
426                let (first, rest) = rows.split_first().unwrap();
427                input1.update(first.clone(), Diff::ONE);
428                input2.update(first.clone(), Diff::ONE);
429                for row in rest {
430                    input1.update(row.clone(), Diff::ONE);
431                }
432                for input in [&mut input1, &mut input2] {
433                    input.advance_to(Timestamp::from(1_u64));
434                    input.flush();
435                }
436                captured
437            })
438        });
439        assert_eq!(extract_sorted(captured), expected);
440    }
441
442    #[mz_ore::test]
443    fn flat_map_datums_arms_agree() {
444        // Project the first datum of each row, exercising `max_demand`.
445        let rows = test_rows();
446        let captured = timely::execute_directly(move |worker| {
447            worker.dataflow::<Timestamp, _, _>(|scope| {
448                let (mut input, collection) = scope.new_collection();
449                let (oks, _errs) = flat_map_datums::<_, RowBuilder, _>(
450                    vec_to_columnar(collection),
451                    1,
452                    |datums, t, d, ok_session, _err_session| {
453                        ok_session.give((Row::pack(datums.iter()), t, d));
454                        1
455                    },
456                );
457                let captured = oks.capture();
458                for row in rows {
459                    input.update(row, Diff::ONE);
460                }
461                input.advance_to(Timestamp::from(1_u64));
462                input.flush();
463                captured
464            })
465        });
466        let updates = extract_sorted(captured);
467        assert!(!updates.is_empty());
468        // Each output row retains at most the first datum of its input.
469        assert!(updates.iter().all(|(r, _, _)| r.iter().count() <= 1));
470    }
471
472    #[mz_ore::test]
473    fn columnar_consolidate_accumulates_and_cancels() {
474        let row1 = Row::pack_slice(&[Datum::Int32(1)]);
475        let row2 = Row::pack_slice(&[Datum::Int32(2)]);
476        let row3 = Row::pack_slice(&[Datum::Int32(3)]);
477        // `row1` accumulates at t=0 and again at t=1, kept apart by time. `row2` cancels
478        // at t=0 and `row3` at t=1, so neither reaches the output.
479        let expected = vec![
480            (row1.clone(), Timestamp::from(0_u64), Diff::from(2)),
481            (row1.clone(), Timestamp::from(1_u64), Diff::ONE),
482        ];
483
484        let captured = timely::execute_directly(move |worker| {
485            worker.dataflow::<Timestamp, _, _>(|scope| {
486                let (mut input, collection) = scope.new_collection();
487                let edge = columnar_consolidate(vec_to_columnar(collection), "Test");
488                let captured = columnar_to_vec(edge).inner.capture();
489                // t=0: row1 accumulates (+1, +1), row2 cancels (+1, -1).
490                input.advance_to(Timestamp::from(0_u64));
491                input.update(row1.clone(), Diff::ONE);
492                input.update(row1.clone(), Diff::ONE);
493                input.update(row2.clone(), Diff::ONE);
494                input.update(row2, -Diff::ONE);
495                // t=1: row1 survives (+1), row3 cancels (+1, -1).
496                input.advance_to(Timestamp::from(1_u64));
497                input.update(row1, Diff::ONE);
498                input.update(row3.clone(), Diff::ONE);
499                input.update(row3, -Diff::ONE);
500                input.advance_to(Timestamp::from(2_u64));
501                input.flush();
502                captured
503            })
504        });
505        assert_eq!(extract_sorted(captured), expected);
506    }
507}