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