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`], a wrapper that lets dataflow edges between Plan
13//! nodes carry either row-based ([`VecCollection`]) or columnar
14//! ([`ColumnarCollection`]) batches of `(D, T, R)` updates.
15//!
16//! # Migration model
17//!
18//! The migration is consumer-first: every Plan-node consumer learns to accept
19//! both variants before any producer emits the columnar variant. Producers can
20//! then flip to columnar one at a time.
21//!
22//! Within a Plan node, operators may freely materialize Vec collections; only
23//! the inter-node edge format is constrained. A decode from columnar to Vec at
24//! a consumer's input is acceptable only when the consumer would have decoded
25//! `Row` to [`mz_repr::Datum`] anyway. Pure passthrough consumers (Negate,
26//! Union) round-trip the columnar variant without decoding.
27//!
28//! Consumers that have not yet learned the columnar form fall back to
29//! [`CollectionEdge::into_vec`], which decodes through the named
30//! `ColumnarToVec` operator. Repack seams therefore stay visible in dataflow
31//! introspection, so they can be found and retired.
32
33use columnar::{Columnar, Index};
34use differential_dataflow::{AsCollection, Collection, VecCollection};
35use mz_repr::{DatumVec, DatumVecBorrow, Diff, Row};
36use mz_timely_util::columnar::Column;
37use mz_timely_util::columnar::builder::ColumnBuilder;
38use mz_timely_util::operator::CollectionExt;
39use timely::ContainerBuilder;
40use timely::container::CapacityContainerBuilder;
41use timely::dataflow::channels::pact::Pipeline;
42use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
43use timely::dataflow::operators::generic::{Operator, OutputBuilder};
44use timely::dataflow::{Scope, Stream, StreamVec};
45
46use crate::render::RenderTimestamp;
47use crate::render::context::{ECB, Session};
48use crate::render::errors::DataflowErrorSer;
49use crate::typedefs::KeyBatcher;
50
51/// A columnar collection of `(D, T, R)` updates traveling on a compute
52/// dataflow edge.
53///
54/// Mirrors differential's [`VecCollection<'scope, T, D, R>`]; the underlying
55/// container is [`Column<(D, T, R)>`] instead of `Vec<(D, T, R)>`.
56pub type ColumnarCollection<'scope, T, D, R> = Collection<'scope, T, Column<(D, T, R)>>;
57
58/// A dataflow edge carrying records as either a row-based [`VecCollection`] or
59/// a [`ColumnarCollection`].
60///
61/// Producers choose a variant; consumers must accept either. Variant-mixing
62/// `concat`s repack the row-based inputs and produce the columnar variant.
63#[derive(Clone)]
64pub enum CollectionEdge<'scope, T: RenderTimestamp> {
65    /// Row-formatted collection. Today's default for every producer.
66    Vec(VecCollection<'scope, T, Row, Diff>),
67    /// Columnar collection. Currently unused by any producer; reserved for the
68    /// producer flip at the end of the migration.
69    Columnar(ColumnarCollection<'scope, T, Row, Diff>),
70}
71
72impl<'scope, T: RenderTimestamp> CollectionEdge<'scope, T> {
73    /// The scope containing this edge.
74    pub fn scope(&self) -> Scope<'scope, T> {
75        match self {
76            CollectionEdge::Vec(c) => c.inner.scope(),
77            CollectionEdge::Columnar(c) => c.inner.scope(),
78        }
79    }
80
81    /// Brings the edge into a sub-region of its current scope.
82    pub fn enter_region<'inner>(self, region: Scope<'inner, T>) -> CollectionEdge<'inner, T> {
83        match self {
84            CollectionEdge::Vec(c) => CollectionEdge::Vec(c.enter_region(region)),
85            CollectionEdge::Columnar(c) => CollectionEdge::Columnar(c.enter_region(region)),
86        }
87    }
88
89    /// Leaves a sub-region back to the outer scope.
90    pub fn leave_region<'outer>(self, outer: Scope<'outer, T>) -> CollectionEdge<'outer, T> {
91        match self {
92            CollectionEdge::Vec(c) => CollectionEdge::Vec(c.leave_region(outer)),
93            CollectionEdge::Columnar(c) => CollectionEdge::Columnar(c.leave_region(outer)),
94        }
95    }
96
97    /// The edge as a row-based [`VecCollection`].
98    ///
99    /// The Vec arm is returned as is. The columnar arm decodes through
100    /// [`columnar_to_vec`], which allocates an owned [`Row`] per record.
101    /// Consumers that can work on the columnar form directly should do so
102    /// instead of calling this.
103    pub fn into_vec(self) -> VecCollection<'scope, T, Row, Diff> {
104        match self {
105            CollectionEdge::Vec(c) => c,
106            CollectionEdge::Columnar(c) => columnar_to_vec(c),
107        }
108    }
109
110    /// Negates the diff on every record in this edge.
111    ///
112    /// Preserves variant. The columnar arm uses [`columnar_negate`], which
113    /// negates diffs without decoding rows.
114    pub fn negate(self) -> Self {
115        match self {
116            CollectionEdge::Vec(c) => CollectionEdge::Vec(c.negate()),
117            CollectionEdge::Columnar(c) => CollectionEdge::Columnar(columnar_negate(c)),
118        }
119    }
120
121    /// Concatenates a collection of edges.
122    ///
123    /// Edges of one shared variant concatenate natively. Mixed inputs upgrade
124    /// the row-based edges through [`vec_to_columnar`] and produce the
125    /// columnar variant. Repacking rows into columns copies bytes but
126    /// allocates no per-record `Row`s, so upgrading is the cheap direction.
127    pub fn concat_many<I>(scope: Scope<'scope, T>, edges: I) -> Self
128    where
129        I: IntoIterator<Item = Self>,
130    {
131        let mut vecs = Vec::new();
132        let mut cols = Vec::new();
133        for edge in edges {
134            match edge {
135                CollectionEdge::Vec(c) => vecs.push(c),
136                CollectionEdge::Columnar(c) => cols.push(c),
137            }
138        }
139        if cols.is_empty() {
140            CollectionEdge::Vec(differential_dataflow::collection::concatenate(scope, vecs))
141        } else {
142            cols.extend(vecs.into_iter().map(vec_to_columnar));
143            CollectionEdge::Columnar(differential_dataflow::collection::concatenate(scope, cols))
144        }
145    }
146
147    /// Applies `logic` to each record in this edge, exposing the record as a
148    /// borrowed [`DatumVecBorrow`] and giving it ok and err output sessions.
149    ///
150    /// `max_demand` bounds the number of columns decoded per row; pass
151    /// `usize::MAX` to decode all columns.
152    ///
153    /// This is the canonical unified entry point for "decoding consumers"
154    /// (operators that read [`mz_repr::Datum`]s from each row anyway). The
155    /// Vec arm uses [`DatumVec::borrow_with_limit`] on each [`Row`]; the
156    /// Columnar arm iterates the columnar batch directly without going
157    /// through an owned [`Row`].
158    pub fn flat_map_datums<DCB, L>(
159        self,
160        max_demand: usize,
161        mut logic: L,
162    ) -> (
163        Stream<'scope, T, DCB::Container>,
164        StreamVec<'scope, T, (DataflowErrorSer, T, Diff)>,
165    )
166    where
167        DCB: ContainerBuilder,
168        L: for<'a> FnMut(
169                &'a mut DatumVecBorrow<'_>,
170                T,
171                Diff,
172                &mut Session<T, DCB>,
173                &mut Session<T, ECB<T>>,
174            ) -> usize
175            + 'static,
176    {
177        match self {
178            CollectionEdge::Vec(c) => {
179                let scope = c.inner.scope();
180                let mut builder = OperatorBuilder::new("CollectionFlatMap".to_string(), scope);
181                let (ok_output, ok_stream) = builder.new_output();
182                let mut ok_output = OutputBuilder::<_, DCB>::from(ok_output);
183                let (err_output, err_stream) = builder.new_output();
184                let mut err_output = OutputBuilder::<_, ECB<T>>::from(err_output);
185                let mut input = builder.new_input(c.inner, Pipeline);
186                builder.build(move |_capabilities| {
187                    let mut datums = DatumVec::new();
188                    move |_frontiers| {
189                        let mut ok_output = ok_output.activate();
190                        let mut err_output = err_output.activate();
191                        input.for_each(|time, data| {
192                            // Retain the input capability to derive a `Capability` for each output;
193                            // the `Session` type alias is fixed to `Capability<T>`.
194                            let ok_cap = time.retain(0);
195                            let err_cap = time.retain(1);
196                            let mut ok_session = ok_output.session_with_builder(&ok_cap);
197                            let mut err_session = err_output.session_with_builder(&err_cap);
198                            for (v, t, d) in data.drain(..) {
199                                logic(
200                                    &mut datums.borrow_with_limit(&v, max_demand),
201                                    t,
202                                    d,
203                                    &mut ok_session,
204                                    &mut err_session,
205                                );
206                            }
207                        });
208                    }
209                });
210                (ok_stream, err_stream)
211            }
212            CollectionEdge::Columnar(c) => {
213                let scope = c.inner.scope();
214                let mut builder = OperatorBuilder::new("CollectionFlatMap".to_string(), scope);
215                let (ok_output, ok_stream) = builder.new_output();
216                let mut ok_output = OutputBuilder::<_, DCB>::from(ok_output);
217                let (err_output, err_stream) = builder.new_output();
218                let mut err_output = OutputBuilder::<_, ECB<T>>::from(err_output);
219                let mut input = builder.new_input(c.inner, Pipeline);
220                builder.build(move |_capabilities| {
221                    let mut datums = DatumVec::new();
222                    move |_frontiers| {
223                        let mut ok_output = ok_output.activate();
224                        let mut err_output = err_output.activate();
225                        input.for_each(|time, data| {
226                            // Retain the input capability to derive a `Capability` for each output;
227                            // the `Session` type alias is fixed to `Capability<T>`.
228                            let ok_cap = time.retain(0);
229                            let err_cap = time.retain(1);
230                            let mut ok_session = ok_output.session_with_builder(&ok_cap);
231                            let mut err_session = err_output.session_with_builder(&err_cap);
232                            // Rows are read from the borrowed column, never
233                            // materialized as owned `Row`s.
234                            for (v, t, d) in data.borrow().into_index_iter() {
235                                logic(
236                                    &mut datums.borrow_with_limit(v, max_demand),
237                                    Columnar::into_owned(t),
238                                    Columnar::into_owned(d),
239                                    &mut ok_session,
240                                    &mut err_session,
241                                );
242                            }
243                        });
244                    }
245                });
246                (ok_stream, err_stream)
247            }
248        }
249    }
250
251    /// Consolidates updates in the edge, preserving variant.
252    pub fn consolidate_named(self, name: &str) -> Self {
253        match self {
254            CollectionEdge::Vec(c) => CollectionEdge::Vec(CollectionExt::consolidate_named::<
255                KeyBatcher<_, _, _>,
256            >(c, name)),
257            CollectionEdge::Columnar(c) => {
258                // TODO: Consolidate natively over columns. The pieces exist
259                // (`columnar_exchange`, the columnar merge batchers), which
260                // would avoid the row round-trip below.
261                let c = columnar_to_vec(c);
262                let c = CollectionExt::consolidate_named::<KeyBatcher<_, _, _>>(c, name);
263                CollectionEdge::Columnar(vec_to_columnar(c))
264            }
265        }
266    }
267}
268
269/// Negates the diff of every record in a [`ColumnarCollection`].
270///
271/// Rows and times are pushed from their borrowed forms. Only the diff is
272/// materialized, and it is `Copy`.
273///
274/// TODO: Rebuild only the diff column. Borrow the input column, build one owned
275/// negated diff column from the borrowed diffs, and re-encode using the borrowed
276/// row and time columns directly, so row and time bytes are copied once rather
277/// than pushed per record. The serialized (`Align` / `Bytes`) input case needs
278/// care, since all columns share a single buffer.
279pub fn columnar_negate<'scope, T>(
280    collection: ColumnarCollection<'scope, T, Row, Diff>,
281) -> ColumnarCollection<'scope, T, Row, Diff>
282where
283    T: RenderTimestamp,
284{
285    collection
286        .inner
287        .unary::<ColumnBuilder<(Row, T, Diff)>, _, _, _>(
288            Pipeline,
289            "ColumnarNegate",
290            |_cap, _info| {
291                move |input, output| {
292                    input.for_each(|time, data| {
293                        let mut session = output.session_with_builder(&time);
294                        for (v, t, d) in data.borrow().into_index_iter() {
295                            let d = -Diff::into_owned(d);
296                            session.give((v, t, &d));
297                        }
298                    });
299                }
300            },
301        )
302        .as_collection()
303}
304
305/// Repacks a row-based collection into columnar batches.
306///
307/// A transitional seam-healer, visible in rendered dataflows as a
308/// `VecToColumnar` operator. Repacking copies row bytes but allocates no
309/// per-record `Row`s.
310pub fn vec_to_columnar<'scope, T>(
311    collection: VecCollection<'scope, T, Row, Diff>,
312) -> ColumnarCollection<'scope, T, Row, Diff>
313where
314    T: RenderTimestamp,
315{
316    collection
317        .inner
318        .unary::<ColumnBuilder<(Row, T, Diff)>, _, _, _>(
319            Pipeline,
320            "VecToColumnar",
321            |_cap, _info| {
322                move |input, output| {
323                    input.for_each(|time, data| {
324                        let mut session = output.session_with_builder(&time);
325                        for (v, t, d) in data.drain(..) {
326                            session.give((&v, &t, &d));
327                        }
328                    });
329                }
330            },
331        )
332        .as_collection()
333}
334
335/// Decodes columnar batches into a row-based collection.
336///
337/// A transitional seam-healer, visible in rendered dataflows as a
338/// `ColumnarToVec` operator. Decoding allocates an owned [`Row`] per record,
339/// so it should only guard consumers that have not yet learned the columnar
340/// form.
341pub fn columnar_to_vec<'scope, T>(
342    collection: ColumnarCollection<'scope, T, Row, Diff>,
343) -> VecCollection<'scope, T, Row, Diff>
344where
345    T: RenderTimestamp,
346{
347    collection
348        .inner
349        .unary::<CapacityContainerBuilder<Vec<(Row, T, Diff)>>, _, _, _>(
350            Pipeline,
351            "ColumnarToVec",
352            |_cap, _info| {
353                move |input, output| {
354                    input.for_each(|time, data| {
355                        let mut session = output.session(&time);
356                        for (v, t, d) in data.borrow().into_index_iter() {
357                            session.give((
358                                Columnar::into_owned(v),
359                                Columnar::into_owned(t),
360                                Columnar::into_owned(d),
361                            ));
362                        }
363                    });
364                }
365            },
366        )
367        .as_collection()
368}
369
370#[cfg(test)]
371mod tests {
372    use differential_dataflow::input::Input;
373    use mz_ore::cast::CastFrom;
374    use mz_repr::{Datum, Timestamp};
375    use timely::dataflow::operators::Capture;
376    use timely::dataflow::operators::capture::{Event, Extract};
377
378    use super::*;
379
380    type RowBuilder = CapacityContainerBuilder<Vec<(Row, Timestamp, Diff)>>;
381    type CapturedRows = std::sync::mpsc::Receiver<Event<Timestamp, Vec<(Row, Timestamp, Diff)>>>;
382
383    fn extract_sorted(captured: CapturedRows) -> Vec<(Row, Timestamp, Diff)> {
384        let mut updates: Vec<_> = captured
385            .extract()
386            .into_iter()
387            .flat_map(|(_, data)| data)
388            .collect();
389        updates.sort();
390        updates
391    }
392
393    fn test_rows() -> Vec<Row> {
394        vec![
395            Row::pack_slice(&[Datum::Int32(42), Datum::String("hello")]),
396            Row::pack_slice(&[Datum::Int64(100), Datum::Null]),
397            Row::pack_slice(&[Datum::True, Datum::False, Datum::Null]),
398            Row::default(),
399        ]
400    }
401
402    #[mz_ore::test]
403    fn round_trip_through_columnar() {
404        let rows = test_rows();
405        let expected: Vec<_> = {
406            let mut updates: Vec<_> = rows
407                .iter()
408                .enumerate()
409                .map(|(i, r)| (r.clone(), Timestamp::from(u64::cast_from(i / 2)), Diff::ONE))
410                .collect();
411            updates.sort();
412            updates
413        };
414        let captured = timely::execute_directly(move |worker| {
415            worker.dataflow::<Timestamp, _, _>(|scope| {
416                let (mut input, collection) = scope.new_collection();
417                let captured = columnar_to_vec(vec_to_columnar(collection)).inner.capture();
418                for (i, row) in rows.into_iter().enumerate() {
419                    input.advance_to(Timestamp::from(u64::cast_from(i / 2)));
420                    input.update(row, Diff::ONE);
421                }
422                input.advance_to(Timestamp::from(2_u64));
423                input.flush();
424                captured
425            })
426        });
427        assert_eq!(extract_sorted(captured), expected);
428    }
429
430    #[mz_ore::test]
431    fn negate_flips_diffs_on_columnar_arm() {
432        let rows = test_rows();
433        let expected: Vec<_> = {
434            let mut updates: Vec<_> = rows
435                .iter()
436                .map(|r| (r.clone(), Timestamp::from(0_u64), -Diff::ONE))
437                .collect();
438            updates.sort();
439            updates
440        };
441        let captured = timely::execute_directly(move |worker| {
442            worker.dataflow::<Timestamp, _, _>(|scope| {
443                let (mut input, collection) = scope.new_collection();
444                let edge = CollectionEdge::Columnar(vec_to_columnar(collection)).negate();
445                assert!(matches!(edge, CollectionEdge::Columnar(_)));
446                let captured = edge.into_vec().inner.capture();
447                for row in rows {
448                    input.update(row, Diff::ONE);
449                }
450                input.advance_to(Timestamp::from(1_u64));
451                input.flush();
452                captured
453            })
454        });
455        assert_eq!(extract_sorted(captured), expected);
456    }
457
458    #[mz_ore::test]
459    fn concat_many_mixed_upgrades_to_columnar() {
460        let rows = test_rows();
461        let expected: Vec<_> = {
462            let mut updates: Vec<_> = rows
463                .iter()
464                .map(|r| (r.clone(), Timestamp::from(0_u64), Diff::ONE))
465                .collect();
466            // The first row arrives on both inputs.
467            updates.push((rows[0].clone(), Timestamp::from(0_u64), Diff::ONE));
468            updates.sort();
469            updates
470        };
471        let captured = timely::execute_directly(move |worker| {
472            worker.dataflow::<Timestamp, _, _>(|scope| {
473                let (mut input1, collection1) = scope.new_collection();
474                let (mut input2, collection2) = scope.new_collection();
475                let edge = CollectionEdge::concat_many(
476                    scope,
477                    [
478                        CollectionEdge::Vec(collection1),
479                        CollectionEdge::Columnar(vec_to_columnar(collection2)),
480                    ],
481                );
482                assert!(matches!(edge, CollectionEdge::Columnar(_)));
483                let captured = edge.into_vec().inner.capture();
484                let (first, rest) = rows.split_first().unwrap();
485                input1.update(first.clone(), Diff::ONE);
486                input2.update(first.clone(), Diff::ONE);
487                for row in rest {
488                    input1.update(row.clone(), Diff::ONE);
489                }
490                for input in [&mut input1, &mut input2] {
491                    input.advance_to(Timestamp::from(1_u64));
492                    input.flush();
493                }
494                captured
495            })
496        });
497        assert_eq!(extract_sorted(captured), expected);
498    }
499
500    #[mz_ore::test]
501    fn flat_map_datums_arms_agree() {
502        // Project the first datum of each row, exercising `max_demand` on both
503        // arms. The two captures must extract identical updates.
504        let rows = test_rows();
505        let (vec_captured, col_captured) = timely::execute_directly(move |worker| {
506            worker.dataflow::<Timestamp, _, _>(|scope| {
507                let (mut input, collection) = scope.new_collection();
508                let mut captures = Vec::new();
509                for edge in [
510                    CollectionEdge::Vec(collection.clone()),
511                    CollectionEdge::Columnar(vec_to_columnar(collection)),
512                ] {
513                    let (oks, _errs) = edge.flat_map_datums::<RowBuilder, _>(
514                        1,
515                        |datums, t, d, ok_session, _err_session| {
516                            ok_session.give((Row::pack(datums.iter()), t, d));
517                            1
518                        },
519                    );
520                    captures.push(oks.capture());
521                }
522                let col = captures.pop().unwrap();
523                let vec = captures.pop().unwrap();
524                for row in rows {
525                    input.update(row, Diff::ONE);
526                }
527                input.advance_to(Timestamp::from(1_u64));
528                input.flush();
529                (vec, col)
530            })
531        });
532        let vec_updates = extract_sorted(vec_captured);
533        assert_eq!(vec_updates, extract_sorted(col_captured));
534        // Each output row retains at most the first datum of its input.
535        assert!(vec_updates.iter().all(|(r, _, _)| r.iter().count() <= 1));
536    }
537
538    #[mz_ore::test]
539    fn consolidate_named_preserves_columnar() {
540        let row1 = Row::pack_slice(&[Datum::Int32(1)]);
541        let row2 = Row::pack_slice(&[Datum::Int32(2)]);
542        let expected = vec![(row1.clone(), Timestamp::from(0_u64), Diff::from(2))];
543        let captured = timely::execute_directly(move |worker| {
544            worker.dataflow::<Timestamp, _, _>(|scope| {
545                let (mut input, collection) = scope.new_collection();
546                let edge =
547                    CollectionEdge::Columnar(vec_to_columnar(collection)).consolidate_named("Test");
548                assert!(matches!(edge, CollectionEdge::Columnar(_)));
549                let captured = edge.into_vec().inner.capture();
550                // `row1` accumulates to a diff of two, `row2` cancels.
551                input.update(row1.clone(), Diff::ONE);
552                input.update(row1, Diff::ONE);
553                input.update(row2.clone(), Diff::ONE);
554                input.update(row2, -Diff::ONE);
555                input.advance_to(Timestamp::from(1_u64));
556                input.flush();
557                captured
558            })
559        });
560        assert_eq!(extract_sorted(captured), expected);
561    }
562}