Skip to main content

mz_compute/render/join/
linear_join.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//! Rendering of linear join plans.
11//!
12//! Consult [LinearJoinPlan] documentation for details.
13
14use std::time::{Duration, Instant};
15
16use columnar::{Columnar, Index};
17use differential_dataflow::lattice::Lattice;
18use differential_dataflow::operators::arrange::arrangement::Arranged;
19use differential_dataflow::trace::cursor::{BatchCursor, BatchKey, BatchVal};
20use differential_dataflow::trace::{Cursor, Navigable, TraceReader};
21use differential_dataflow::{AsCollection, Data, VecCollection};
22use mz_compute_types::dyncfgs::{ENABLE_MZ_JOIN_CORE, LINEAR_JOIN_YIELDING};
23use mz_compute_types::plan::join::JoinClosure;
24use mz_compute_types::plan::join::linear_join::{LinearJoinPlan, LinearStagePlan};
25use mz_compute_types::plan::scalar::LirScalarExpr;
26use mz_dyncfg::ConfigSet;
27use mz_expr::Eval;
28use mz_repr::fixed_length::ExtendDatums;
29use mz_repr::{DatumVec, DatumVecBorrow, Diff, Row, RowArena, SharedRow};
30use mz_timely_util::columnar::Column;
31use mz_timely_util::columnar::batcher;
32use mz_timely_util::columnar::builder::ColumnBuilder;
33use mz_timely_util::columnar::chunk::{
34    AccountedChunkBatcher, ChunkChunker, ComputeSpill, UnchunkBuilder,
35};
36use mz_timely_util::columnar::consolidate::ConsolidatingColumnBuilder;
37use mz_timely_util::columnar::{Col2ValBatcher, Col2ValColBatcher, columnar_exchange};
38use mz_timely_util::operator::StreamExt;
39use timely::ContainerBuilder;
40use timely::container::{CapacityContainerBuilder, PushInto};
41use timely::dataflow::channels::pact::{ExchangeCore, Pipeline};
42use timely::dataflow::operators::generic::Operator;
43use timely::dataflow::{Scope, Stream};
44
45use crate::extensions::arrange::{ArrangementBatcher, MzArrangeCore};
46use crate::render::RenderTimestamp;
47use crate::render::columnar::{ColCollection, flat_map_datums};
48use crate::render::context::{ArrangementFlavor, CollectionBundle, Context};
49use crate::render::errors::DataflowErrorSer;
50use crate::render::join::mz_join_core::mz_join_core;
51use crate::typedefs::{RowRowAgent, RowRowEnter};
52use mz_row_spine::{RowRowBuilder, RowRowColPagedBuilder, RowRowSpine};
53
54/// Available linear join implementations.
55///
56/// See the `mz_join_core` module docs for our rationale for providing two join implementations.
57#[derive(Clone, Copy)]
58enum LinearJoinImpl {
59    Materialize,
60    DifferentialDataflow,
61}
62
63/// Specification of how linear joins are to be executed.
64///
65/// Note that currently `yielding` only affects the `Materialize` join implementation, as the DD
66/// join doesn't allow configuring its yielding behavior. Merging [#390] would fix this.
67///
68/// [#390]: https://github.com/TimelyDataflow/differential-dataflow/pull/390
69#[derive(Clone, Copy)]
70pub struct LinearJoinSpec {
71    implementation: LinearJoinImpl,
72    yielding: YieldSpec,
73}
74
75impl Default for LinearJoinSpec {
76    fn default() -> Self {
77        Self {
78            implementation: LinearJoinImpl::Materialize,
79            yielding: Default::default(),
80        }
81    }
82}
83
84impl LinearJoinSpec {
85    /// Create a `LinearJoinSpec` based on the given config.
86    pub fn from_config(config: &ConfigSet) -> Self {
87        let implementation = if ENABLE_MZ_JOIN_CORE.get(config) {
88            LinearJoinImpl::Materialize
89        } else {
90            LinearJoinImpl::DifferentialDataflow
91        };
92
93        let yielding_raw = LINEAR_JOIN_YIELDING.get(config);
94        let yielding = YieldSpec::try_from_str(&yielding_raw).unwrap_or_else(|| {
95            tracing::error!("invalid LINEAR_JOIN_YIELDING config: {yielding_raw}");
96            YieldSpec::default()
97        });
98
99        Self {
100            implementation,
101            yielding,
102        }
103    }
104
105    /// Render a join operator according to this specification, assembling its
106    /// output through `CB`.
107    ///
108    /// The `DifferentialDataflow` implementation builds its own `Vec` output and
109    /// cannot be handed a container builder, so that arm re-encodes through `CB`.
110    /// The `Materialize` implementation writes `CB` directly.
111    fn render<'s, T, Tr1, Tr2, L, I, CB>(
112        &self,
113        arranged1: Arranged<'s, Tr1>,
114        arranged2: Arranged<'s, Tr2>,
115        result: L,
116    ) -> Stream<'s, T, CB::Container>
117    where
118        T: Lattice + timely::progress::Timestamp,
119        CB: ContainerBuilder + PushInto<(I::Item, T, Diff)> + 'static,
120        Tr1: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
121        Tr2: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
122        BatchCursor<Tr1>: Cursor<Time = T, Diff = Diff>,
123        for<'a> BatchCursor<Tr2>: Cursor<Key<'a> = BatchKey<'a, Tr1>, Time = T, Diff = Diff>,
124        L: FnMut(BatchKey<'_, Tr1>, BatchVal<'_, Tr1>, BatchVal<'_, Tr2>) -> I + 'static,
125        I: IntoIterator<Item: Data> + 'static,
126    {
127        use LinearJoinImpl::*;
128
129        match (
130            self.implementation,
131            self.yielding.after_work,
132            self.yielding.after_time,
133        ) {
134            (DifferentialDataflow, _, _) => {
135                encode_updates::<_, _, CB>(arranged1.join_core(arranged2, result), "JoinCoreEncode")
136            }
137            (Materialize, Some(work_limit), Some(time_limit)) => {
138                let yield_fn =
139                    move |start: Instant, work| work >= work_limit || start.elapsed() >= time_limit;
140                mz_join_core::<_, _, _, _, _, _, CB>(arranged1, arranged2, result, yield_fn)
141            }
142            (Materialize, Some(work_limit), None) => {
143                let yield_fn = move |_start, work| work >= work_limit;
144                mz_join_core::<_, _, _, _, _, _, CB>(arranged1, arranged2, result, yield_fn)
145            }
146            (Materialize, None, Some(time_limit)) => {
147                let yield_fn = move |start: Instant, _work| start.elapsed() >= time_limit;
148                mz_join_core::<_, _, _, _, _, _, CB>(arranged1, arranged2, result, yield_fn)
149            }
150            (Materialize, None, None) => {
151                let yield_fn = |_start, _work| false;
152                mz_join_core::<_, _, _, _, _, _, CB>(arranged1, arranged2, result, yield_fn)
153            }
154        }
155    }
156}
157
158/// Specification of a dataflow operator's yielding behavior.
159#[derive(Clone, Copy)]
160struct YieldSpec {
161    /// Yield after the given amount of work was performed.
162    after_work: Option<usize>,
163    /// Yield after the given amount of time has elapsed.
164    after_time: Option<Duration>,
165}
166
167impl Default for YieldSpec {
168    fn default() -> Self {
169        Self {
170            after_work: Some(1_000_000),
171            after_time: Some(Duration::from_millis(100)),
172        }
173    }
174}
175
176impl YieldSpec {
177    fn try_from_str(s: &str) -> Option<Self> {
178        let mut after_work = None;
179        let mut after_time = None;
180
181        let options = s.split(',').map(|o| o.trim());
182        for option in options {
183            let mut iter = option.split(':').map(|p| p.trim());
184            match std::array::from_fn(|_| iter.next()) {
185                [Some("work"), Some(amount), None] => {
186                    let amount = amount.parse().ok()?;
187                    after_work = Some(amount);
188                }
189                [Some("time"), Some(millis), None] => {
190                    let millis = millis.parse().ok()?;
191                    let duration = Duration::from_millis(millis);
192                    after_time = Some(duration);
193                }
194                _ => return None,
195            }
196        }
197
198        Some(Self {
199            after_work,
200            after_time,
201        })
202    }
203}
204
205/// Applies `closure` to the datums of one record, packing the row it computes.
206///
207/// The two finalization paths differ only in where the datums come from, so this is what
208/// they share.
209fn apply_closure<'a>(
210    closure: &'a JoinClosure,
211    datums: &mut DatumVecBorrow<'a>,
212    temp_storage: &'a RowArena,
213) -> Result<Option<Row>, DataflowErrorSer> {
214    let mut row_builder = SharedRow::get();
215    // `cloned` detaches the result from `temp_storage` and the shared row builder, both of
216    // which drop at the end of the caller's per-record work.
217    closure
218        .apply(datums, temp_storage, &mut row_builder)
219        .map(|row| row.cloned())
220        .map_err(DataflowErrorSer::from)
221}
222
223/// Applies `closure` to every record of `edge`, writing the rows it computes onto the
224/// output edge.
225///
226/// The closure borrows the row it reads, so reading the edge costs nothing, whereas
227/// decoding it would cost an owned [`Row`] per record.
228fn apply_closure_to_edge<'s, T>(
229    edge: ColCollection<'s, T>,
230    name: &str,
231    closure: JoinClosure,
232) -> (
233    ColCollection<'s, T>,
234    VecCollection<'s, T, DataflowErrorSer, Diff>,
235)
236where
237    T: RenderTimestamp,
238{
239    let (oks, errs) = flat_map_datums::<_, ConsolidatingColumnBuilder<Row, T, Diff>, _>(
240        edge,
241        name,
242        usize::MAX,
243        {
244            let mut datum_vec = DatumVec::new();
245            move |row_datums, time, diff, ok_session, err_session| {
246                // `JoinClosure::apply` unifies the lifetimes of `&self`, the datums, and
247                // the arena. Copying the datums into a local vec lets that lifetime shrink
248                // to this call. The copy moves datum references, not row data.
249                let temp_storage = RowArena::new();
250                let mut datums = datum_vec.borrow();
251                datums.extend(row_datums.iter());
252                match apply_closure(&closure, &mut datums, &temp_storage) {
253                    Ok(Some(row)) => {
254                        ok_session.give((row, time, diff));
255                        1
256                    }
257                    Ok(None) => 0,
258                    Err(e) => {
259                        err_session.give((e, time, diff));
260                        1
261                    }
262                }
263            }
264        },
265    );
266    (oks.as_collection(), errs.as_collection())
267}
268
269/// Different forms the streamed data might take.
270enum JoinedFlavor<'scope, T: RenderTimestamp> {
271    /// The join's source input, before it enters the first stage.
272    /// `differential_join` forms its arrangement key off the edge, so a columnar source
273    /// needs no decode.
274    Collection(ColCollection<'scope, T>),
275    /// A dataflow-local arrangement.
276    Local(Arranged<'scope, RowRowAgent<T, Diff>>),
277    /// An imported arrangement.
278    Trace(Arranged<'scope, RowRowEnter<mz_repr::Timestamp, Diff, T>>),
279}
280
281impl<'scope, T> Context<'scope, T>
282where
283    T: Lattice + RenderTimestamp,
284{
285    pub(crate) fn render_join(
286        &self,
287        inputs: Vec<CollectionBundle<'scope, T>>,
288        linear_plan: LinearJoinPlan,
289    ) -> CollectionBundle<'scope, T> {
290        self.scope.clone().region_named("Join(Linear)", |inner| {
291            self.render_join_inner(inputs, linear_plan, inner)
292        })
293    }
294
295    fn render_join_inner(
296        &self,
297        inputs: Vec<CollectionBundle<'scope, T>>,
298        linear_plan: LinearJoinPlan,
299        inner: Scope<'_, T>,
300    ) -> CollectionBundle<'scope, T> {
301        // Collect all error streams, and concatenate them at the end.
302        let mut errors = Vec::new();
303
304        // Determine which form our maintained spine of updates will initially take.
305        // First, just check out the availability of an appropriate arrangement.
306        // This will be `None` in the degenerate single-input join case, which ensures
307        // that we do not panic if we never go around the `stage_plans` loop.
308        let arrangement = linear_plan
309            .stage_plans
310            .get(0)
311            .and_then(|stage| inputs[linear_plan.source_relation].arrangement(&stage.stream_key));
312        // We can use an arrangement if it exists and an initial closure does not.
313        let mut joined = match (arrangement, linear_plan.initial_closure) {
314            (Some(ArrangementFlavor::Local(oks, errs)), None) => {
315                errors.push(errs.as_collection(|k, _v| k.clone()).enter_region(inner));
316                JoinedFlavor::Local(oks.enter_region(inner))
317            }
318            (Some(ArrangementFlavor::Trace(_gid, oks, errs)), None) => {
319                errors.push(errs.as_collection(|k, _v| k.clone()).enter_region(inner));
320                JoinedFlavor::Trace(oks.enter_region(inner))
321            }
322            (_, initial_closure) => {
323                // TODO: extract closure from the first stage in the join plan, should it exist.
324                // TODO: apply that closure in `flat_map_ref` rather than calling `.collection`.
325                let (joined, errs) = match linear_plan.source_key.as_deref() {
326                    None => inputs[linear_plan.source_relation]
327                        .collection
328                        .clone()
329                        .expect("The unarranged collection doesn't exist."),
330                    Some(key) => {
331                        inputs[linear_plan.source_relation].as_specific_collection(Some(key))
332                    }
333                };
334                errors.push(errs.enter_region(inner));
335                let joined = joined.enter_region(inner);
336
337                // In the current code this should always be `None`, but we have this here should
338                // we change that and want to know what we should be doing.
339                if let Some(closure) = initial_closure {
340                    // If there is no starting arrangement, then we can run filters
341                    // directly on the starting collection.
342                    // If there is only one input, we are done joining, so run filters.
343                    // Current lowering never takes this branch.
344                    let (j, errs) =
345                        apply_closure_to_edge(joined, "LinearJoinInitialization", closure);
346                    errors.push(errs);
347                    JoinedFlavor::Collection(j)
348                } else {
349                    JoinedFlavor::Collection(joined)
350                }
351            }
352        };
353
354        // progress through stages, updating partial results and errors.
355        //
356        // The last stage writes the node's output edge only when no finalization closure
357        // follows it. With a closure, the closure's builder writes the edge and the stage
358        // feeds it the accumulator, which the closure reads borrowed either way.
359        let stage_count = linear_plan.stage_plans.len();
360        let terminal_stage_writes_edge = linear_plan.final_closure.is_none();
361        for (index, stage_plan) in linear_plan.stage_plans.into_iter().enumerate() {
362            let terminal = index + 1 == stage_count && terminal_stage_writes_edge;
363            // Different variants of `joined` implement this differently,
364            // and the logic is centralized there.
365            joined = self.differential_join(
366                joined,
367                inputs[stage_plan.lookup_relation].enter_region(inner),
368                stage_plan,
369                terminal,
370                &mut errors,
371            );
372        }
373
374        // We have completed the join building, but may have work remaining.
375        // For example, we may have expressions not pushed down (e.g. literals)
376        // and projections that could not be applied (e.g. column repetition).
377        // The result is either the source edge (single-input join, no stages) or the
378        // accumulator (after one or more stages); it is never arranged.
379        let ok_edge = match (joined, linear_plan.final_closure) {
380            (JoinedFlavor::Collection(edge), None) => edge,
381            (JoinedFlavor::Collection(edge), Some(closure)) => {
382                let (updates, errs) =
383                    apply_closure_to_edge(edge, "LinearJoinFinalization", closure);
384                errors.push(errs);
385                updates
386            }
387            _ => panic!("Unexpectedly arranged join output"),
388        };
389
390        // Return joined results and all produced errors collected together.
391        let bundle = CollectionBundle::from_edge(
392            ok_edge,
393            differential_dataflow::collection::concatenate(inner, errors),
394        );
395        bundle.leave_region(self.scope)
396    }
397
398    /// Looks up the arrangement for the next input and joins it to the arranged
399    /// version of the join of previous inputs.
400    ///
401    /// `terminal` marks a stage whose output is the node's output, which makes
402    /// it write the output edge rather than the `Vec` accumulator.
403    fn differential_join<'s>(
404        &self,
405        mut joined: JoinedFlavor<'s, T>,
406        lookup_relation: CollectionBundle<'s, T>,
407        LinearStagePlan {
408            stream_key,
409            stream_thinning,
410            lookup_key,
411            closure,
412            lookup_relation: _,
413        }: LinearStagePlan,
414        terminal: bool,
415        errors: &mut Vec<VecCollection<'s, T, DataflowErrorSer, Diff>>,
416    ) -> JoinedFlavor<'s, T> {
417        // A streamed input must first form an arrangement.
418        match joined {
419            JoinedFlavor::Collection(edge) => {
420                let (arranged, errs) = arrange_join_input(
421                    edge,
422                    stream_key,
423                    stream_thinning,
424                    ArrangementBatcher::from_config(&self.config_set),
425                );
426                errors.push(errs);
427                joined = JoinedFlavor::Local(arranged);
428            }
429            JoinedFlavor::Local(_) | JoinedFlavor::Trace(_) => {}
430        }
431
432        // Demultiplex the four different cross products of arrangement types we might have.
433        let arrangement = lookup_relation
434            .arrangement(&lookup_key[..])
435            .expect("Arrangement absent despite explicit construction");
436
437        match joined {
438            JoinedFlavor::Collection(_) => {
439                unreachable!("streamed join input arranged at top of method");
440            }
441            JoinedFlavor::Local(local) => match arrangement {
442                ArrangementFlavor::Local(oks, errs1) => {
443                    let (oks, errs2) = self
444                        .differential_join_inner::<RowRowAgent<_, _>, RowRowAgent<_, _>>(
445                            local, oks, closure, terminal,
446                        );
447
448                    errors.push(errs1.as_collection(|k, _v| k.clone()));
449                    errors.extend(errs2);
450                    oks
451                }
452                ArrangementFlavor::Trace(_gid, oks, errs1) => {
453                    let (oks, errs2) = self
454                        .differential_join_inner::<RowRowAgent<_, _>, RowRowEnter<_, _, _>>(
455                            local, oks, closure, terminal,
456                        );
457
458                    errors.push(errs1.as_collection(|k, _v| k.clone()));
459                    errors.extend(errs2);
460                    oks
461                }
462            },
463            JoinedFlavor::Trace(trace) => match arrangement {
464                ArrangementFlavor::Local(oks, errs1) => {
465                    let (oks, errs2) = self
466                        .differential_join_inner::<RowRowEnter<_, _, _>, RowRowAgent<_, _>>(
467                            trace, oks, closure, terminal,
468                        );
469
470                    errors.push(errs1.as_collection(|k, _v| k.clone()));
471                    errors.extend(errs2);
472                    oks
473                }
474                ArrangementFlavor::Trace(_gid, oks, errs1) => {
475                    let (oks, errs2) = self
476                        .differential_join_inner::<RowRowEnter<_, _, _>, RowRowEnter<_, _, _>>(
477                            trace, oks, closure, terminal,
478                        );
479
480                    errors.push(errs1.as_collection(|k, _v| k.clone()));
481                    errors.extend(errs2);
482                    oks
483                }
484            },
485        }
486    }
487
488    /// Joins the arrangement for `next_input` to the arranged version of the
489    /// join of previous inputs. This is split into its own method to enable
490    /// reuse of code with different types of `next_input`.
491    ///
492    /// The return type includes an optional error collection, which may be
493    /// `None` if we can determine that `closure` cannot error.
494    /// Every arm writes the columnar collection, so `terminal` selects only whether the
495    /// builder consolidates: a terminal stage's output leaves the operator, while a
496    /// non-terminal stage's is consolidated by the next stage's batcher.
497    fn differential_join_inner<'s, Tr1, Tr2>(
498        &self,
499        prev_keyed: Arranged<'s, Tr1>,
500        next_input: Arranged<'s, Tr2>,
501        closure: JoinClosure,
502        terminal: bool,
503    ) -> (
504        JoinedFlavor<'s, T>,
505        Option<VecCollection<'s, T, DataflowErrorSer, Diff>>,
506    )
507    where
508        Tr1: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
509        Tr2: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
510        for<'a> BatchCursor<Tr1>:
511            Cursor<Key<'a>: ExtendDatums, Val<'a>: ExtendDatums, Time = T, Diff = Diff>,
512        for<'a> BatchCursor<Tr2>:
513            Cursor<Key<'a> = BatchKey<'a, Tr1>, Val<'a>: ExtendDatums, Time = T, Diff = Diff>,
514    {
515        // Reuseable allocation for unpacking.
516        let mut datums = DatumVec::new();
517
518        // The builder for the error-capable arm, whose `Result`s are not columnar.
519        type VecCB<D, T> = CapacityContainerBuilder<Vec<(D, T, Diff)>>;
520
521        if closure.could_error() {
522            let results = self
523                .linear_join_spec
524                .render::<T, _, _, _, _, VecCB<Result<Row, DataflowErrorSer>, T>>(
525                    prev_keyed,
526                    next_input,
527                    move |key, old, new| {
528                        apply_join_closure(&closure, &mut datums, key, old, new)
529                            .map_err(DataflowErrorSer::from)
530                            .transpose()
531                    },
532                );
533            let (oks, errs) = demux_join_results(results);
534            (JoinedFlavor::Collection(oks), Some(errs))
535        } else if terminal {
536            let oks = self
537                .linear_join_spec
538                .render::<T, _, _, _, _, ConsolidatingColumnBuilder<Row, T, Diff>>(
539                    prev_keyed,
540                    next_input,
541                    move |key, old, new| {
542                        apply_join_closure(&closure, &mut datums, key, old, new)
543                            .expect("Closure claimed to never error")
544                    },
545                );
546
547            (JoinedFlavor::Collection(oks.as_collection()), None)
548        } else {
549            // The next stage's arrangement batcher consolidates this, and `mz_join_core`
550            // has already consolidated each chunk it produces, so this builder does not.
551            let oks = self
552                .linear_join_spec
553                .render::<T, _, _, _, _, ColumnBuilder<(Row, T, Diff)>>(
554                    prev_keyed,
555                    next_input,
556                    move |key, old, new| {
557                        apply_join_closure(&closure, &mut datums, key, old, new)
558                            .expect("Closure claimed to never error")
559                    },
560                );
561
562            (JoinedFlavor::Collection(oks.as_collection()), None)
563        }
564    }
565}
566
567/// Unpacks one join match into datums and applies `closure` to it.
568///
569/// `None` means the closure filtered the match out. The output row is owned
570/// because the row `closure` writes borrows the shared row builder, which the
571/// caller must not hold past this call.
572fn apply_join_closure<K, V1, V2>(
573    closure: &JoinClosure,
574    datums: &mut DatumVec,
575    key: K,
576    old: V1,
577    new: V2,
578) -> Result<Option<Row>, mz_expr::EvalError>
579where
580    K: ExtendDatums,
581    V1: ExtendDatums,
582    V2: ExtendDatums,
583{
584    let mut row_builder = SharedRow::get();
585    let temp_storage = RowArena::new();
586
587    let mut datums_local = datums.borrow();
588    key.extend_datums(&temp_storage, &mut datums_local, None);
589    old.extend_datums(&temp_storage, &mut datums_local, None);
590    new.extend_datums(&temp_storage, &mut datums_local, None);
591
592    closure
593        .apply(&mut datums_local, &temp_storage, &mut row_builder)
594        .map(|row| row.cloned())
595}
596
597/// Splits a stage's `Result`s, writing the rows onto the edge and the errors to a `Vec`.
598///
599/// [`LinearJoinSpec::render`] has one output, so a stage whose closure can error produces
600/// `Result`s and separates them afterwards. The rows are pushed borrowed, so the split is
601/// also the encode.
602fn demux_join_results<'s, T>(
603    results: Stream<'s, T, Vec<(Result<Row, DataflowErrorSer>, T, Diff)>>,
604) -> (
605    ColCollection<'s, T>,
606    VecCollection<'s, T, DataflowErrorSer, Diff>,
607)
608where
609    T: RenderTimestamp,
610{
611    let (oks, errs) = results.unary_fallible::<ColumnBuilder<(Row, T, Diff)>, _, _, _>(
612        Pipeline,
613        "LinearJoinStageDemux",
614        |_, _| {
615            Box::new(move |input, ok, err| {
616                input.for_each(|time, data| {
617                    let mut ok_session = ok.session_with_builder(&time);
618                    let mut err_session = err.session(&time);
619                    for (result, time, diff) in data.drain(..) {
620                        match result {
621                            Ok(row) => ok_session.give((&row, &time, &diff)),
622                            Err(e) => err_session.give((e, time, diff)),
623                        }
624                    }
625                });
626            })
627        },
628    );
629    (oks.as_collection(), errs.as_collection())
630}
631
632/// Re-encodes a `Vec` collection through `CB`.
633///
634/// For a join implementation that builds its own `Vec` output and so cannot be
635/// handed a container builder.
636fn encode_updates<'s, T, D, CB>(
637    collection: VecCollection<'s, T, D, Diff>,
638    name: &str,
639) -> Stream<'s, T, CB::Container>
640where
641    T: timely::progress::Timestamp,
642    D: Data,
643    CB: ContainerBuilder + PushInto<(D, T, Diff)> + 'static,
644{
645    collection
646        .inner
647        .unary::<CB, _, _, _>(Pipeline, name, |_, _| {
648            move |input, output| {
649                input.for_each(|time, data| {
650                    output
651                        .session_with_builder(&time)
652                        .give_iterator(data.drain(..));
653                });
654            }
655        })
656}
657
658/// Exchanges keyed join updates by key and arranges them into a `RowRowSpine`.
659fn arrange_keyed_join_input<'s, T>(
660    keyed: Stream<'s, T, Column<((Row, Row), T, Diff)>>,
661    errs: Stream<'s, T, Vec<(DataflowErrorSer, T, Diff)>>,
662    batcher: ArrangementBatcher,
663) -> (
664    Arranged<'s, RowRowAgent<T, Diff>>,
665    VecCollection<'s, T, DataflowErrorSer, Diff>,
666)
667where
668    T: Lattice + RenderTimestamp,
669{
670    let exchange =
671        ExchangeCore::<ColumnBuilder<_>, _>::new_core(columnar_exchange::<Row, Row, T, Diff>);
672    let arranged = match batcher {
673        ArrangementBatcher::Chunked => keyed.mz_arrange_core::<
674            _,
675            ChunkChunker<(Row, Row), T, Diff, ComputeSpill>,
676            AccountedChunkBatcher<(Row, Row), T, Diff, ComputeSpill>,
677            UnchunkBuilder<RowRowColPagedBuilder<T, Diff>, (Row, Row), T, Diff, ComputeSpill>,
678            RowRowSpine<_, _>,
679        >(exchange, "JoinStage"),
680        ArrangementBatcher::Columnar => keyed.mz_arrange_core::<
681            _,
682            batcher::ColumnChunker<_>,
683            Col2ValColBatcher<_, _, _, _>,
684            RowRowColPagedBuilder<_, _>,
685            RowRowSpine<_, _>,
686        >(exchange, "JoinStage"),
687        ArrangementBatcher::Columnation => keyed.mz_arrange_core::<
688            _,
689            batcher::Chunker<_>,
690            Col2ValBatcher<_, _, _, _>,
691            RowRowBuilder<_, _>,
692            RowRowSpine<_, _>,
693        >(exchange, "JoinStage"),
694    };
695    (arranged, errs.as_collection())
696}
697
698/// Forms the source arrangement for a streamed join input off a collection edge.
699///
700/// Pushes the key and value borrowed into the `ColumnBuilder` the `Col2Val` batcher
701/// consumes, so the ok path holds no owned `Row` per record. Only the error path owns a
702/// time and diff.
703fn arrange_join_input<'s, T>(
704    edge: ColCollection<'s, T>,
705    stream_key: Vec<LirScalarExpr>,
706    stream_thinning: Vec<usize>,
707    batcher: ArrangementBatcher,
708) -> (
709    Arranged<'s, RowRowAgent<T, Diff>>,
710    VecCollection<'s, T, DataflowErrorSer, Diff>,
711)
712where
713    T: Lattice + RenderTimestamp,
714{
715    let (keyed, errs) = edge
716        .inner
717        .unary_fallible::<ColumnBuilder<((Row, Row), T, Diff)>, _, _, _>(
718            Pipeline,
719            "LinearJoinKeyPreparation",
720            |_, _| {
721                Box::new(move |input, ok, errs| {
722                    let mut temp_storage = RowArena::new();
723                    let mut key_buf = Row::default();
724                    let mut val_buf = Row::default();
725                    let mut datums = DatumVec::new();
726                    input.for_each(|time, data| {
727                        let mut ok_session = ok.session_with_builder(&time);
728                        let mut err_session = errs.session(&time);
729                        for (row, time, diff) in data.borrow().into_index_iter() {
730                            temp_storage.clear();
731                            let datums_local = datums.borrow_with(row);
732                            let datums = stream_key
733                                .iter()
734                                .map(|e| e.eval(&datums_local, &temp_storage));
735                            match key_buf.packer().try_extend(datums) {
736                                Ok(()) => {
737                                    val_buf
738                                        .packer()
739                                        .extend(stream_thinning.iter().map(|e| datums_local[*e]));
740                                    ok_session.give(((&key_buf, &val_buf), time, diff));
741                                }
742                                Err(e) => {
743                                    err_session.give((
744                                        e.into(),
745                                        Columnar::into_owned(time),
746                                        Columnar::into_owned(diff),
747                                    ));
748                                }
749                            }
750                        }
751                    });
752                })
753            },
754        );
755    arrange_keyed_join_input(keyed, errs, batcher)
756}
757
758#[cfg(test)]
759mod tests {
760    use differential_dataflow::input::Input;
761    use mz_expr::EvalError;
762    use mz_repr::{Datum, ReprScalarType, Timestamp};
763    use timely::dataflow::operators::Capture;
764    use timely::dataflow::operators::capture::{Event, Extract};
765
766    use super::*;
767    use crate::render::columnar::vec_to_columnar;
768
769    type KeyedUpdate = ((Row, Row), Timestamp, Diff);
770    type ErrUpdate = (DataflowErrorSer, Timestamp, Diff);
771    type Captured<D> = std::sync::mpsc::Receiver<Event<Timestamp, Vec<D>>>;
772
773    fn extract_sorted(captured: Captured<KeyedUpdate>) -> Vec<KeyedUpdate> {
774        let mut updates: Vec<_> = captured
775            .extract()
776            .into_iter()
777            .flat_map(|(_, data)| data)
778            .collect();
779        updates.sort();
780        updates
781    }
782
783    // `DataflowErrorSer` is not `Ord`, so order by the error's debug string.
784    fn extract_err(captured: Captured<ErrUpdate>) -> Vec<(String, Timestamp, Diff)> {
785        let mut updates: Vec<_> = captured
786            .extract()
787            .into_iter()
788            .flat_map(|(_, data)| data)
789            .map(|(e, t, d)| (format!("{e:?}"), t, d))
790            .collect();
791        updates.sort();
792        updates
793    }
794
795    /// Rows across several timestamps, including two `-1` diffs. Those retract at a
796    /// `(row, time)` with no matching insertion, so the `InputSession`'s pre-send
797    /// consolidation does not cancel them out.
798    fn test_input() -> Vec<(Row, u64, Diff)> {
799        vec![
800            (
801                Row::pack_slice(&[Datum::Int32(1), Datum::String("a")]),
802                0,
803                Diff::ONE,
804            ),
805            (
806                Row::pack_slice(&[Datum::Int32(2), Datum::String("b")]),
807                1,
808                Diff::ONE,
809            ),
810            (
811                Row::pack_slice(&[Datum::Int32(1), Datum::String("c")]),
812                2,
813                Diff::ONE,
814            ),
815            (
816                Row::pack_slice(&[Datum::Int32(3), Datum::Null]),
817                2,
818                Diff::ONE,
819            ),
820            (
821                Row::pack_slice(&[Datum::Int32(2), Datum::String("b")]),
822                2,
823                -Diff::ONE,
824            ),
825            (
826                Row::pack_slice(&[Datum::Int32(4), Datum::String("d")]),
827                1,
828                -Diff::ONE,
829            ),
830        ]
831    }
832
833    /// Runs `arrange_join_input`, keying by `key` with column 1 as the value, and returns
834    /// the sorted ok updates, read back from the arrangement, and err updates.
835    fn run_columnar(
836        input: Vec<(Row, u64, Diff)>,
837        key: Vec<LirScalarExpr>,
838    ) -> (Vec<KeyedUpdate>, Vec<(String, Timestamp, Diff)>) {
839        let (ok, err) = timely::execute_directly(move |worker| {
840            worker.dataflow::<Timestamp, _, _>(|scope| {
841                let (mut handle, collection) = scope.new_collection();
842                let (arranged, errs) = arrange_join_input(
843                    vec_to_columnar(collection),
844                    key,
845                    vec![1],
846                    ArrangementBatcher::Columnation,
847                );
848                let keyed = arranged.as_collection(|k, v| (k.to_row(), v.to_row()));
849                let ok = keyed.inner.capture();
850                let err = errs.inner.capture();
851                for (row, time, diff) in input {
852                    handle.update_at(row, Timestamp::from(time), diff);
853                }
854                handle.advance_to(Timestamp::from(3_u64));
855                handle.flush();
856                (ok, err)
857            })
858        });
859        (extract_sorted(ok), extract_err(err))
860    }
861
862    /// Agreeing contents do not rule out a silent decode on the ok path. That the path
863    /// never decodes holds by inspection, not by this test.
864    #[mz_ore::test]
865    fn arrange_join_input_keys_correctly() {
866        let (ok, err) = run_columnar(test_input(), vec![LirScalarExpr::column(0)]);
867        assert!(!ok.is_empty());
868        assert!(err.is_empty());
869        // A retraction survives into the arrangement, so the ok path handled a
870        // negative (borrowed) diff.
871        assert!(ok.iter().any(|(_, _, d)| *d < Diff::ZERO));
872        // Key is column 0, value is column 1 (the thinning), so both are single
873        // datums.
874        for ((key, value), _t, _d) in &ok {
875            assert_eq!(key.iter().count(), 1);
876            assert_eq!(value.iter().count(), 1);
877        }
878    }
879
880    /// A key expression that always errors drives every record onto the
881    /// `try_extend` Err branch, exercising `Columnar::into_owned` reconstruction
882    /// of each error's `(time, diff)`. The ok output must be empty.
883    #[mz_ore::test]
884    fn arrange_join_input_error_path() {
885        let key = vec![LirScalarExpr::literal(
886            Err(EvalError::DivisionByZero),
887            ReprScalarType::Int32,
888        )];
889        let (ok, err) = run_columnar(test_input(), key);
890        assert!(ok.is_empty());
891        assert!(!err.is_empty());
892    }
893}