Skip to main content

mz_compute/render/
context.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//! Management of dataflow-local state, like arrangements, while building a
11//! dataflow.
12
13use std::collections::BTreeMap;
14use std::rc::Rc;
15
16use differential_dataflow::consolidation::ConsolidatingContainerBuilder;
17use differential_dataflow::operators::arrange::Arranged;
18use differential_dataflow::trace::cursor::{BatchCursor, BatchKey, BatchVal};
19use differential_dataflow::trace::implementations::BatchContainer;
20use differential_dataflow::trace::{Cursor, Navigable, TraceReader};
21use differential_dataflow::{AsCollection, Data, VecCollection};
22use mz_compute_types::dataflows::DataflowDescription;
23use mz_compute_types::dyncfgs::{
24    ENABLE_COLUMN_PAGED_BATCHER, ENABLE_COMPUTE_RENDER_FUELED_AS_SPECIFIC_COLLECTION,
25    ENABLE_COMPUTE_TEMPORAL_BUCKETING, TEMPORAL_BUCKETING_SUMMARY,
26};
27use mz_compute_types::plan::scalar::{LirScalarExpr, mfp_mir_to_lir_plan, mfp_plan_lir_to_mir};
28use mz_compute_types::plan::{ArrangementStrategy, AvailableCollections};
29use mz_dyncfg::ConfigSet;
30use mz_expr::{Eval, Id, MfpPlan};
31use mz_ore::soft_assert_or_log;
32use mz_repr::fixed_length::ExtendDatums;
33use mz_repr::{DatumVec, DatumVecBorrow, Diff, GlobalId, Row, RowArena, SharedRow};
34use mz_storage_types::controller::CollectionMetadata;
35use mz_timely_util::columnar::batcher;
36use mz_timely_util::columnar::builder::ColumnBuilder;
37use mz_timely_util::columnar::{Col2ValBatcher, Col2ValPagedBatcher, columnar_exchange};
38use mz_timely_util::columnation::ColumnationChunker;
39use timely::ContainerBuilder;
40use timely::container::{CapacityContainerBuilder, PushInto};
41use timely::dataflow::channels::pact::{ExchangeCore, Pipeline};
42use timely::dataflow::operators::Capability;
43use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
44use timely::dataflow::operators::generic::{OutputBuilder, OutputBuilderSession};
45use timely::dataflow::{Scope, Stream};
46use timely::progress::operate::FrontierInterest;
47use timely::progress::{Antichain, Timestamp};
48
49use crate::compute_state::ComputeState;
50use crate::extensions::arrange::{KeyCollection, MzArrange, MzArrangeCore};
51use crate::render::columnar::CollectionEdge;
52use crate::render::errors::{DataflowErrorSer, ErrorLogger};
53use crate::render::{LinearJoinSpec, MaybeBucketByTime, RenderTimestamp};
54use crate::typedefs::{
55    ErrAgent, ErrBatcher, ErrBuilder, ErrEnter, ErrSpine, RowRowAgent, RowRowEnter, RowRowSpine,
56};
57use mz_row_spine::{DatumSeq, RowRowBuilder, RowRowColPagedBuilder};
58
59/// Dataflow-local collections and arrangements.
60///
61/// A context means to wrap available data assets and present them in an easy-to-use manner.
62/// These assets include dataflow-local collections and arrangements, as well as imported
63/// arrangements from outside the dataflow.
64///
65/// Context has a timestamp type `T`, which is the timestamp used by the scope in question.
66pub struct Context<'scope, T: RenderTimestamp> {
67    /// The scope within which all managed collections exist.
68    ///
69    /// It is an error to add any collections not contained in this scope.
70    pub(crate) scope: Scope<'scope, T>,
71    /// The debug name of the dataflow associated with this context.
72    pub debug_name: String,
73    /// The Timely ID of the dataflow associated with this context.
74    pub dataflow_id: usize,
75    /// The collection IDs of exports of the dataflow associated with this context.
76    pub export_ids: Vec<GlobalId>,
77    /// Frontier before which updates should not be emitted.
78    ///
79    /// We *must* apply it to sinks, to ensure correct outputs.
80    /// We *should* apply it to sources and imported traces, because it improves performance.
81    pub as_of_frontier: Antichain<mz_repr::Timestamp>,
82    /// Frontier after which updates should not be emitted.
83    /// Used to limit the amount of work done when appropriate.
84    pub until: Antichain<mz_repr::Timestamp>,
85    /// Bindings of identifiers to collections.
86    pub bindings: BTreeMap<Id, CollectionBundle<'scope, T>>,
87    /// The logger, from Timely's logging framework, if logs are enabled.
88    pub(super) compute_logger: Option<crate::logging::compute::Logger>,
89    /// Specification for rendering linear joins.
90    pub(super) linear_join_spec: LinearJoinSpec,
91    /// The expiration time for dataflows in this context. The output's frontier should never advance
92    /// past this frontier, except the empty frontier.
93    pub dataflow_expiration: Antichain<mz_repr::Timestamp>,
94    /// The config set for this context.
95    pub config_set: Rc<ConfigSet>,
96}
97
98impl<'scope, T: RenderTimestamp> Context<'scope, T> {
99    /// Creates a new empty Context.
100    pub fn for_dataflow_in<Plan>(
101        dataflow: &DataflowDescription<Plan, CollectionMetadata>,
102        scope: Scope<'scope, T>,
103        compute_state: &ComputeState,
104        until: Antichain<mz_repr::Timestamp>,
105        dataflow_expiration: Antichain<mz_repr::Timestamp>,
106    ) -> Self {
107        use mz_ore::collections::CollectionExt as IteratorExt;
108        let dataflow_id = *scope.addr().into_first();
109        let as_of_frontier = dataflow
110            .as_of
111            .clone()
112            .unwrap_or_else(|| Antichain::from_elem(Timestamp::minimum()));
113
114        let export_ids = dataflow.export_ids().collect();
115
116        // Skip compute event logging for transient dataflows. We do this to avoid overhead for
117        // slow-path peeks, but it also affects subscribes. For now that seems fine, but we may
118        // want to reconsider in the future.
119        let compute_logger = if dataflow.is_transient() {
120            None
121        } else {
122            compute_state.compute_logger.clone()
123        };
124
125        Self {
126            scope,
127            debug_name: dataflow.debug_name.clone(),
128            dataflow_id,
129            export_ids,
130            as_of_frontier,
131            until,
132            bindings: BTreeMap::new(),
133            compute_logger,
134            linear_join_spec: compute_state.linear_join_spec,
135            dataflow_expiration,
136            config_set: Rc::clone(&compute_state.worker_config),
137        }
138    }
139}
140
141impl<'scope, T: RenderTimestamp> Context<'scope, T> {
142    /// Insert a collection bundle by an identifier.
143    ///
144    /// This is expected to be used to install external collections (sources, indexes, other views),
145    /// as well as for `Let` bindings of local collections.
146    pub fn insert_id(
147        &mut self,
148        id: Id,
149        collection: CollectionBundle<'scope, T>,
150    ) -> Option<CollectionBundle<'scope, T>> {
151        self.bindings.insert(id, collection)
152    }
153    /// Remove a collection bundle by an identifier.
154    ///
155    /// The primary use of this method is uninstalling `Let` bindings.
156    pub fn remove_id(&mut self, id: Id) -> Option<CollectionBundle<'scope, T>> {
157        self.bindings.remove(&id)
158    }
159    /// Melds a collection bundle to whatever exists.
160    pub fn update_id(&mut self, id: Id, collection: CollectionBundle<'scope, T>) {
161        if !self.bindings.contains_key(&id) {
162            self.bindings.insert(id, collection);
163        } else {
164            let binding = self
165                .bindings
166                .get_mut(&id)
167                .expect("Binding verified to exist");
168            if collection.collection.is_some() {
169                binding.collection = collection.collection;
170            }
171            for (key, flavor) in collection.arranged.into_iter() {
172                binding.arranged.insert(key, flavor);
173            }
174        }
175    }
176    /// Look up a collection bundle by an identifier.
177    pub fn lookup_id(&self, id: Id) -> Option<CollectionBundle<'scope, T>> {
178        self.bindings.get(&id).cloned()
179    }
180
181    pub(super) fn error_logger(&self) -> ErrorLogger {
182        ErrorLogger::new(self.debug_name.clone())
183    }
184}
185
186impl<'scope, T: RenderTimestamp> Context<'scope, T> {
187    /// Brings the underlying arrangements and collections into a region.
188    pub fn enter_region<'a>(
189        &self,
190        region: Scope<'a, T>,
191        bindings: Option<&std::collections::BTreeSet<Id>>,
192    ) -> Context<'a, T> {
193        let bindings = self
194            .bindings
195            .iter()
196            .filter(|(key, _)| bindings.as_ref().map(|b| b.contains(key)).unwrap_or(true))
197            .map(|(key, bundle)| (*key, bundle.enter_region(region)))
198            .collect();
199
200        Context {
201            scope: region,
202            debug_name: self.debug_name.clone(),
203            dataflow_id: self.dataflow_id.clone(),
204            export_ids: self.export_ids.clone(),
205            as_of_frontier: self.as_of_frontier.clone(),
206            until: self.until.clone(),
207            compute_logger: self.compute_logger.clone(),
208            linear_join_spec: self.linear_join_spec.clone(),
209            bindings,
210            dataflow_expiration: self.dataflow_expiration.clone(),
211            config_set: Rc::clone(&self.config_set),
212        }
213    }
214}
215
216/// Describes flavor of arrangement: local or imported trace.
217#[derive(Clone)]
218pub enum ArrangementFlavor<'scope, T: RenderTimestamp> {
219    /// A dataflow-local arrangement.
220    Local(
221        Arranged<'scope, RowRowAgent<T, Diff>>,
222        Arranged<'scope, ErrAgent<T, Diff>>,
223    ),
224    /// An imported trace from outside the dataflow.
225    ///
226    /// The `GlobalId` identifier exists so that exports of this same trace
227    /// can refer back to and depend on the original instance.
228    Trace(
229        GlobalId,
230        Arranged<'scope, RowRowEnter<mz_repr::Timestamp, Diff, T>>,
231        Arranged<'scope, ErrEnter<mz_repr::Timestamp, T>>,
232    ),
233}
234
235impl<'scope, T: RenderTimestamp> ArrangementFlavor<'scope, T> {
236    /// Presents `self` as a stream of updates.
237    ///
238    /// Deprecated: This function is not fueled and hence risks flattening the whole arrangement.
239    ///
240    /// This method presents the contents as they are, without further computation.
241    /// If you have logic that could be applied to each record, consider using the
242    /// `flat_map` methods which allows this and can reduce the work done.
243    #[deprecated(note = "Use `flat_map` instead.")]
244    pub fn as_collection(
245        &self,
246    ) -> (
247        VecCollection<'scope, T, Row, Diff>,
248        VecCollection<'scope, T, DataflowErrorSer, Diff>,
249    ) {
250        let mut datums = DatumVec::new();
251        let logic = move |k: DatumSeq, v: DatumSeq| {
252            let temp_storage = RowArena::new();
253            let mut datums_borrow = datums.borrow();
254            k.extend_datums(&temp_storage, &mut datums_borrow, None);
255            v.extend_datums(&temp_storage, &mut datums_borrow, None);
256            SharedRow::pack(&**datums_borrow)
257        };
258        match &self {
259            ArrangementFlavor::Local(oks, errs) => (
260                oks.clone().as_collection(logic),
261                errs.clone().as_collection(|k, &()| k.clone()),
262            ),
263            ArrangementFlavor::Trace(_, oks, errs) => (
264                oks.clone().as_collection(logic),
265                errs.clone().as_collection(|k, &()| k.clone()),
266            ),
267        }
268    }
269
270    /// Constructs and applies logic to elements of `self` and returns the results.
271    ///
272    /// The `logic` callback receives a borrow of the decoded datum vector, a timestamp, a
273    /// diff, and two output sessions: one for `ok` updates of type `(D, T, Diff)` and one for
274    /// MFP-style `DataflowErrorSer` updates. It must return the number of records *produced*
275    /// (written to either session), not the number of input tuples consumed.
276    ///
277    /// # Fuel
278    ///
279    /// The operator accumulates the returned counts as fuel and yields when the total reaches
280    /// an internal refuel threshold. The metric is output-produced (not input-consumed) on
281    /// purpose: it regulates two asymmetric pressures.
282    ///
283    /// * **Drain inputs.** The operator holds a clone of each pending `Batch` until its work
284    ///   item pops; we want to release that memory back to the upstream arrangement as soon
285    ///   as possible. A `filter(false)` MFP returns 0 for every tuple, so fuel never trips
286    ///   and the cursor runs to end-of-batch in one activation.
287    /// * **Throttle outputs.** A `map("1KB-string")` MFP produces large records per input;
288    ///   stopping when emit count hits the threshold caps how much data a single activation
289    ///   dumps on the next operator.
290    ///
291    /// The refuel constant is a pragmatic compromise: large enough to be a non-event in
292    /// steady-state, small enough that one activation can't flood downstream. There is no
293    /// universal value across MFP shapes.
294    ///
295    /// If `key` is set, this is a promise that `logic` will produce no results on
296    /// records for which the key does not evaluate to the value. This is used to
297    /// leap directly to exactly those records.
298    ///
299    /// The `max_demand` parameter limits the number of columns decoded from the
300    /// input. Only the first `max_demand` columns are decoded. Pass `usize::MAX` to
301    /// decode all columns.
302    pub fn flat_map<D, DCB, L>(
303        &self,
304        key: Option<&Row>,
305        max_demand: usize,
306        logic: L,
307    ) -> (
308        Stream<'scope, T, DCB::Container>,
309        VecCollection<'scope, T, DataflowErrorSer, Diff>,
310    )
311    where
312        D: Data,
313        DCB: ContainerBuilder + PushInto<(D, T, Diff)>,
314        L: for<'a, 'b> FnMut(
315                &'a mut DatumVecBorrow<'b>,
316                T,
317                Diff,
318                &mut Session<T, DCB>,
319                &mut Session<T, ECB<T>>,
320            ) -> usize
321            + 'static,
322    {
323        // `logic` is passed straight through to `flat_map_core_fallible`, which owns the per-row
324        // decode (and the activation-scoped arena it decodes into).
325        match &self {
326            ArrangementFlavor::Local(oks, errs) => {
327                let (oks, mfp_errs) = CollectionBundle::<T>::flat_map_core_fallible::<_, _, DCB, _>(
328                    oks.clone(),
329                    key,
330                    max_demand,
331                    logic,
332                    REFUEL,
333                );
334                let errs = errs.clone().as_collection(|k, &()| k.clone());
335                let errs = errs.concat(mfp_errs.as_collection());
336                (oks, errs)
337            }
338            ArrangementFlavor::Trace(_, oks, errs) => {
339                let (oks, mfp_errs) = CollectionBundle::<T>::flat_map_core_fallible::<_, _, DCB, _>(
340                    oks.clone(),
341                    key,
342                    max_demand,
343                    logic,
344                    REFUEL,
345                );
346                let errs = errs.clone().as_collection(|k, &()| k.clone());
347                let errs = errs.concat(mfp_errs.as_collection());
348                (oks, errs)
349            }
350        }
351    }
352
353    /// Ok-only variant of [`Self::flat_map`]. The `logic` callback receives a single output
354    /// session, cannot produce errors, and returns the number of records produced (see
355    /// [`Self::flat_map`] for fuel semantics). The returned err collection comes solely from
356    /// the arrangement; no extra operator is built to carry an empty MFP-error stream.
357    pub fn flat_map_ok<D, DCB, L>(
358        &self,
359        key: Option<&Row>,
360        max_demand: usize,
361        logic: L,
362    ) -> (
363        Stream<'scope, T, DCB::Container>,
364        VecCollection<'scope, T, DataflowErrorSer, Diff>,
365    )
366    where
367        D: Data,
368        DCB: ContainerBuilder + PushInto<(D, T, Diff)>,
369        L: for<'a, 'b> FnMut(&'a mut DatumVecBorrow<'b>, T, Diff, &mut Session<T, DCB>) -> usize
370            + 'static,
371    {
372        match &self {
373            ArrangementFlavor::Local(oks, errs) => {
374                let oks = CollectionBundle::<T>::flat_map_core_ok::<_, _, DCB, _>(
375                    oks.clone(),
376                    key,
377                    max_demand,
378                    logic,
379                    REFUEL,
380                );
381                let errs = errs.clone().as_collection(|k, &()| k.clone());
382                (oks, errs)
383            }
384            ArrangementFlavor::Trace(_, oks, errs) => {
385                let oks = CollectionBundle::<T>::flat_map_core_ok::<_, _, DCB, _>(
386                    oks.clone(),
387                    key,
388                    max_demand,
389                    logic,
390                    REFUEL,
391                );
392                let errs = errs.clone().as_collection(|k, &()| k.clone());
393                (oks, errs)
394            }
395        }
396    }
397}
398impl<'scope, T: RenderTimestamp> ArrangementFlavor<'scope, T> {
399    /// The scope containing the collection bundle.
400    pub fn scope(&self) -> Scope<'scope, T> {
401        match self {
402            ArrangementFlavor::Local(oks, _errs) => oks.stream.scope(),
403            ArrangementFlavor::Trace(_gid, oks, _errs) => oks.stream.scope(),
404        }
405    }
406
407    /// Brings the arrangement flavor into a region.
408    pub fn enter_region<'a>(&self, region: Scope<'a, T>) -> ArrangementFlavor<'a, T> {
409        match self {
410            ArrangementFlavor::Local(oks, errs) => ArrangementFlavor::Local(
411                oks.clone().enter_region(region),
412                errs.clone().enter_region(region),
413            ),
414            ArrangementFlavor::Trace(gid, oks, errs) => ArrangementFlavor::Trace(
415                *gid,
416                oks.clone().enter_region(region),
417                errs.clone().enter_region(region),
418            ),
419        }
420    }
421}
422impl<'scope, T: RenderTimestamp> ArrangementFlavor<'scope, T> {
423    /// Extracts the arrangement flavor from a region.
424    pub fn leave_region<'outer>(&self, outer: Scope<'outer, T>) -> ArrangementFlavor<'outer, T> {
425        match self {
426            ArrangementFlavor::Local(oks, errs) => ArrangementFlavor::Local(
427                oks.clone().leave_region(outer),
428                errs.clone().leave_region(outer),
429            ),
430            ArrangementFlavor::Trace(gid, oks, errs) => ArrangementFlavor::Trace(
431                *gid,
432                oks.clone().leave_region(outer),
433                errs.clone().leave_region(outer),
434            ),
435        }
436    }
437}
438
439/// A bundle of the various ways a collection can be represented.
440///
441/// This type maintains the invariant that it does contain at least one valid
442/// source of data, either a collection or at least one arrangement.
443#[derive(Clone)]
444pub struct CollectionBundle<'scope, T: RenderTimestamp> {
445    pub collection: Option<(
446        CollectionEdge<'scope, T>,
447        VecCollection<'scope, T, DataflowErrorSer, Diff>,
448    )>,
449    pub arranged: BTreeMap<Vec<LirScalarExpr>, ArrangementFlavor<'scope, T>>,
450}
451
452impl<'scope, T: RenderTimestamp> CollectionBundle<'scope, T> {
453    /// Construct a new collection bundle from update streams.
454    pub fn from_collections(
455        oks: VecCollection<'scope, T, Row, Diff>,
456        errs: VecCollection<'scope, T, DataflowErrorSer, Diff>,
457    ) -> Self {
458        Self::from_edge(CollectionEdge::Vec(oks), errs)
459    }
460
461    /// Construct a new collection bundle from a [`CollectionEdge`] and an error stream.
462    pub fn from_edge(
463        oks: CollectionEdge<'scope, T>,
464        errs: VecCollection<'scope, T, DataflowErrorSer, Diff>,
465    ) -> Self {
466        Self {
467            collection: Some((oks, errs)),
468            arranged: BTreeMap::default(),
469        }
470    }
471
472    /// Inserts arrangements by the expressions on which they are keyed.
473    pub fn from_expressions(
474        exprs: Vec<LirScalarExpr>,
475        arrangements: ArrangementFlavor<'scope, T>,
476    ) -> Self {
477        let mut arranged = BTreeMap::new();
478        arranged.insert(exprs, arrangements);
479        Self {
480            collection: None,
481            arranged,
482        }
483    }
484
485    /// Inserts arrangements by the columns on which they are keyed.
486    pub fn from_columns<I: IntoIterator<Item = usize>>(
487        columns: I,
488        arrangements: ArrangementFlavor<'scope, T>,
489    ) -> Self {
490        let mut keys = Vec::new();
491        for column in columns {
492            keys.push(LirScalarExpr::column(column));
493        }
494        Self::from_expressions(keys, arrangements)
495    }
496
497    /// The scope containing the collection bundle.
498    pub fn scope(&self) -> Scope<'scope, T> {
499        if let Some((oks, _errs)) = &self.collection {
500            oks.scope()
501        } else {
502            self.arranged
503                .values()
504                .next()
505                .expect("Must contain a valid collection")
506                .scope()
507        }
508    }
509
510    /// Brings the collection bundle into a region.
511    pub fn enter_region<'inner>(&self, region: Scope<'inner, T>) -> CollectionBundle<'inner, T> {
512        CollectionBundle {
513            collection: self.collection.as_ref().map(|(oks, errs)| {
514                (
515                    oks.clone().enter_region(region),
516                    errs.clone().enter_region(region),
517                )
518            }),
519            arranged: self
520                .arranged
521                .iter()
522                .map(|(key, bundle)| (key.clone(), bundle.enter_region(region)))
523                .collect(),
524        }
525    }
526}
527
528impl<'scope, T: RenderTimestamp> CollectionBundle<'scope, T> {
529    /// Extracts the collection bundle from a region.
530    pub fn leave_region<'outer>(&self, outer: Scope<'outer, T>) -> CollectionBundle<'outer, T> {
531        CollectionBundle {
532            collection: self.collection.as_ref().map(|(oks, errs)| {
533                (
534                    oks.clone().leave_region(outer),
535                    errs.clone().leave_region(outer),
536                )
537            }),
538            arranged: self
539                .arranged
540                .iter()
541                .map(|(key, bundle)| (key.clone(), bundle.leave_region(outer)))
542                .collect(),
543        }
544    }
545}
546
547impl<'scope, T: RenderTimestamp> CollectionBundle<'scope, T> {
548    /// Asserts that the arrangement for a specific key
549    /// (or the raw collection for no key) exists,
550    /// and returns the corresponding collection.
551    ///
552    /// This returns the collection as-is, without
553    /// doing any unthinning transformation.
554    /// Therefore, it should be used when the appropriate transformation
555    /// was planned as part of a following MFP.
556    ///
557    /// If `key` is specified, the function converts the arrangement to a collection. It uses either
558    /// the fueled `flat_map` or `as_collection` method, depending on the flag
559    /// [`ENABLE_COMPUTE_RENDER_FUELED_AS_SPECIFIC_COLLECTION`].
560    pub fn as_specific_collection(
561        &self,
562        key: Option<&[LirScalarExpr]>,
563        config_set: &ConfigSet,
564    ) -> (
565        VecCollection<'scope, T, Row, Diff>,
566        VecCollection<'scope, T, DataflowErrorSer, Diff>,
567    ) {
568        // Any operator that uses this method was told to use a particular
569        // collection during LIR planning, where we should have made
570        // sure that that collection exists.
571        //
572        // If it doesn't, we panic.
573        match key {
574            None => {
575                let (oks, errs) = self
576                    .collection
577                    .clone()
578                    .expect("The unarranged collection doesn't exist.");
579                (oks.into_vec(), errs)
580            }
581            Some(key) => {
582                let arranged = self.arranged.get(key).unwrap_or_else(|| {
583                    panic!("The collection arranged by {:?} doesn't exist.", key)
584                });
585                if ENABLE_COMPUTE_RENDER_FUELED_AS_SPECIFIC_COLLECTION.get(config_set) {
586                    // Decode all columns, pass max_demand as usize::MAX. Output is 1:1 from the
587                    // cursor (no duplicates), so a non-consolidating container builder is the
588                    // right choice.
589                    let (ok, err) = arranged
590                        .flat_map_ok::<_, CapacityContainerBuilder<Vec<(Row, T, Diff)>>, _>(
591                            None,
592                            usize::MAX,
593                            |borrow, t, r, ok_session| {
594                                ok_session.give((SharedRow::pack(borrow.iter()), t, r));
595                                1
596                            },
597                        );
598                    (ok.as_collection(), err)
599                } else {
600                    #[allow(deprecated)]
601                    arranged.as_collection()
602                }
603            }
604        }
605    }
606
607    /// Constructs and applies logic to elements of a collection and returns the results.
608    ///
609    /// The function applies `logic` on elements. The logic conceptually receives
610    /// `(&Row, &Row)` pairs in the form of a datum vec in the expected order.
611    ///
612    /// If `key_val` is set, this is a promise that `logic` will produce no results on
613    /// records for which the key does not evaluate to the value. This is used when we
614    /// have an arrangement by that key to leap directly to exactly those records.
615    /// It is important that `logic` still guard against data that does not satisfy
616    /// this constraint, as this method does not statically know that it will have
617    /// that arrangement.
618    ///
619    /// The `max_demand` parameter limits the number of columns decoded from the
620    /// input. Only the first `max_demand` columns are decoded. Pass `usize::MAX` to
621    /// decode all columns.
622    pub fn flat_map<D, DCB, L>(
623        &self,
624        key_val: Option<(Vec<LirScalarExpr>, Option<Row>)>,
625        max_demand: usize,
626        logic: L,
627    ) -> (
628        Stream<'scope, T, DCB::Container>,
629        VecCollection<'scope, T, DataflowErrorSer, Diff>,
630    )
631    where
632        D: Data,
633        DCB: ContainerBuilder + PushInto<(D, T, Diff)>,
634        L: for<'a> FnMut(
635                &'a mut DatumVecBorrow<'_>,
636                T,
637                Diff,
638                &mut Session<T, DCB>,
639                &mut Session<T, ECB<T>>,
640            ) -> usize
641            + 'static,
642    {
643        // If `key_val` is set, we should have to use the corresponding arrangement.
644        // If there isn't one, that implies an error in the contract between
645        // key-production and available arrangements.
646        if let Some((key, val)) = key_val {
647            self.arrangement(&key)
648                .expect("Should have ensured during planning that this arrangement exists.")
649                .flat_map::<_, DCB, _>(val.as_ref(), max_demand, logic)
650        } else {
651            let (oks, errs) = self
652                .collection
653                .clone()
654                .expect("Invariant violated: CollectionBundle contains no collection.");
655            let (ok_stream, err_stream) = oks.flat_map_datums::<DCB, _>(max_demand, logic);
656            let errs = errs.concat(err_stream.as_collection());
657            (ok_stream, errs)
658        }
659    }
660
661    /// Factored out common logic for using literal keys in general traces.
662    ///
663    /// This logic is sufficiently interesting that we want to write it only
664    /// once, and thereby avoid any skew in the two uses of the logic.
665    ///
666    /// The function presents the contents of the trace as `(key, value, time, delta)` tuples,
667    /// where key and value are potentially specialized, but convertible into rows. The `logic`
668    /// callback writes ok results into the first session and errors into the second, returning
669    /// the number of records produced. See [`ArrangementFlavor::flat_map`] for the fuel
670    /// rationale.
671    fn flat_map_core_fallible<Tr, D, DCB, L>(
672        trace: Arranged<'scope, Tr>,
673        key: Option<&<<BatchCursor<Tr> as Cursor>::KeyContainer as BatchContainer>::Owned>,
674        max_demand: usize,
675        mut logic: L,
676        refuel: usize,
677    ) -> (
678        Stream<'scope, T, DCB::Container>,
679        Stream<'scope, T, Vec<(DataflowErrorSer, T, Diff)>>,
680    )
681    where
682        Tr: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
683        for<'a> BatchCursor<Tr>:
684            Cursor<Key<'a>: ExtendDatums, Val<'a>: ExtendDatums, Time = T, Diff = mz_repr::Diff>,
685        <<BatchCursor<Tr> as Cursor>::KeyContainer as BatchContainer>::Owned: PartialEq,
686        D: Data,
687        DCB: ContainerBuilder + PushInto<(D, T, Diff)>,
688        // `logic` receives the key and value already decoded into a `DatumVecBorrow`. The decode
689        // (and its arena/`DatumVec`) lives in the per-activation closure below, so it is scoped to
690        // a single scheduling invocation rather than to the operator.
691        L: for<'a, 'b> FnMut(
692                &'a mut DatumVecBorrow<'b>,
693                T,
694                mz_repr::Diff,
695                &mut Session<T, DCB>,
696                &mut Session<T, ECB<T>>,
697            ) -> usize
698            + 'static,
699    {
700        let scope = trace.stream.scope();
701
702        let mut key_con = <BatchCursor<Tr> as Cursor>::KeyContainer::with_capacity(1);
703        if let Some(key) = &key {
704            key_con.push_own(key);
705        }
706        let mode = if key.is_some() { "index" } else { "scan" };
707        let name = format!("ArrangementFlatMap({})", mode);
708
709        let mut builder = OperatorBuilder::new(name, scope.clone());
710        let (ok_output, ok_stream) = builder.new_output();
711        let mut ok_output = OutputBuilder::<_, DCB>::from(ok_output);
712        let (err_output, err_stream) = builder.new_output();
713        let mut err_output = OutputBuilder::<_, ECB<T>>::from(err_output);
714        let mut input = builder.new_input(trace.stream.clone(), Pipeline);
715        let operator_info = builder.operator_info();
716
717        builder.build(move |_capabilities| {
718            // Acquire an activator to reschedule the operator when it has unfinished work.
719            let activator = scope.activator_for(operator_info.address);
720            // Maintain a list of work to do, cursor to navigate and process.
721            let mut todo = std::collections::VecDeque::new();
722            move |_frontiers| {
723                let key = key_con.get(0);
724                let mut ok_output = ok_output.activate();
725                let mut err_output = err_output.activate();
726
727                // First, dequeue all batches.
728                input.for_each(|time, data| {
729                    // Retain a capability for each output, as the work may complete across
730                    // multiple activations.
731                    let ok_cap = time.retain(0);
732                    let err_cap = time.retain(1);
733                    for batch in data.iter() {
734                        todo.push_back(PendingWork::new(
735                            ok_cap.clone(),
736                            err_cap.clone(),
737                            batch.cursor(),
738                            batch.clone(),
739                        ));
740                    }
741                });
742
743                // Decode the key/value of each record into datums for `logic`. The arena and datum
744                // buffer are created here, so they are scoped to this activation (dropped when it
745                // returns) rather than retained for the operator's lifetime; both are reused across
746                // the records processed within the activation.
747                let mut temp_storage = RowArena::new();
748                let mut datums = DatumVec::new();
749                let mut decode_logic =
750                    |k: BatchKey<'_, Tr>,
751                     v: BatchVal<'_, Tr>,
752                     t: T,
753                     d: mz_repr::Diff,
754                     ok_session: &mut Session<T, DCB>,
755                     err_session: &mut Session<T, ECB<T>>| {
756                        temp_storage.clear();
757                        let mut datums_borrow = datums.borrow();
758                        k.extend_datums(&temp_storage, &mut datums_borrow, Some(max_demand));
759                        let remaining = max_demand.saturating_sub(datums_borrow.len());
760                        v.extend_datums(&temp_storage, &mut datums_borrow, Some(remaining));
761                        logic(&mut datums_borrow, t, d, ok_session, err_session)
762                    };
763
764                // Second, make progress on `todo`.
765                let mut fuel = refuel;
766                while !todo.is_empty() && fuel > 0 {
767                    todo.front_mut().unwrap().do_work(
768                        key.as_ref(),
769                        &mut decode_logic,
770                        &mut fuel,
771                        &mut ok_output,
772                        &mut err_output,
773                    );
774                    if fuel > 0 {
775                        todo.pop_front();
776                    }
777                }
778                // If we have not finished all work, re-activate the operator.
779                if !todo.is_empty() {
780                    activator.activate();
781                }
782            }
783        });
784
785        (ok_stream, err_stream)
786    }
787
788    /// Ok-only variant of [`Self::flat_map_core_fallible`]. The `logic` callback writes results
789    /// into a single output session and returns the number of records produced (see the
790    /// fallible variant for fuel semantics). Use this when the caller statically knows it
791    /// will never produce `DataflowErrorSer` records, to avoid building a second output port
792    /// and the empty err stream that would follow it.
793    fn flat_map_core_ok<Tr, D, DCB, L>(
794        trace: Arranged<'scope, Tr>,
795        key: Option<&<<BatchCursor<Tr> as Cursor>::KeyContainer as BatchContainer>::Owned>,
796        max_demand: usize,
797        mut logic: L,
798        refuel: usize,
799    ) -> Stream<'scope, T, DCB::Container>
800    where
801        Tr: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
802        for<'a> BatchCursor<Tr>:
803            Cursor<Key<'a>: ExtendDatums, Val<'a>: ExtendDatums, Time = T, Diff = mz_repr::Diff>,
804        <<BatchCursor<Tr> as Cursor>::KeyContainer as BatchContainer>::Owned: PartialEq,
805        D: Data,
806        DCB: ContainerBuilder + PushInto<(D, T, Diff)>,
807        // See `flat_map_core_fallible`: `logic` takes already-decoded datums; the decode lives in
808        // the per-activation closure below.
809        L: for<'a, 'b> FnMut(
810                &'a mut DatumVecBorrow<'b>,
811                T,
812                mz_repr::Diff,
813                &mut Session<T, DCB>,
814            ) -> usize
815            + 'static,
816    {
817        let scope = trace.stream.scope();
818
819        let mut key_con = <BatchCursor<Tr> as Cursor>::KeyContainer::with_capacity(1);
820        if let Some(key) = &key {
821            key_con.push_own(key);
822        }
823        let mode = if key.is_some() { "index" } else { "scan" };
824        let name = format!("ArrangementFlatMapOk({})", mode);
825
826        let mut builder = OperatorBuilder::new(name, scope.clone());
827        let (ok_output, ok_stream) = builder.new_output();
828        let mut ok_output = OutputBuilder::<_, DCB>::from(ok_output);
829        let mut input = builder.new_input(trace.stream.clone(), Pipeline);
830        let operator_info = builder.operator_info();
831
832        builder.build(move |_capabilities| {
833            let activator = scope.activator_for(operator_info.address);
834            let mut todo = std::collections::VecDeque::new();
835            move |_frontiers| {
836                let key = key_con.get(0);
837                let mut ok_output = ok_output.activate();
838
839                input.for_each(|time, data| {
840                    let cap = time.retain(0);
841                    for batch in data.iter() {
842                        todo.push_back(PendingWorkOk::new(
843                            cap.clone(),
844                            batch.cursor(),
845                            batch.clone(),
846                        ));
847                    }
848                });
849
850                // Activation-scoped decode storage; see `flat_map_core_fallible`.
851                let mut temp_storage = RowArena::new();
852                let mut datums = DatumVec::new();
853                let mut decode_logic =
854                    |k: BatchKey<'_, Tr>,
855                     v: BatchVal<'_, Tr>,
856                     t: T,
857                     d: mz_repr::Diff,
858                     ok_session: &mut Session<T, DCB>| {
859                        temp_storage.clear();
860                        let mut datums_borrow = datums.borrow();
861                        k.extend_datums(&temp_storage, &mut datums_borrow, Some(max_demand));
862                        let remaining = max_demand.saturating_sub(datums_borrow.len());
863                        v.extend_datums(&temp_storage, &mut datums_borrow, Some(remaining));
864                        logic(&mut datums_borrow, t, d, ok_session)
865                    };
866
867                let mut fuel = refuel;
868                while !todo.is_empty() && fuel > 0 {
869                    todo.front_mut().unwrap().do_work(
870                        key.as_ref(),
871                        &mut decode_logic,
872                        &mut fuel,
873                        &mut ok_output,
874                    );
875                    if fuel > 0 {
876                        todo.pop_front();
877                    }
878                }
879                if !todo.is_empty() {
880                    activator.activate();
881                }
882            }
883        });
884
885        ok_stream
886    }
887
888    /// Look up an arrangement by the expressions that form the key.
889    ///
890    /// The result may be `None` if no such arrangement exists, or it may be one of many
891    /// "arrangement flavors" that represent the types of arranged data we might have.
892    pub fn arrangement(&self, key: &[LirScalarExpr]) -> Option<ArrangementFlavor<'scope, T>> {
893        self.arranged.get(key).map(|x| x.clone())
894    }
895}
896
897impl<'scope, T: RenderTimestamp> CollectionBundle<'scope, T> {
898    /// Presents `self` as a stream of updates, having been subjected to `mfp`.
899    ///
900    /// This operator is able to apply the logic of `mfp` early, which can substantially
901    /// reduce the amount of data produced when `mfp` is non-trivial.
902    ///
903    /// The `key_val` argument, when present, indicates that a specific arrangement should
904    /// be used, and if, in addition, the `val` component is present,
905    /// that we can seek to the supplied row.
906    pub fn as_collection_core(
907        &self,
908        mfp_plan: MfpPlan<LirScalarExpr>,
909        key_val: Option<(Vec<LirScalarExpr>, Option<Row>)>,
910        until: Antichain<mz_repr::Timestamp>,
911        config_set: &ConfigSet,
912    ) -> (
913        VecCollection<'scope, T, mz_repr::Row, Diff>,
914        VecCollection<'scope, T, DataflowErrorSer, Diff>,
915    ) {
916        // If the MFP is trivial, we can just call `as_collection`.
917        // In the case that we weren't going to apply the `key_val` optimization,
918        // this path results in a slightly smaller and faster
919        // dataflow graph, and is intended to fix
920        // https://github.com/MaterializeInc/database-issues/issues/3111
921        let has_key_val = if let Some((_key, Some(_val))) = &key_val {
922            true
923        } else {
924            false
925        };
926
927        if mfp_plan.is_identity() && !has_key_val {
928            let key = key_val.map(|(k, _v)| k);
929            return self.as_specific_collection(key.as_deref(), config_set);
930        }
931
932        // Apply demand-based column pruning. We round-trip through MIR
933        // so temporal bounds are folded back as mz_now() predicates —
934        // this way demand() sees all column references (including those
935        // in temporal bounds), and permute_fn applies uniformly.
936        let (mfp_plan, max_demand) = {
937            let mut mir_mfp = mfp_plan_lir_to_mir(mfp_plan).into_map_filter_project();
938            let max_demand = mir_mfp.demand().last().map(|x| *x + 1).unwrap_or(0);
939            mir_mfp.permute_fn(|c| c, max_demand);
940            mir_mfp.optimize();
941            let plan = mfp_mir_to_lir_plan(mir_mfp);
942            (plan, max_demand)
943        };
944
945        let mut datum_vec = DatumVec::new();
946        // Wrap in an `Rc` so that lifetimes work out.
947        let until = std::rc::Rc::new(until);
948
949        let (stream, errors) = self
950            .flat_map::<_, ConsolidatingContainerBuilder<Vec<(Row, T, Diff)>>, _>(
951                key_val,
952                max_demand,
953                move |row_datums, time, diff, ok_session, err_session| {
954                    let mut row_builder = SharedRow::get();
955                    let until = std::rc::Rc::clone(&until);
956                    let temp_storage = RowArena::new();
957                    let row_iter = row_datums.iter();
958                    let mut datums_local = datum_vec.borrow();
959                    datums_local.extend(row_iter);
960                    let event_time = time.event_time();
961                    let mut work: usize = 0;
962                    for result in mfp_plan.evaluate(
963                        &mut datums_local,
964                        &temp_storage,
965                        event_time,
966                        diff.clone(),
967                        move |time| !until.less_equal(time),
968                        &mut row_builder,
969                    ) {
970                        work += 1;
971                        match result {
972                            Ok((row, event_time, diff)) => {
973                                // Copy the whole time, and re-populate event time.
974                                let mut time: T = time.clone();
975                                *time.event_time_mut() = event_time;
976                                ok_session.give((row, time, diff));
977                            }
978                            Err((e, event_time, diff)) => {
979                                // Copy the whole time, and re-populate event time.
980                                let mut time: T = time.clone();
981                                *time.event_time_mut() = event_time;
982                                err_session.give((e, time, diff));
983                            }
984                        }
985                    }
986                    work
987                },
988            );
989
990        (stream.as_collection(), errors)
991    }
992    pub fn ensure_collections(
993        mut self,
994        collections: AvailableCollections,
995        input_key: Option<Vec<LirScalarExpr>>,
996        input_mfp: MfpPlan<LirScalarExpr>,
997        as_of: Antichain<mz_repr::Timestamp>,
998        until: Antichain<mz_repr::Timestamp>,
999        config_set: &ConfigSet,
1000        strategy: ArrangementStrategy,
1001    ) -> Self
1002    where
1003        T: MaybeBucketByTime,
1004    {
1005        if collections == Default::default() {
1006            return self;
1007        }
1008        // Cache collection to avoid reforming it each time.
1009        //
1010        // TODO(mcsherry): In theory this could be faster run out of another arrangement,
1011        // as the `map_fallible` that follows could be run against an arrangement itself.
1012        //
1013        // Note(btv): If we ever do that, we would then only need to make the raw collection here
1014        // if `collections.raw` is true.
1015
1016        for (key, _, _) in collections.arranged.iter() {
1017            soft_assert_or_log!(
1018                !self.arranged.contains_key(key),
1019                "LIR ArrangeBy tried to create an existing arrangement"
1020            );
1021        }
1022
1023        // Track whether we already applied temporal bucketing in this call, to
1024        // avoid bucketing the same updates twice.
1025        let mut bucketed = false;
1026
1027        // True iff at least one new arrangement will actually be built below. Bucketing only
1028        // pays off when something downstream merges/compacts the future-stamped updates; on a
1029        // pure raw collection (no new arrangement) the work is wasted.
1030        let will_create_arrangement = collections
1031            .arranged
1032            .iter()
1033            .any(|(key, _, _)| !self.arranged.contains_key(key));
1034
1035        // We need the collection if either (1) it is explicitly demanded, or (2) we are going to render any arrangement
1036        let form_raw_collection = collections.raw || will_create_arrangement;
1037        if form_raw_collection && self.collection.is_none() {
1038            let (oks, errs) =
1039                self.as_collection_core(input_mfp, input_key.map(|k| (k, None)), until, config_set);
1040            // Apply temporal bucketing when the lowering selected `TemporalBucketing` and
1041            // we will build at least one arrangement. This path fires when the collection
1042            // must be formed from scratch (e.g., from an arrangement via as_collection_core).
1043            let effective_strategy = if will_create_arrangement {
1044                strategy
1045            } else {
1046                ArrangementStrategy::Direct
1047            };
1048            let oks = if matches!(effective_strategy, ArrangementStrategy::TemporalBucketing)
1049                && ENABLE_COMPUTE_TEMPORAL_BUCKETING.get(config_set)
1050            {
1051                let summary: mz_repr::Timestamp = TEMPORAL_BUCKETING_SUMMARY
1052                    .get(config_set)
1053                    .try_into()
1054                    .expect("must fit");
1055                bucketed = true;
1056                T::maybe_apply_temporal_bucketing(oks.inner, as_of.clone(), summary)
1057            } else {
1058                oks
1059            };
1060            self.collection = Some((CollectionEdge::Vec(oks), errs));
1061        }
1062        for (key, _, thinning) in collections.arranged {
1063            if !self.arranged.contains_key(&key) {
1064                // TODO: Consider allowing more expressive names.
1065                let name = format!("ArrangeBy[{:?}]", key);
1066
1067                let (oks, errs) = self
1068                    .collection
1069                    .take()
1070                    .expect("Collection constructed above");
1071                let oks = oks.into_vec();
1072                // Apply temporal bucketing if the collection already existed on
1073                // the bundle (e.g., from an upstream temporal Mfp or Get) and we
1074                // haven't bucketed yet. This is the common path for temporal-MFP
1075                // → ArrangeBy flows.
1076                let effective_strategy = if bucketed {
1077                    ArrangementStrategy::Direct
1078                } else {
1079                    strategy
1080                };
1081                let oks = if matches!(effective_strategy, ArrangementStrategy::TemporalBucketing)
1082                    && ENABLE_COMPUTE_TEMPORAL_BUCKETING.get(config_set)
1083                {
1084                    let summary: mz_repr::Timestamp = TEMPORAL_BUCKETING_SUMMARY
1085                        .get(config_set)
1086                        .try_into()
1087                        .expect("must fit");
1088                    bucketed = true;
1089                    T::maybe_apply_temporal_bucketing(oks.inner, as_of.clone(), summary)
1090                } else {
1091                    oks
1092                };
1093                let use_paged_path = ENABLE_COLUMN_PAGED_BATCHER.get(config_set);
1094                let (oks, errs_keyed, passthrough) = Self::arrange_collection(
1095                    &name,
1096                    oks,
1097                    key.clone(),
1098                    thinning.clone(),
1099                    use_paged_path,
1100                );
1101                let errs_concat: KeyCollection<_, _, _> = errs.clone().concat(errs_keyed).into();
1102                self.collection = Some((CollectionEdge::Vec(passthrough), errs));
1103                let errs =
1104                    errs_concat.mz_arrange::<
1105                        ColumnationChunker<_>,
1106                        ErrBatcher<_, _>,
1107                        ErrBuilder<_, _>,
1108                        ErrSpine<_, _>,
1109                    >(
1110                        &format!("{}-errors", name),
1111                    );
1112                self.arranged
1113                    .insert(key, ArrangementFlavor::Local(oks, errs));
1114            }
1115        }
1116        self
1117    }
1118
1119    /// Builds an arrangement from a collection, using the specified key and value thinning.
1120    ///
1121    /// The arrangement's key is based on the `key` expressions, and the value the input with
1122    /// the `thinning` applied to it. It selects which of the input columns are included in the
1123    /// value of the arrangement. The thinning is in support of permuting arrangements such that
1124    /// columns in the key are not included in the value.
1125    ///
1126    /// In addition to the ok and err streams, we produce a passthrough stream that forwards
1127    /// the input as-is, which allows downstream consumers to reuse the collection without
1128    /// teeing the stream.
1129    fn arrange_collection(
1130        name: &String,
1131        oks: VecCollection<'scope, T, Row, Diff>,
1132        key: Vec<LirScalarExpr>,
1133        thinning: Vec<usize>,
1134        use_paged_path: bool,
1135    ) -> (
1136        Arranged<'scope, RowRowAgent<T, Diff>>,
1137        VecCollection<'scope, T, DataflowErrorSer, Diff>,
1138        VecCollection<'scope, T, Row, Diff>,
1139    ) {
1140        // This operator implements a `map_fallible`, but produces columnar updates for the ok
1141        // stream. The `map_fallible` cannot be used here because the closure cannot return
1142        // references, which is what we need to push into columnar streams. Instead, we use a
1143        // bespoke operator that also optimizes reuse of allocations across individual updates.
1144        let mut builder = OperatorBuilder::new("FormArrangementKey".to_string(), oks.inner.scope());
1145        let (ok_output, ok_stream) = builder.new_output();
1146        let mut ok_output =
1147            OutputBuilder::<_, ColumnBuilder<((Row, Row), T, Diff)>>::from(ok_output);
1148        let (err_output, err_stream) = builder.new_output();
1149        let mut err_output = OutputBuilder::from(err_output);
1150        let (passthrough_output, passthrough_stream) = builder.new_output();
1151        let mut passthrough_output = OutputBuilder::from(passthrough_output);
1152        let mut input = builder.new_input(oks.inner, Pipeline);
1153        builder.set_notify_for(0, FrontierInterest::Never);
1154        builder.build(move |_capabilities| {
1155            let mut key_buf = Row::default();
1156            let mut val_buf = Row::default();
1157            let mut datums = DatumVec::new();
1158            move |_frontiers| {
1159                // Scoped to the activation so the arena's retained capacity does not outlive a
1160                // single scheduling invocation; cleared per row to reuse it within the batch.
1161                let mut temp_storage = RowArena::new();
1162                let mut ok_output = ok_output.activate();
1163                let mut err_output = err_output.activate();
1164                let mut passthrough_output = passthrough_output.activate();
1165                input.for_each(|time, data| {
1166                    let mut ok_session = ok_output.session_with_builder(&time);
1167                    let mut err_session = err_output.session(&time);
1168                    for (row, time, diff) in data.iter() {
1169                        temp_storage.clear();
1170                        let datums = datums.borrow_with(row);
1171                        let key_iter = key.iter().map(|k| k.eval(&datums, &temp_storage));
1172                        match key_buf.packer().try_extend(key_iter) {
1173                            Ok(()) => {
1174                                let val_datum_iter = thinning.iter().map(|c| datums[*c]);
1175                                val_buf.packer().extend(val_datum_iter);
1176                                ok_session.give(((&*key_buf, &*val_buf), time, diff));
1177                            }
1178                            Err(e) => {
1179                                err_session.give((e.into(), time.clone(), *diff));
1180                            }
1181                        }
1182                    }
1183                    passthrough_output.session(&time).give_container(data);
1184                });
1185            }
1186        });
1187
1188        let exchange =
1189            ExchangeCore::<ColumnBuilder<_>, _>::new_core(columnar_exchange::<Row, Row, T, Diff>);
1190        let oks = if use_paged_path {
1191            ok_stream.mz_arrange_core::<
1192                _,
1193                batcher::ColumnChunker<_>,
1194                Col2ValPagedBatcher<_, _, _, _>,
1195                RowRowColPagedBuilder<_, _>,
1196                RowRowSpine<_, _>,
1197            >(exchange, name)
1198        } else {
1199            ok_stream.mz_arrange_core::<
1200                _,
1201                batcher::Chunker<_>,
1202                Col2ValBatcher<_, _, _, _>,
1203                RowRowBuilder<_, _>,
1204                RowRowSpine<_, _>,
1205            >(exchange, name)
1206        };
1207        (
1208            oks,
1209            err_stream.as_collection(),
1210            passthrough_stream.as_collection(),
1211        )
1212    }
1213}
1214
1215/// Type alias for a timely output `Session` whose capability is a `Capability<T>`. The container
1216/// builder `CB` is left to the caller; sessions can therefore drive consolidating, capacity, or
1217/// (in the future) columnar output builders without changing call sites.
1218pub(crate) type Session<'a, 'b, T, CB> =
1219    timely::dataflow::operators::generic::Session<'a, 'b, T, CB, Capability<T>>;
1220
1221/// Container builder used for the err output of every flat_map variant. Pre-refactor the
1222/// merged Ok/Err stream flowed through a [`ConsolidatingContainerBuilder`] before the
1223/// `map_fallible` demux split it; we preserve that consolidation here so errors with the
1224/// same `(error, time)` cancel within a batch rather than propagating to downstream.
1225pub(crate) type ECB<T> = ConsolidatingContainerBuilder<Vec<(DataflowErrorSer, T, Diff)>>;
1226
1227/// Number of output records the arrangement flat_map operators may produce before yielding.
1228/// See [`ArrangementFlavor::flat_map`] for the fuel rationale; the constant is a pragmatic
1229/// compromise and not tuned empirically.
1230const REFUEL: usize = 1_000_000;
1231
1232struct PendingWork<C>
1233where
1234    C: Cursor,
1235{
1236    /// Capability for the `ok` output (output port 0).
1237    ok_capability: Capability<C::Time>,
1238    /// Capability for the `err` output (output port 1).
1239    err_capability: Capability<C::Time>,
1240    cursor: C,
1241    batch: C::Storage,
1242}
1243
1244impl<C> PendingWork<C>
1245where
1246    C: Cursor<KeyContainer: BatchContainer<Owned: PartialEq + Sized>>,
1247{
1248    /// Create a new bundle of pending work, from a pair of capabilities (one per output),
1249    /// a cursor, and backing storage.
1250    fn new(
1251        ok_capability: Capability<C::Time>,
1252        err_capability: Capability<C::Time>,
1253        cursor: C,
1254        batch: C::Storage,
1255    ) -> Self {
1256        Self {
1257            ok_capability,
1258            err_capability,
1259            cursor,
1260            batch,
1261        }
1262    }
1263    /// Perform roughly `fuel` work through the cursor, applying `logic` and sending results to
1264    /// the two output sessions.
1265    fn do_work<D, DCB, L>(
1266        &mut self,
1267        key: Option<&C::Key<'_>>,
1268        logic: &mut L,
1269        fuel: &mut usize,
1270        ok_output: &mut OutputBuilderSession<'_, C::Time, DCB>,
1271        err_output: &mut OutputBuilderSession<'_, C::Time, ECB<C::Time>>,
1272    ) where
1273        D: Data,
1274        DCB: ContainerBuilder + PushInto<(D, C::Time, C::Diff)>,
1275        L: FnMut(
1276            C::Key<'_>,
1277            C::Val<'_>,
1278            C::Time,
1279            C::Diff,
1280            &mut Session<C::Time, DCB>,
1281            &mut Session<C::Time, ECB<C::Time>>,
1282        ) -> usize,
1283    {
1284        let mut ok_session = ok_output.session_with_builder(&self.ok_capability);
1285        let mut err_session = err_output.session_with_builder(&self.err_capability);
1286        walk_cursor(&mut self.cursor, &self.batch, key, fuel, |k, v, t, d| {
1287            logic(k, v, t, d, &mut ok_session, &mut err_session)
1288        });
1289    }
1290}
1291
1292/// Pending work for the Ok-only variant of `flat_map_core_fallible`. Holds a single capability since
1293/// the operator has only one output port.
1294struct PendingWorkOk<C>
1295where
1296    C: Cursor,
1297{
1298    capability: Capability<C::Time>,
1299    cursor: C,
1300    batch: C::Storage,
1301}
1302
1303impl<C> PendingWorkOk<C>
1304where
1305    C: Cursor<KeyContainer: BatchContainer<Owned: PartialEq + Sized>>,
1306{
1307    fn new(capability: Capability<C::Time>, cursor: C, batch: C::Storage) -> Self {
1308        Self {
1309            capability,
1310            cursor,
1311            batch,
1312        }
1313    }
1314
1315    /// Perform roughly `fuel` work through the cursor, applying `logic` and sending results to
1316    /// the single output session.
1317    fn do_work<D, DCB, L>(
1318        &mut self,
1319        key: Option<&C::Key<'_>>,
1320        logic: &mut L,
1321        fuel: &mut usize,
1322        ok_output: &mut OutputBuilderSession<'_, C::Time, DCB>,
1323    ) where
1324        D: Data,
1325        DCB: ContainerBuilder + PushInto<(D, C::Time, C::Diff)>,
1326        L: FnMut(C::Key<'_>, C::Val<'_>, C::Time, C::Diff, &mut Session<C::Time, DCB>) -> usize,
1327    {
1328        let mut ok_session = ok_output.session_with_builder(&self.capability);
1329        walk_cursor(&mut self.cursor, &self.batch, key, fuel, |k, v, t, d| {
1330            logic(k, v, t, d, &mut ok_session)
1331        });
1332    }
1333}
1334
1335/// Walk a cursor, calling `emit` for each consolidated `(key, val, time, diff)` tuple. If
1336/// `key` is set, the cursor is seeked to it and only values for that key are produced.
1337///
1338/// `emit` returns the number of records it produced for the given input tuple. The cursor
1339/// stops as soon as the accumulated emit count reaches `*fuel`, leaving the cursor in place
1340/// so work can resume on a later call. Within a batch, both the inner val loop and the
1341/// outer key loop are bounded only by emit count, so selective filters (`emit` returns 0)
1342/// run to batch completion in a single activation — see [`ArrangementFlavor::flat_map`]
1343/// for why fuel counts output rather than input.
1344fn walk_cursor<C, F>(
1345    cursor: &mut C,
1346    batch: &C::Storage,
1347    key: Option<&C::Key<'_>>,
1348    fuel: &mut usize,
1349    mut emit: F,
1350) where
1351    C: Cursor<KeyContainer: BatchContainer<Owned: PartialEq + Sized>>,
1352    F: FnMut(C::Key<'_>, C::Val<'_>, C::Time, C::Diff) -> usize,
1353{
1354    use differential_dataflow::consolidation::consolidate;
1355
1356    let mut work: usize = 0;
1357    let mut buffer = Vec::new();
1358    if let Some(key) = key {
1359        let key = C::KeyContainer::reborrow(*key);
1360        if cursor.get_key(batch).map(|k| k == key) != Some(true) {
1361            cursor.seek_key(batch, key);
1362        }
1363        if cursor.get_key(batch).map(|k| k == key) == Some(true) {
1364            let key = cursor.key(batch);
1365            while let Some(val) = cursor.get_val(batch) {
1366                cursor.map_times(batch, |time, diff| {
1367                    buffer.push((C::owned_time(time), C::owned_diff(diff)));
1368                });
1369                consolidate(&mut buffer);
1370                for (time, diff) in buffer.drain(..) {
1371                    work += emit(key, val, time, diff);
1372                }
1373                cursor.step_val(batch);
1374                if work >= *fuel {
1375                    *fuel = 0;
1376                    return;
1377                }
1378            }
1379        }
1380    } else {
1381        while let Some(key) = cursor.get_key(batch) {
1382            while let Some(val) = cursor.get_val(batch) {
1383                cursor.map_times(batch, |time, diff| {
1384                    buffer.push((C::owned_time(time), C::owned_diff(diff)));
1385                });
1386                consolidate(&mut buffer);
1387                for (time, diff) in buffer.drain(..) {
1388                    work += emit(key, val, time, diff);
1389                }
1390                cursor.step_val(batch);
1391                if work >= *fuel {
1392                    *fuel = 0;
1393                    return;
1394                }
1395            }
1396            cursor.step_key(batch);
1397        }
1398    }
1399    *fuel -= work;
1400}