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