Skip to main content

mz_compute/render/
flat_map.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
10use std::collections::VecDeque;
11
12use columnar::{Columnar, Index};
13use differential_dataflow::consolidation::ConsolidatingContainerBuilder;
14use mz_compute_types::dyncfgs::COMPUTE_FLAT_MAP_FUEL;
15use mz_compute_types::plan::scalar::LirScalarExpr;
16use mz_expr::TableFunc;
17use mz_expr::{Eval, MfpPlan};
18use mz_repr::{DatumVec, RowArena, SharedRow};
19use mz_repr::{Diff, Row, RowRef, Timestamp};
20use mz_timely_util::columnar::Column;
21use mz_timely_util::columnar::consolidate::ConsolidatingColumnBuilder;
22use mz_timely_util::operator::StreamExt;
23use timely::dataflow::channels::pact::Pipeline;
24use timely::dataflow::operators::CapabilitySet;
25use timely::dataflow::operators::generic::Session;
26use timely::dataflow::{Scope, Stream};
27use timely::progress::Antichain;
28
29use crate::render::RenderTimestamp;
30use crate::render::context::{CollectionBundle, Context};
31use crate::render::errors::DataflowErrorSer;
32
33impl<'scope, T: crate::render::RenderTimestamp> Context<'scope, T> {
34    /// Applies a `TableFunc` to every row, followed by an `mfp`.
35    pub fn render_flat_map(
36        &self,
37        input_key: Option<Vec<LirScalarExpr>>,
38        input: CollectionBundle<'scope, T>,
39        exprs: Vec<LirScalarExpr>,
40        func: TableFunc,
41        mfp_plan: MfpPlan<LirScalarExpr>,
42    ) -> CollectionBundle<'scope, T> {
43        let until = self.until.clone();
44        let scope = input.scope();
45
46        // Budget to limit the number of rows processed in a single invocation.
47        //
48        // The current implementation can only yield between input batches, but not from within
49        // a batch. A `generate_series` can still cause unavailability if it generates many rows.
50        let budget = COMPUTE_FLAT_MAP_FUEL.get(&self.config_set);
51
52        // The unarranged path reads the edge directly, so a columnar input is never
53        // decoded here. The keyed path materializes an existing arrangement, which
54        // `as_specific_collection` presents as a columnar edge.
55        let (edge, err_collection) = match input_key.as_deref() {
56            None => input
57                .collection
58                .clone()
59                .expect("The unarranged collection doesn't exist."),
60            Some(key) => input.as_specific_collection(Some(key)),
61        };
62
63        let (oks, errs) = flat_map_stage(edge.inner, scope, exprs, func, mfp_plan, until, budget);
64
65        use differential_dataflow::AsCollection;
66        let ok_collection = oks.as_collection();
67        let new_err_collection = errs.as_collection();
68        let err_collection = err_collection.concat(new_err_collection);
69        CollectionBundle::from_edge(ok_collection, err_collection)
70    }
71}
72
73/// Output ok-session container builder for [`flat_map_stage`].
74///
75/// Consolidating like the err builder, but emits `Column<(Row, T, Diff)>`. The mfp builds
76/// each output row fresh, so the owned give into staging is a move.
77type FlatMapOk<T> = ConsolidatingColumnBuilder<Row, T, Diff>;
78/// Output err-session container builder for [`flat_map_stage`].
79type FlatMapErr<T> = ConsolidatingContainerBuilder<Vec<(DataflowErrorSer, T, Diff)>>;
80
81/// The fueled FlatMap operator.
82///
83/// Each activation expands queued batches until the `budget` runs out, then re-activates
84/// and defers the rest of the queue, which bounds what one `generate_series` does before
85/// the worker yields.
86fn flat_map_stage<'scope, T>(
87    stream: Stream<'scope, T, Column<(Row, T, Diff)>>,
88    scope: Scope<'scope, T>,
89    exprs: Vec<LirScalarExpr>,
90    func: TableFunc,
91    mfp_plan: MfpPlan<LirScalarExpr>,
92    until: Antichain<Timestamp>,
93    budget: usize,
94) -> (
95    Stream<'scope, T, Column<(Row, T, Diff)>>,
96    Stream<'scope, T, Vec<(DataflowErrorSer, T, Diff)>>,
97)
98where
99    T: RenderTimestamp,
100{
101    stream.unary_fallible::<FlatMapOk<T>, FlatMapErr<T>, _, _>(
102        Pipeline,
103        "FlatMapStage",
104        move |_, info| {
105            let activator = scope.activator_for(info.address);
106            let mut queue = VecDeque::new();
107            Box::new(move |input, ok_output, err_output| {
108                let mut datums = DatumVec::new();
109                let mut datums_mfp = DatumVec::new();
110
111                // Buffer for extensions to `input_row`.
112                let mut table_func_output = Vec::new();
113
114                // Reused so a record's time and diff do not allocate per record.
115                let mut time = T::minimum();
116                let mut diff = Diff::ZERO;
117
118                let mut budget = budget;
119
120                input.for_each(|cap, data| {
121                    // A message's stamp need not be a singleton, so hold the whole set.
122                    queue.push_back((
123                        cap.retain_stamp(0),
124                        cap.retain_stamp(1),
125                        std::mem::take(data),
126                    ))
127                });
128
129                while let Some((ok_cap, err_cap, data)) = queue.pop_front() {
130                    let mut ok_session = ok_output.session_with_builder(&ok_cap);
131                    let mut err_session = err_output.session_with_builder(&err_cap);
132
133                    // Rows stay borrowed. The time and diff have to be owned to pass
134                    // them by reference, so they are copied into buffers rather than
135                    // built fresh: an iterative `T` owns a `PointStamp`'s allocation.
136                    for (input_row, t, d) in data.borrow().into_index_iter() {
137                        time.copy_from(t);
138                        diff.copy_from(d);
139                        process_flat_map_row(
140                            input_row,
141                            &time,
142                            &diff,
143                            &exprs,
144                            &func,
145                            &mfp_plan,
146                            &until,
147                            &mut datums,
148                            &mut datums_mfp,
149                            &mut table_func_output,
150                            &mut ok_session,
151                            &mut err_session,
152                            &mut budget,
153                        );
154                    }
155                    if budget == 0 {
156                        activator.activate();
157                        break;
158                    }
159                }
160            })
161        },
162    )
163}
164
165/// Expands one input record's table function and drains it through the mfp.
166///
167/// The expansion is chunked so [`drain_through_mfp`] amortizes the input-row decode.
168/// Argument or function evaluation errors emit to the err session and return early.
169fn process_flat_map_row<T>(
170    input_row: &RowRef,
171    time: &T,
172    diff: &Diff,
173    exprs: &[LirScalarExpr],
174    func: &TableFunc,
175    mfp_plan: &MfpPlan<LirScalarExpr>,
176    until: &Antichain<Timestamp>,
177    datums: &mut DatumVec,
178    datums_mfp: &mut DatumVec,
179    table_func_output: &mut Vec<(Row, Diff)>,
180    ok_session: &mut Session<'_, '_, T, FlatMapOk<T>, CapabilitySet<T>>,
181    err_session: &mut Session<'_, '_, T, FlatMapErr<T>, CapabilitySet<T>>,
182    budget: &mut usize,
183) where
184    T: RenderTimestamp,
185{
186    let temp_storage = RowArena::new();
187
188    // Unpack datums for expression evaluation.
189    let datums_local = datums.borrow_with(input_row);
190    let args = exprs
191        .iter()
192        .map(|e| e.eval(&datums_local, &temp_storage))
193        .collect::<Result<Vec<_>, _>>();
194    let args = match args {
195        Ok(args) => args,
196        Err(e) => {
197            err_session.give((e.into(), time.clone(), *diff));
198            return;
199        }
200    };
201    let mut extensions = match func.eval(&args, &temp_storage) {
202        Ok(exts) => exts.fuse(),
203        Err(e) => {
204            err_session.give((e.into(), time.clone(), *diff));
205            return;
206        }
207    };
208
209    // Draw additional columns out of the table func evaluation.
210    while let Some((extension, output_diff)) = extensions.next() {
211        table_func_output.push((extension, output_diff));
212        table_func_output.extend((&mut extensions).take(1023));
213        // We could consolidate `table_func_output`, but it seems unlikely to be productive.
214        drain_through_mfp(
215            input_row,
216            time,
217            diff,
218            datums_mfp,
219            table_func_output,
220            mfp_plan,
221            until,
222            ok_session,
223            err_session,
224            budget,
225        );
226        table_func_output.clear();
227    }
228}
229
230/// Drains a list of extensions to `input_row` through a supplied `MfpPlan` and into output buffers.
231///
232/// The method decodes `input_row`, and should be amortized across non-trivial `extensions`.
233fn drain_through_mfp<T>(
234    input_row: &RowRef,
235    input_time: &T,
236    input_diff: &Diff,
237    datum_vec: &mut DatumVec,
238    extensions: &[(Row, Diff)],
239    mfp_plan: &MfpPlan<LirScalarExpr>,
240    until: &Antichain<Timestamp>,
241    ok_output: &mut Session<'_, '_, T, FlatMapOk<T>, CapabilitySet<T>>,
242    err_output: &mut Session<'_, '_, T, FlatMapErr<T>, CapabilitySet<T>>,
243    budget: &mut usize,
244) where
245    T: RenderTimestamp,
246{
247    let temp_storage = RowArena::new();
248    let mut row_builder = SharedRow::get();
249
250    // This is not cheap, and is meant to be amortized across many `extensions`.
251    let mut datums_local = datum_vec.borrow_with(input_row);
252    let datums_len = datums_local.len();
253
254    let event_time = input_time.event_time().clone();
255
256    for (cols, diff) in extensions.iter() {
257        // Arrange `datums_local` to reflect the intended output pre-mfp.
258        datums_local.truncate(datums_len);
259        datums_local.extend(cols.iter());
260
261        let results = mfp_plan.evaluate(
262            &mut datums_local,
263            &temp_storage,
264            event_time,
265            *diff * *input_diff,
266            |time| !until.less_equal(time),
267            &mut row_builder,
268        );
269
270        for result in results {
271            *budget = budget.saturating_sub(1);
272            match result {
273                Ok((row, event_time, diff)) => {
274                    // Copy the whole time, and re-populate event time.
275                    let mut time = input_time.clone();
276                    *time.event_time_mut() = event_time;
277                    ok_output.give((row, time, diff));
278                }
279                Err((err, event_time, diff)) => {
280                    // Copy the whole time, and re-populate event time.
281                    let mut time = input_time.clone();
282                    *time.event_time_mut() = event_time;
283                    err_output.give((err, time, diff));
284                }
285            };
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use std::cell::RefCell;
293    use std::rc::Rc;
294
295    use differential_dataflow::input::Input;
296    use mz_expr::MapFilterProject;
297    use mz_repr::{Datum, ReprScalarType};
298    use timely::dataflow::operators::Inspect;
299    use timely::dataflow::operators::capture::{Capture, Extract};
300
301    use super::*;
302    use crate::render::columnar::vec_to_columnar;
303
304    // `generate_series(1, stop, 1)`, reading `stop` from column 1, with an identity mfp.
305    fn flat_map_args() -> (Vec<LirScalarExpr>, TableFunc, MfpPlan<LirScalarExpr>) {
306        let exprs = vec![
307            LirScalarExpr::column(0),
308            LirScalarExpr::column(1),
309            LirScalarExpr::literal_ok(Datum::Int64(1), ReprScalarType::Int64),
310        ];
311        let func = TableFunc::GenerateSeriesInt64;
312        let mfp = MapFilterProject::<LirScalarExpr>::new(3)
313            .into_plan()
314            .expect("identity mfp");
315        (exprs, func, mfp)
316    }
317
318    fn input_row(stop: i64) -> Row {
319        Row::pack_slice(&[Datum::Int64(1), Datum::Int64(stop)])
320    }
321
322    /// Runs one input row at each of `batches` distinct timestamps through `flat_map_stage`,
323    /// stepping the worker manually. Returns the steps taken and the output-record count.
324    fn run_fueled(batches: u64, stop: i64, budget: usize) -> (usize, usize) {
325        let expected = usize::try_from(batches).unwrap() * usize::try_from(stop).unwrap();
326        timely::execute_directly(move |worker| {
327            let collected = Rc::new(RefCell::new(0usize));
328            let sink = Rc::clone(&collected);
329            let mut input = worker.dataflow::<Timestamp, _, _>(|scope| {
330                let (input, collection) = scope.new_collection();
331                let (exprs, func, mfp) = flat_map_args();
332                // Feed the operator the columnar edge it is given in production,
333                // so the fuel assertions below cover the shipped path.
334                let stream = vec_to_columnar(collection).inner;
335                let scope = stream.scope();
336                let (oks, _errs) =
337                    flat_map_stage(stream, scope, exprs, func, mfp, Antichain::new(), budget);
338                // Counted per container: a per-record `inspect` needs
339                // `&Container: IntoIterator`, which on macOS recurses through `objc2`'s
340                // blanket impls until the trait solver overflows. `inspect_core` carries
341                // no such bound.
342                oks.inspect_core(move |event| {
343                    if let Ok((_time, data)) = event {
344                        *sink.borrow_mut() += data.borrow().into_index_iter().count();
345                    }
346                });
347                input
348            });
349
350            // One batch per timestamp, so the per-batch output does not consolidate away.
351            for i in 0..batches {
352                input.advance_to(Timestamp::from(i));
353                input.update(input_row(stop), Diff::ONE);
354                input.flush();
355            }
356            input.advance_to(Timestamp::from(batches));
357            input.flush();
358
359            let mut steps = 0;
360            while *collected.borrow() < expected {
361                worker.step();
362                steps += 1;
363                assert!(steps < 10_000, "flat map did not converge");
364            }
365            (steps, *collected.borrow())
366        })
367    }
368
369    #[mz_ore::test]
370    fn flat_map_fuel_bounds_per_activation() {
371        let (fueled_steps, fueled_count) = run_fueled(4, 3, 1);
372        let (unfueled_steps, unfueled_count) = run_fueled(4, 3, usize::MAX);
373        assert_eq!(fueled_count, 12);
374        assert_eq!(unfueled_count, 12);
375        assert_eq!(
376            unfueled_steps, 1,
377            "unbounded budget drains in one activation"
378        );
379        assert!(
380            fueled_steps > unfueled_steps,
381            "fuel budget must spread work across activations: fueled={fueled_steps} unfueled={unfueled_steps}"
382        );
383    }
384
385    #[mz_ore::test]
386    fn flat_map_reads_columnar_input() {
387        // Several timestamps and a retraction, so time handling and a negative diff are
388        // both decoded.
389        let captured = timely::execute_directly(move |worker| {
390            worker.dataflow::<Timestamp, _, _>(|scope| {
391                let (mut input, collection) = scope.new_collection();
392                let (exprs, func, mfp) = flat_map_args();
393                let stream = vec_to_columnar(collection).inner;
394                let scope = stream.scope();
395                let (oks, _errs) = flat_map_stage(
396                    stream,
397                    scope,
398                    exprs,
399                    func,
400                    mfp,
401                    Antichain::new(),
402                    usize::MAX,
403                );
404                let captured = oks.capture();
405                // t=0: generate_series(1, 2); t=1: generate_series(1, 3);
406                // t=2: retract the t=0 row.
407                input.advance_to(Timestamp::from(0_u64));
408                input.update(input_row(2), Diff::ONE);
409                input.advance_to(Timestamp::from(1_u64));
410                input.update(input_row(3), Diff::ONE);
411                input.advance_to(Timestamp::from(2_u64));
412                input.update(input_row(2), -Diff::ONE);
413                input.advance_to(Timestamp::from(3_u64));
414                input.flush();
415                captured
416            })
417        });
418
419        let updates = extract_sorted_columns(captured);
420        assert!(!updates.is_empty());
421        assert!(
422            updates.iter().any(|(_, _, d)| *d < Diff::ZERO),
423            "the retraction must survive as a negative diff"
424        );
425    }
426
427    /// Decodes a capture of the columnar output into sorted `(row, time, diff)` updates.
428    fn extract_sorted_columns(
429        captured: std::sync::mpsc::Receiver<
430            timely::dataflow::operators::capture::Event<Timestamp, Column<(Row, Timestamp, Diff)>>,
431        >,
432    ) -> Vec<(Row, Timestamp, Diff)> {
433        let mut updates: Vec<(Row, Timestamp, Diff)> = captured
434            .extract()
435            .into_iter()
436            .flat_map(|(_, col)| {
437                col.borrow()
438                    .into_index_iter()
439                    .map(|(v, t, d)| {
440                        (
441                            Columnar::into_owned(v),
442                            Columnar::into_owned(t),
443                            Columnar::into_owned(d),
444                        )
445                    })
446                    .collect::<Vec<_>>()
447            })
448            .collect();
449        updates.sort();
450        updates
451    }
452
453    #[mz_ore::test]
454    fn flat_map_output_consolidates_within_batch() {
455        // Two input rows whose expansions overlap once the mfp projects away the
456        // differing `stop` column, so the output builder has duplicates to fold.
457        let captured = timely::execute_directly(move |worker| {
458            worker.dataflow::<Timestamp, _, _>(|scope| {
459                let (mut input, collection) = scope.new_collection();
460                let exprs = vec![
461                    LirScalarExpr::column(0),
462                    LirScalarExpr::column(1),
463                    LirScalarExpr::literal_ok(Datum::Int64(1), ReprScalarType::Int64),
464                ];
465                let func = TableFunc::GenerateSeriesInt64;
466                // Project to the generated value alone, collapsing the distinct prefixes.
467                let mfp = MapFilterProject::<LirScalarExpr>::new(3)
468                    .project(vec![2])
469                    .into_plan()
470                    .expect("project mfp");
471                let stream = vec_to_columnar(collection).inner;
472                let scope = stream.scope();
473                let (oks, _errs) = flat_map_stage(
474                    stream,
475                    scope,
476                    exprs,
477                    func,
478                    mfp,
479                    Antichain::new(),
480                    usize::MAX,
481                );
482                let captured = oks.capture();
483                // Both at t=0: {1, 2} and {1, 2, 3}, so 1 and 2 fold to a diff of two.
484                input.advance_to(Timestamp::from(0_u64));
485                input.update(input_row(2), Diff::ONE);
486                input.update(input_row(3), Diff::ONE);
487                input.advance_to(Timestamp::from(1_u64));
488                input.flush();
489                captured
490            })
491        });
492
493        let updates = extract_sorted_columns(captured);
494        let expected = vec![
495            (
496                Row::pack_slice(&[Datum::Int64(1)]),
497                Timestamp::from(0_u64),
498                Diff::from(2),
499            ),
500            (
501                Row::pack_slice(&[Datum::Int64(2)]),
502                Timestamp::from(0_u64),
503                Diff::from(2),
504            ),
505            (
506                Row::pack_slice(&[Datum::Int64(3)]),
507                Timestamp::from(0_u64),
508                Diff::ONE,
509            ),
510        ];
511        assert_eq!(updates, expected);
512    }
513}