Skip to main content

mz_compute/
render.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//! Renders a plan into a timely/differential dataflow computation.
11//!
12//! ## Error handling
13//!
14//! Timely and differential have no idioms for computations that can error. The
15//! philosophy is, reasonably, to define the semantics of the computation such
16//! that errors are unnecessary: e.g., by using wrap-around semantics for
17//! integer overflow.
18//!
19//! Unfortunately, SQL semantics are not nearly so elegant, and require errors
20//! in myriad cases. The classic example is a division by zero, but invalid
21//! input for casts, overflowing integer operations, and dozens of other
22//! functions need the ability to produce errors ar runtime.
23//!
24//! At the moment, only *scalar* expression evaluation can fail, so only
25//! operators that evaluate scalar expressions can fail. At the time of writing,
26//! that includes map, filter, reduce, and join operators. Constants are a bit
27//! of a special case: they can be either a constant vector of rows *or* a
28//! constant, singular error.
29//!
30//! The approach taken is to build two parallel trees of computation: one for
31//! the rows that have been successfully evaluated (the "oks tree"), and one for
32//! the errors that have been generated (the "errs tree"). For example:
33//!
34//! ```text
35//!    oks1  errs1       oks2  errs2
36//!      |     |           |     |
37//!      |     |           |     |
38//!   project  |           |     |
39//!      |     |           |     |
40//!      |     |           |     |
41//!     map    |           |     |
42//!      |\    |           |     |
43//!      | \   |           |     |
44//!      |  \  |           |     |
45//!      |   \ |           |     |
46//!      |    \|           |     |
47//!   project  +           +     +
48//!      |     |          /     /
49//!      |     |         /     /
50//!    join ------------+     /
51//!      |     |             /
52//!      |     | +----------+
53//!      |     |/
54//!     oks   errs
55//! ```
56//!
57//! The project operation cannot fail, so errors from errs1 are propagated
58//! directly. Map operators are fallible and so can inject additional errors
59//! into the stream. Join operators combine the errors from each of their
60//! inputs.
61//!
62//! The semantics of the error stream are minimal. From the perspective of SQL,
63//! a dataflow is considered to be in an error state if there is at least one
64//! element in the final errs collection. The error value returned to the user
65//! is selected arbitrarily; SQL only makes provisions to return one error to
66//! the user at a time. There are plans to make the err collection accessible to
67//! end users, so they can see all errors at once.
68//!
69//! To make errors transient, simply ensure that the operator can retract any
70//! produced errors when corrected data arrives. To make errors permanent, write
71//! the operator such that it never retracts the errors it produced. Future work
72//! will likely want to introduce some sort of sort order for errors, so that
73//! permanent errors are returned to the user ahead of transient errors—probably
74//! by introducing a new error type a la:
75//!
76//! ```no_run
77//! # struct EvalError;
78//! # struct SourceError;
79//! enum DataflowError {
80//!     Transient(EvalError),
81//!     Permanent(SourceError),
82//! }
83//! ```
84//!
85//! If the error stream is empty, the oks stream must be correct. If the error
86//! stream is non-empty, then there are no semantics for the oks stream. This is
87//! sufficient to support SQL in its current form, but is likely to be
88//! unsatisfactory long term. We suspect that we can continue to imbue the oks
89//! stream with semantics if we are very careful in describing what data should
90//! and should not be produced upon encountering an error. Roughly speaking, the
91//! oks stream could represent the correct result of the computation where all
92//! rows that caused an error have been pruned from the stream. There are
93//! strange and confusing questions here around foreign keys, though: what if
94//! the optimizer proves that a particular key must exist in a collection, but
95//! the key gets pruned away because its row participated in a scalar expression
96//! evaluation that errored?
97//!
98//! In the meantime, it is probably wise for operators to keep the oks stream
99//! roughly "as correct as possible" even when errors are present in the errs
100//! stream. This reduces the amount of recomputation that must be performed
101//! if/when the errors are retracted.
102
103use std::any::Any;
104use std::cell::RefCell;
105use std::collections::{BTreeMap, BTreeSet};
106use std::convert::Infallible;
107use std::future::Future;
108use std::pin::Pin;
109use std::rc::{Rc, Weak};
110use std::sync::Arc;
111use std::task::Poll;
112
113use differential_dataflow::dynamic::pointstamp::PointStamp;
114use differential_dataflow::lattice::Lattice;
115use differential_dataflow::operators::arrange::Arranged;
116use differential_dataflow::operators::arrange::ShutdownButton;
117use differential_dataflow::operators::iterate::Variable;
118use differential_dataflow::trace::cursor::{BatchCursor, BatchDiff, BatchKey, BatchVal};
119use differential_dataflow::trace::{BatchReader, Cursor, Navigable, TraceReader};
120use differential_dataflow::{AsCollection, Data, VecCollection};
121use futures::FutureExt;
122use futures::channel::oneshot;
123use itertools::Itertools;
124use mz_compute_types::dataflows::{DataflowDescription, IndexDesc};
125use mz_compute_types::dyncfgs::{
126    COMPUTE_APPLY_COLUMN_DEMANDS, COMPUTE_LOGICAL_BACKPRESSURE_INFLIGHT_SLACK,
127    COMPUTE_LOGICAL_BACKPRESSURE_MAX_RETAINED_CAPABILITIES, ENABLE_COMPUTE_LOGICAL_BACKPRESSURE,
128    ENABLE_COMPUTE_TEMPORAL_BUCKETING, SUBSCRIBE_SNAPSHOT_OPTIMIZATION, TEMPORAL_BUCKETING_SUMMARY,
129};
130use mz_compute_types::plan::render_plan::{
131    self, BindStage, LetBind, LetFreePlan, RecBind, RenderPlan,
132};
133use mz_compute_types::plan::scalar::LirScalarExpr;
134use mz_compute_types::plan::{ArrangementStrategy, LirId};
135use mz_expr::{EvalError, Id, LocalId, permutation_for_arrangement};
136use mz_persist_client::operators::shard_source::{ErrorHandler, SnapshotMode};
137use mz_repr::explain::DummyHumanizer;
138use mz_repr::fixed_length::ExtendDatums;
139use mz_repr::{Datum, DatumVec, Diff, GlobalId, ReprRelationType, Row, RowArena, SharedRow};
140use mz_storage_operators::persist_source;
141use mz_storage_types::controller::CollectionMetadata;
142use mz_timely_util::columnation::ColumnationChunker;
143use mz_timely_util::operator::{CollectionExt, StreamExt};
144use mz_timely_util::probe::{Handle as MzProbeHandle, ProbeNotify};
145use mz_timely_util::scope_label::ScopeExt;
146use timely::PartialOrder;
147use timely::container::CapacityContainerBuilder;
148use timely::dataflow::channels::pact::Pipeline;
149use timely::dataflow::operators::vec::ToStream;
150use timely::dataflow::operators::vec::{BranchWhen, Filter};
151use timely::dataflow::operators::{Capability, Operator, Probe, probe};
152use timely::dataflow::{Scope, Stream, StreamVec};
153use timely::order::{Product, TotalOrder};
154use timely::progress::timestamp::Refines;
155use timely::progress::{Antichain, Timestamp};
156use timely::scheduling::ActivateOnDrop;
157use timely::worker::Worker as TimelyWorker;
158
159use crate::arrangement::manager::TraceBundle;
160use crate::compute_state::ComputeState;
161use crate::extensions::arrange::{KeyCollection, MzArrange};
162use crate::extensions::reduce::MzReduce;
163use crate::extensions::temporal_bucket::TemporalBucketing;
164use crate::logging::compute::{
165    ComputeEvent, DataflowGlobal, LirMapping, LirMetadata, LogDataflowErrors, OperatorHydration,
166};
167use crate::render::columnar::CollectionEdge;
168use crate::render::context::{ArrangementFlavor, Context};
169use crate::render::errors::DataflowErrorSer;
170use crate::typedefs::{ErrBatcher, ErrBuilder, ErrSpine, KeyBatcher, MzTimestamp};
171use mz_row_spine::{DatumSeq, RowRowBatcher, RowRowBuilder};
172
173pub(crate) mod columnar;
174pub mod context;
175pub(crate) mod errors;
176mod flat_map;
177mod join;
178mod reduce;
179pub mod sinks;
180mod threshold;
181mod top_k;
182
183pub use context::CollectionBundle;
184pub use join::LinearJoinSpec;
185
186/// Guard that presses a differential [`ShutdownButton`] when dropped.
187///
188/// Dropping this guard releases the imported trace's capabilities.
189struct PressOnDrop<T>(ShutdownButton<T>);
190
191impl<T> Drop for PressOnDrop<T> {
192    fn drop(&mut self) {
193        self.0.press();
194    }
195}
196
197/// Assemble the "compute"  side of a dataflow, i.e. all but the sources.
198///
199/// This method imports sources from provided assets, and then builds the remaining
200/// dataflow using "compute-local" assets like shared arrangements, and producing
201/// both arrangements and sinks.
202pub fn build_compute_dataflow(
203    timely_worker: &mut TimelyWorker,
204    compute_state: &mut ComputeState,
205    dataflow: DataflowDescription<RenderPlan, CollectionMetadata>,
206    start_signal: StartSignal,
207    until: Antichain<mz_repr::Timestamp>,
208    dataflow_expiration: Antichain<mz_repr::Timestamp>,
209) {
210    // Mutually recursive view definitions require special handling.
211    let recursive = dataflow
212        .objects_to_build
213        .iter()
214        .any(|object| object.plan.is_recursive());
215
216    // Determine indexes to export, and their dependencies.
217    let indexes = dataflow
218        .index_exports
219        .iter()
220        .map(|(idx_id, (idx, _typ))| (*idx_id, dataflow.depends_on(idx.on_id), idx.as_lir()))
221        .collect::<Vec<_>>();
222
223    // Determine sinks to export, and their dependencies.
224    let sinks = dataflow
225        .sink_exports
226        .iter()
227        .map(|(sink_id, sink)| (*sink_id, dataflow.depends_on(sink.from), sink.clone()))
228        .collect::<Vec<_>>();
229
230    let worker_logging = timely_worker.logger_for("timely").map(Into::into);
231    let apply_demands = COMPUTE_APPLY_COLUMN_DEMANDS.get(&compute_state.worker_config);
232    let subscribe_snapshot_optimization =
233        SUBSCRIBE_SNAPSHOT_OPTIMIZATION.get(&compute_state.worker_config);
234
235    let name = format!("Dataflow: {}", &dataflow.debug_name);
236    let input_name = format!("InputRegion: {}", &dataflow.debug_name);
237    let build_name = format!("BuildRegion: {}", &dataflow.debug_name);
238
239    timely_worker.dataflow_core(&name, worker_logging, Box::new(()), |_, scope| {
240        let scope = scope.with_label();
241
242        // The scope.clone() occurs to allow import in the region.
243        // We build a region here to establish a pattern of a scope inside the dataflow,
244        // so that other similar uses (e.g. with iterative scopes) do not require weird
245        // alternate type signatures.
246        let mut imported_sources = Vec::new();
247        let mut tokens: BTreeMap<_, Rc<dyn Any>> = BTreeMap::new();
248        let output_probe = MzProbeHandle::default();
249
250        scope.clone().region_named(&input_name, |region| {
251            // Import declared sources into the rendering context.
252            for (source_id, import) in dataflow.source_imports.iter() {
253                region.region_named(&format!("Source({:?})", source_id), |inner| {
254                    let mut read_schema = None;
255                    let mut mfp = import.desc.arguments.operators.clone().map(|mut ops| {
256                        // If enabled, we read from Persist with a `RelationDesc` that
257                        // omits uneeded columns.
258                        if apply_demands {
259                            let demands = ops.demand();
260                            let new_desc = import
261                                .desc
262                                .storage_metadata
263                                .relation_desc
264                                .apply_demand(&demands);
265                            let new_arity = demands.len();
266                            let remap: BTreeMap<_, _> = demands
267                                .into_iter()
268                                .enumerate()
269                                .map(|(new, old)| (old, new))
270                                .collect();
271                            ops.permute_fn(|old_idx| remap[&old_idx], new_arity);
272                            read_schema = Some(new_desc);
273                        }
274
275                        mz_expr::MfpPlan::create_from(ops)
276                            .expect("Linear operators should always be valid")
277                    });
278
279                    let snapshot_mode = if import.with_snapshot || !subscribe_snapshot_optimization
280                    {
281                        SnapshotMode::Include
282                    } else {
283                        compute_state.metrics.inc_subscribe_snapshot_optimization();
284                        SnapshotMode::Exclude
285                    };
286                    let suppress_early_progress_as_of = dataflow.as_of.clone();
287
288                    // Note: For correctness, we require that sources only emit times advanced by
289                    // `dataflow.as_of`. `persist_source` is documented to provide this guarantee.
290                    let (mut ok_stream, err_stream, token) =
291                        persist_source::persist_source::<DataflowErrorSer>(
292                            inner,
293                            *source_id,
294                            Arc::clone(&compute_state.persist_clients),
295                            &compute_state.txns_ctx,
296                            import.desc.storage_metadata.clone(),
297                            read_schema,
298                            dataflow.as_of.clone(),
299                            snapshot_mode,
300                            until.clone(),
301                            mfp.as_mut(),
302                            compute_state.dataflow_max_inflight_bytes(),
303                            start_signal.clone().into_send_future(),
304                            ErrorHandler::Halt("compute_import"),
305                        );
306
307                    // If `mfp` is non-identity, we need to apply what remains.
308                    // For the moment, assert that it is either trivial or `None`.
309                    assert!(mfp.map(|x| x.is_identity()).unwrap_or(true));
310
311                    // To avoid a memory spike during arrangement hydration (database-issues#6368), need to
312                    // ensure that the first frontier we report into the dataflow is beyond the
313                    // `as_of`.
314                    if let Some(as_of) = suppress_early_progress_as_of {
315                        ok_stream = suppress_early_progress(ok_stream, as_of);
316                    }
317
318                    if ENABLE_COMPUTE_LOGICAL_BACKPRESSURE.get(&compute_state.worker_config) {
319                        // Apply logical backpressure to the source.
320                        let limit = COMPUTE_LOGICAL_BACKPRESSURE_MAX_RETAINED_CAPABILITIES
321                            .get(&compute_state.worker_config);
322                        let slack = COMPUTE_LOGICAL_BACKPRESSURE_INFLIGHT_SLACK
323                            .get(&compute_state.worker_config)
324                            .as_millis()
325                            .try_into()
326                            .expect("must fit");
327
328                        let stream = ok_stream.limit_progress(
329                            output_probe.clone(),
330                            slack,
331                            limit,
332                            import.upper.clone(),
333                            name.clone(),
334                        );
335                        ok_stream = stream;
336                    }
337
338                    // Attach a probe reporting the input frontier.
339                    let input_probe =
340                        compute_state.input_probe_for(*source_id, dataflow.export_ids());
341                    ok_stream = ok_stream.probe_with(&input_probe);
342
343                    let (oks, errs) = (
344                        ok_stream
345                            .as_collection()
346                            .leave_region(region)
347                            .leave_region(scope),
348                        err_stream
349                            .as_collection()
350                            .leave_region(region)
351                            .leave_region(scope),
352                    );
353
354                    imported_sources.push((mz_expr::Id::Global(*source_id), (oks, errs)));
355
356                    // Associate returned tokens with the source identifier.
357                    tokens.insert(*source_id, Rc::new(token));
358                });
359            }
360        });
361
362        // If there exists a recursive expression, we'll need to use a non-region scope,
363        // in order to support additional timestamp coordinates for iteration.
364        if recursive {
365            scope.clone().iterative::<PointStamp<u64>, _, _>(|region| {
366                let mut context = Context::for_dataflow_in(
367                    &dataflow,
368                    region.clone(),
369                    compute_state,
370                    until,
371                    dataflow_expiration,
372                );
373
374                for (id, (oks, errs)) in imported_sources.into_iter() {
375                    let bundle = crate::render::CollectionBundle::from_collections(
376                        oks.enter(region),
377                        errs.enter(region),
378                    );
379                    // Associate collection bundle with the source identifier.
380                    context.insert_id(id, bundle);
381                }
382
383                // Import declared indexes into the rendering context.
384                for (idx_id, idx) in &dataflow.index_imports {
385                    let input_probe = compute_state.input_probe_for(*idx_id, dataflow.export_ids());
386                    let snapshot_mode = if idx.with_snapshot || !subscribe_snapshot_optimization {
387                        SnapshotMode::Include
388                    } else {
389                        compute_state.metrics.inc_subscribe_snapshot_optimization();
390                        SnapshotMode::Exclude
391                    };
392                    context.import_index(
393                        scope,
394                        compute_state,
395                        &mut tokens,
396                        input_probe,
397                        *idx_id,
398                        &idx.desc.as_lir(),
399                        &idx.typ,
400                        snapshot_mode,
401                        start_signal.clone(),
402                    );
403                }
404
405                // Build declared objects.
406                for object in dataflow.objects_to_build {
407                    let bundle = context.scope.clone().region_named(
408                        &format!("BuildingObject({:?})", object.id),
409                        |region| {
410                            let depends = object.plan.depends();
411                            let in_let = object.plan.is_recursive();
412                            context
413                                .enter_region(region, Some(&depends))
414                                .render_recursive_plan(
415                                    object.id,
416                                    0,
417                                    object.plan,
418                                    // recursive plans _must_ have bodies in a let
419                                    BindingInfo::Body { in_let },
420                                )
421                                .leave_region(context.scope)
422                        },
423                    );
424                    let global_id = object.id;
425
426                    context.log_dataflow_global_id(
427                        *bundle
428                            .scope()
429                            .addr()
430                            .first()
431                            .expect("Dataflow root id must exist"),
432                        global_id,
433                    );
434                    context.insert_id(Id::Global(object.id), bundle);
435                }
436
437                // Export declared indexes.
438                for (idx_id, dependencies, idx) in indexes {
439                    context.export_index_iterative(
440                        scope,
441                        compute_state,
442                        &tokens,
443                        dependencies,
444                        idx_id,
445                        &idx,
446                        &output_probe,
447                    );
448                }
449
450                // Export declared sinks.
451                for (sink_id, dependencies, sink) in sinks {
452                    context.export_sink(
453                        compute_state,
454                        &tokens,
455                        dependencies,
456                        sink_id,
457                        &sink,
458                        start_signal.clone(),
459                        &output_probe,
460                        scope,
461                    );
462                }
463            });
464        } else {
465            scope.clone().region_named(&build_name, |region| {
466                let mut context = Context::for_dataflow_in(
467                    &dataflow,
468                    region.clone(),
469                    compute_state,
470                    until,
471                    dataflow_expiration,
472                );
473
474                for (id, (oks, errs)) in imported_sources.into_iter() {
475                    let bundle = crate::render::CollectionBundle::from_collections(
476                        oks.enter_region(region),
477                        errs.enter_region(region),
478                    );
479                    // Associate collection bundle with the source identifier.
480                    context.insert_id(id, bundle);
481                }
482
483                // Import declared indexes into the rendering context.
484                for (idx_id, idx) in &dataflow.index_imports {
485                    let input_probe = compute_state.input_probe_for(*idx_id, dataflow.export_ids());
486                    let snapshot_mode = if idx.with_snapshot || !subscribe_snapshot_optimization {
487                        SnapshotMode::Include
488                    } else {
489                        compute_state.metrics.inc_subscribe_snapshot_optimization();
490                        SnapshotMode::Exclude
491                    };
492                    context.import_index(
493                        scope,
494                        compute_state,
495                        &mut tokens,
496                        input_probe,
497                        *idx_id,
498                        &idx.desc.as_lir(),
499                        &idx.typ,
500                        snapshot_mode,
501                        start_signal.clone(),
502                    );
503                }
504
505                // Build declared objects.
506                for object in dataflow.objects_to_build {
507                    let bundle = context.scope.clone().region_named(
508                        &format!("BuildingObject({:?})", object.id),
509                        |region| {
510                            let depends = object.plan.depends();
511                            context
512                                .enter_region(region, Some(&depends))
513                                .render_plan(object.id, object.plan)
514                                .leave_region(context.scope)
515                        },
516                    );
517                    let global_id = object.id;
518                    context.log_dataflow_global_id(
519                        *bundle
520                            .scope()
521                            .addr()
522                            .first()
523                            .expect("Dataflow root id must exist"),
524                        global_id,
525                    );
526                    context.insert_id(Id::Global(object.id), bundle);
527                }
528
529                // Export declared indexes.
530                for (idx_id, dependencies, idx) in indexes {
531                    context.export_index(
532                        compute_state,
533                        &tokens,
534                        dependencies,
535                        idx_id,
536                        &idx,
537                        &output_probe,
538                    );
539                }
540
541                // Export declared sinks.
542                for (sink_id, dependencies, sink) in sinks {
543                    context.export_sink(
544                        compute_state,
545                        &tokens,
546                        dependencies,
547                        sink_id,
548                        &sink,
549                        start_signal.clone(),
550                        &output_probe,
551                        scope,
552                    );
553                }
554            });
555        }
556    });
557}
558
559// This implementation block allows child timestamps to vary from parent timestamps,
560// but requires the parent timestamp to be `repr::Timestamp`.
561impl<'g, T> Context<'g, T>
562where
563    T: Refines<mz_repr::Timestamp> + RenderTimestamp,
564{
565    /// Import the collection from the arrangement, discarding batches from the snapshot.
566    /// (This does not guarantee that no records from the snapshot are included; the assumption is
567    /// that we'll filter those out later if necessary.)
568    fn import_filtered_index_collection<
569        'outer,
570        Tr: TraceReader<Time = mz_repr::Timestamp, Batch: Navigable> + Clone,
571        V: Data,
572    >(
573        &self,
574        arranged: Arranged<'outer, Tr>,
575        start_signal: StartSignal,
576        mut logic: impl FnMut(BatchKey<'_, Tr>, BatchVal<'_, Tr>) -> V + 'static,
577    ) -> VecCollection<'g, T, V, BatchDiff<Tr>>
578    where
579        // This is implied by the fact that the outer timestamp = mz_repr::Timestamp, but it's essential
580        // for our batch-level filtering to be safe, so we document it here regardless.
581        mz_repr::Timestamp: TotalOrder,
582        BatchCursor<Tr>: Cursor<Time = mz_repr::Timestamp>,
583    {
584        let oks = arranged.stream.with_start_signal(start_signal).filter({
585            let as_of = self.as_of_frontier.clone();
586            move |b| !<Antichain<mz_repr::Timestamp> as PartialOrder>::less_equal(b.upper(), &as_of)
587        });
588        Arranged::<'outer, Tr>::flat_map_batches(oks, move |a, b| [logic(a, b)]).enter(self.scope)
589    }
590
591    pub(crate) fn import_index<'outer>(
592        &mut self,
593        outer: Scope<'outer, mz_repr::Timestamp>,
594        compute_state: &mut ComputeState,
595        tokens: &mut BTreeMap<GlobalId, Rc<dyn std::any::Any>>,
596        input_probe: probe::Handle<mz_repr::Timestamp>,
597        idx_id: GlobalId,
598        idx: &IndexDesc<LirScalarExpr>,
599        typ: &ReprRelationType,
600        snapshot_mode: SnapshotMode,
601        start_signal: StartSignal,
602    ) {
603        if let Some(traces) = compute_state.traces.get_mut(&idx_id) {
604            assert!(
605                PartialOrder::less_equal(&traces.compaction_frontier(), &self.as_of_frontier),
606                "Index {idx_id} has been allowed to compact beyond the dataflow as_of"
607            );
608
609            let token = traces.to_drop().clone();
610
611            let (mut oks, ok_button) = traces.oks_mut().import_frontier_core(
612                outer,
613                &format!("Index({}, {:?})", idx.on_id, idx.key),
614                self.as_of_frontier.clone(),
615                self.until.clone(),
616            );
617
618            oks.stream = oks.stream.probe_with(&input_probe);
619
620            let (err_arranged, err_button) = traces.errs_mut().import_frontier_core(
621                outer,
622                &format!("ErrIndex({}, {:?})", idx.on_id, idx.key),
623                self.as_of_frontier.clone(),
624                self.until.clone(),
625            );
626
627            let bundle = match snapshot_mode {
628                SnapshotMode::Include => {
629                    let ok_arranged = oks
630                        .enter(self.scope)
631                        .with_start_signal(start_signal.clone());
632                    let err_arranged = err_arranged
633                        .enter(self.scope)
634                        .with_start_signal(start_signal);
635                    CollectionBundle::from_expressions(
636                        idx.key.clone(),
637                        ArrangementFlavor::Trace(idx_id, ok_arranged, err_arranged),
638                    )
639                }
640                SnapshotMode::Exclude => {
641                    // When we import an index without a snapshot, we have two balancing considerations:
642                    // - It's easy to filter out irrelevant batches from the stream, but hard to filter them out from an arrangement.
643                    //   (The `TraceFrontier` wrapper allows us to set an "until" frontier, but not a lower.)
644                    // - We do not actually need to reference the arrangement in this dataflow, since all operators that use the arrangement
645                    //   (joins, reduces, etc.) also require the snapshot data.
646                    // So: when the snapshot is excluded, we import only the (filtered) collection itself and ignore the arrangement.
647                    let oks = {
648                        let mut datums = DatumVec::new();
649                        let (permutation, _thinning) =
650                            permutation_for_arrangement(&idx.key, typ.arity());
651                        self.import_filtered_index_collection(
652                            oks,
653                            start_signal.clone(),
654                            move |k: DatumSeq, v: DatumSeq| {
655                                let temp_storage = RowArena::new();
656                                let mut datums_borrow = datums.borrow();
657                                k.extend_datums(&temp_storage, &mut datums_borrow, None);
658                                v.extend_datums(&temp_storage, &mut datums_borrow, None);
659                                SharedRow::pack(permutation.iter().map(|i| datums_borrow[*i]))
660                            },
661                        )
662                    };
663                    let errs = self.import_filtered_index_collection(
664                        err_arranged,
665                        start_signal,
666                        |e, _| e.clone(),
667                    );
668                    CollectionBundle::from_collections(oks, errs)
669                }
670            };
671            self.update_id(Id::Global(idx.on_id), bundle);
672            tokens.insert(
673                idx_id,
674                Rc::new((PressOnDrop(ok_button), PressOnDrop(err_button), token)),
675            );
676        } else {
677            panic!(
678                "import of index {} failed while building dataflow {}",
679                idx_id, self.dataflow_id
680            );
681        }
682    }
683}
684
685// This implementation block requires the scopes have the same timestamp as the trace manager.
686// That makes some sense, because we are hoping to deposit an arrangement in the trace manager.
687impl<'g> Context<'g, mz_repr::Timestamp> {
688    pub(crate) fn export_index(
689        &self,
690        compute_state: &mut ComputeState,
691        tokens: &BTreeMap<GlobalId, Rc<dyn std::any::Any>>,
692        dependency_ids: BTreeSet<GlobalId>,
693        idx_id: GlobalId,
694        idx: &IndexDesc<LirScalarExpr>,
695        output_probe: &MzProbeHandle<mz_repr::Timestamp>,
696    ) {
697        // put together tokens that belong to the export
698        let mut needed_tokens = Vec::new();
699        for dep_id in dependency_ids {
700            if let Some(token) = tokens.get(&dep_id) {
701                needed_tokens.push(Rc::clone(token));
702            }
703        }
704        let bundle = self.lookup_id(Id::Global(idx_id)).unwrap_or_else(|| {
705            panic!(
706                "Arrangement alarmingly absent! id: {:?}",
707                Id::Global(idx_id)
708            )
709        });
710
711        let key = &idx.key;
712        match bundle.arrangement(key) {
713            Some(ArrangementFlavor::Local(mut oks, mut errs)) => {
714                // Ensure that the frontier does not advance past the expiration time, if set.
715                // Otherwise, we might write down incorrect data.
716                if let Some(&expiration) = self.dataflow_expiration.as_option() {
717                    oks.stream = oks.stream.expire_stream_at(
718                        &format!("{}_export_index_oks", self.debug_name),
719                        expiration,
720                    );
721                    errs.stream = errs.stream.expire_stream_at(
722                        &format!("{}_export_index_errs", self.debug_name),
723                        expiration,
724                    );
725                }
726
727                oks.stream = oks.stream.probe_notify_with(vec![output_probe.clone()]);
728
729                // Attach logging of dataflow errors.
730                if let Some(logger) = compute_state.compute_logger.clone() {
731                    errs.stream = errs.stream.log_dataflow_errors(logger, idx_id);
732                }
733
734                compute_state.traces.set(
735                    idx_id,
736                    TraceBundle::new(oks.trace, errs.trace).with_drop(needed_tokens),
737                );
738            }
739            Some(ArrangementFlavor::Trace(gid, _, _)) => {
740                // Duplicate of existing arrangement with id `gid`, so
741                // just create another handle to that arrangement.
742                let trace = compute_state.traces.get(&gid).unwrap().clone();
743                compute_state.traces.set(idx_id, trace);
744            }
745            None => {
746                println!("collection available: {:?}", bundle.collection.is_none());
747                println!(
748                    "keys available: {:?}",
749                    bundle.arranged.keys().collect::<Vec<_>>()
750                );
751                panic!(
752                    "Arrangement alarmingly absent! id: {:?}, keys: {:?}",
753                    Id::Global(idx_id),
754                    &key
755                );
756            }
757        };
758    }
759}
760
761// This implementation block requires the scopes have the same timestamp as the trace manager.
762// That makes some sense, because we are hoping to deposit an arrangement in the trace manager.
763impl<'g, T> Context<'g, T>
764where
765    T: RenderTimestamp,
766{
767    pub(crate) fn export_index_iterative<'outer>(
768        &self,
769        outer: Scope<'outer, mz_repr::Timestamp>,
770        compute_state: &mut ComputeState,
771        tokens: &BTreeMap<GlobalId, Rc<dyn std::any::Any>>,
772        dependency_ids: BTreeSet<GlobalId>,
773        idx_id: GlobalId,
774        idx: &IndexDesc<LirScalarExpr>,
775        output_probe: &MzProbeHandle<mz_repr::Timestamp>,
776    ) {
777        // put together tokens that belong to the export
778        let mut needed_tokens = Vec::new();
779        for dep_id in dependency_ids {
780            if let Some(token) = tokens.get(&dep_id) {
781                needed_tokens.push(Rc::clone(token));
782            }
783        }
784        let bundle = self.lookup_id(Id::Global(idx_id)).unwrap_or_else(|| {
785            panic!(
786                "Arrangement alarmingly absent! id: {:?}",
787                Id::Global(idx_id)
788            )
789        });
790
791        let key = &idx.key;
792        match bundle.arrangement(key) {
793            Some(ArrangementFlavor::Local(oks, errs)) => {
794                // TODO: The following as_collection/leave/arrange sequence could be optimized.
795                //   * Combine as_collection and leave into a single function.
796                //   * Use columnar to extract columns from the batches to implement leave.
797                let mut oks = oks
798                    .as_collection(|k, v| (k.to_row(), v.to_row()))
799                    .leave(outer)
800                    .mz_arrange::<
801                        ColumnationChunker<_>,
802                        RowRowBatcher<_, _>,
803                        RowRowBuilder<_, _>,
804                        _,
805                    >(
806                        "Arrange export iterative",
807                    );
808
809                let mut errs = errs
810                    .as_collection(|k, v| (k.clone(), v.clone()))
811                    .leave(outer)
812                    .mz_arrange::<ColumnationChunker<_>, ErrBatcher<_, _>, ErrBuilder<_, _>, _>(
813                        "Arrange export iterative err",
814                    );
815
816                // Ensure that the frontier does not advance past the expiration time, if set.
817                // Otherwise, we might write down incorrect data.
818                if let Some(&expiration) = self.dataflow_expiration.as_option() {
819                    oks.stream = oks.stream.expire_stream_at(
820                        &format!("{}_export_index_iterative_oks", self.debug_name),
821                        expiration,
822                    );
823                    errs.stream = errs.stream.expire_stream_at(
824                        &format!("{}_export_index_iterative_err", self.debug_name),
825                        expiration,
826                    );
827                }
828
829                oks.stream = oks.stream.probe_notify_with(vec![output_probe.clone()]);
830
831                // Attach logging of dataflow errors.
832                if let Some(logger) = compute_state.compute_logger.clone() {
833                    errs.stream = errs.stream.log_dataflow_errors(logger, idx_id);
834                }
835
836                compute_state.traces.set(
837                    idx_id,
838                    TraceBundle::new(oks.trace, errs.trace).with_drop(needed_tokens),
839                );
840            }
841            Some(ArrangementFlavor::Trace(gid, _, _)) => {
842                // Duplicate of existing arrangement with id `gid`, so
843                // just create another handle to that arrangement.
844                let trace = compute_state.traces.get(&gid).unwrap().clone();
845                compute_state.traces.set(idx_id, trace);
846            }
847            None => {
848                println!("collection available: {:?}", bundle.collection.is_none());
849                println!(
850                    "keys available: {:?}",
851                    bundle.arranged.keys().collect::<Vec<_>>()
852                );
853                panic!(
854                    "Arrangement alarmingly absent! id: {:?}, keys: {:?}",
855                    Id::Global(idx_id),
856                    &key,
857                );
858            }
859        };
860    }
861}
862
863/// Information about bindings, tracked in `render_recursive_plan` and
864/// `render_plan`, to be passed to `render_letfree_plan`.
865///
866/// `render_letfree_plan` uses these to produce nice output (e.g., `With ...
867/// Returning ...`) for local bindings in the `mz_lir_mapping` output.
868enum BindingInfo {
869    Body { in_let: bool },
870    Let { id: LocalId, last: bool },
871    LetRec { id: LocalId, last: bool },
872}
873
874impl<'scope> Context<'scope, Product<mz_repr::Timestamp, PointStamp<u64>>> {
875    /// Renders a plan to a differential dataflow, producing the collection of results.
876    ///
877    /// This method allows for `plan` to contain [`RecBind`]s, and is planned
878    /// in the context of `level` pre-existing iteration coordinates.
879    ///
880    /// This method recursively descends [`RecBind`] values, establishing nested scopes for each
881    /// and establishing the appropriate recursive dependencies among the bound variables.
882    /// Once all [`RecBind`]s have been rendered it calls in to `render_plan` which will error if
883    /// further [`RecBind`]s are found.
884    ///
885    /// The method requires that all variables conclude with a physical representation that
886    /// contains a collection (i.e. a non-arrangement), and it will panic otherwise.
887    fn render_recursive_plan(
888        &mut self,
889        object_id: GlobalId,
890        level: usize,
891        plan: RenderPlan,
892        binding: BindingInfo,
893    ) -> CollectionBundle<'scope, Product<mz_repr::Timestamp, PointStamp<u64>>> {
894        for BindStage { lets, recs } in plan.binds {
895            // Render the let bindings in order.
896            let mut let_iter = lets.into_iter().peekable();
897            while let Some(LetBind { id, value }) = let_iter.next() {
898                let bundle =
899                    self.scope
900                        .clone()
901                        .region_named(&format!("Binding({:?})", id), |region| {
902                            let depends = value.depends();
903                            let last = let_iter.peek().is_none();
904                            let binding = BindingInfo::Let { id, last };
905                            self.enter_region(region, Some(&depends))
906                                .render_letfree_plan(object_id, value, binding)
907                                .leave_region(self.scope)
908                        });
909                self.insert_id(Id::Local(id), bundle);
910            }
911
912            let rec_ids: Vec<_> = recs.iter().map(|r| r.id).collect();
913
914            // Define variables for rec bindings.
915            // It is important that we only use the `Variable` until the object is bound.
916            // At that point, all subsequent uses should have access to the object itself.
917            let mut variables = BTreeMap::new();
918            for id in rec_ids.iter() {
919                use differential_dataflow::dynamic::feedback_summary;
920                let inner = feedback_summary::<u64>(level + 1, 1);
921                let (oks_v, oks_collection) =
922                    Variable::new(self.scope, Product::new(Default::default(), inner.clone()));
923                let (err_v, err_collection) =
924                    Variable::new(self.scope, Product::new(Default::default(), inner));
925
926                self.insert_id(
927                    Id::Local(*id),
928                    CollectionBundle::from_collections(oks_collection, err_collection),
929                );
930                variables.insert(Id::Local(*id), (oks_v, err_v));
931            }
932            // Now render each of the rec bindings.
933            let mut rec_iter = recs.into_iter().peekable();
934            while let Some(RecBind { id, value, limit }) = rec_iter.next() {
935                let last = rec_iter.peek().is_none();
936                let binding = BindingInfo::LetRec { id, last };
937                let bundle = self.render_recursive_plan(object_id, level + 1, value, binding);
938                // We need to ensure that the raw collection exists, but do not have enough information
939                // here to cause that to happen.
940                let (oks, mut err) = bundle.collection.clone().unwrap();
941                let oks = oks.into_vec();
942                self.insert_id(Id::Local(id), bundle);
943                let (oks_v, err_v) = variables.remove(&Id::Local(id)).unwrap();
944
945                // Set oks variable to `oks` but consolidated to ensure iteration ceases at fixed point.
946                let mut oks = CollectionExt::consolidate_named::<KeyBatcher<_, _, _>>(
947                    oks,
948                    "LetRecConsolidation",
949                );
950
951                if let Some(limit) = limit {
952                    // We swallow the results of the `max_iter`th iteration, because
953                    // these results would go into the `max_iter + 1`th iteration.
954                    let (in_limit, over_limit) =
955                        oks.inner.branch_when(move |Product { inner: ps, .. }| {
956                            // The iteration number, or if missing a zero (as trailing zeros are truncated).
957                            let iteration_index = *ps.get(level).unwrap_or(&0);
958                            // The pointstamp starts counting from 0, so we need to add 1.
959                            iteration_index + 1 >= limit.max_iters.into()
960                        });
961                    oks = VecCollection::new(in_limit);
962                    if !limit.return_at_limit {
963                        err = err.concat(VecCollection::new(over_limit).map(move |_data| {
964                            DataflowErrorSer::from(EvalError::LetRecLimitExceeded(
965                                format!("{}", limit.max_iters.get()).into(),
966                            ))
967                        }));
968                    }
969                }
970
971                // Set err variable to the distinct elements of `err`.
972                // Distinctness is important, as we otherwise might add the same error each iteration,
973                // say if the limit of `oks` has an error. This would result in non-terminating rather
974                // than a clean report of the error. The trade-off is that we lose information about
975                // multiplicities of errors, but .. this seems to be the better call.
976                let err: KeyCollection<_, _, _> = err.into();
977                let errs = err
978                    .mz_arrange::<
979                        ColumnationChunker<_>,
980                        ErrBatcher<_, _>,
981                        ErrBuilder<_, _>,
982                        ErrSpine<_, _>,
983                    >("Arrange recursive err")
984                    .mz_reduce_abelian::<_, ErrBuilder<_, _>, ErrSpine<_, _>, _>(
985                        "Distinct recursive err",
986                        move |_k, _s, t| t.push(((), Diff::ONE)),
987                    )
988                    .as_collection(|k, _| k.clone());
989
990                oks_v.set(oks);
991                err_v.set(errs);
992            }
993            // Now extract each of the rec bindings into the outer scope.
994            for id in rec_ids.into_iter() {
995                let bundle = self.remove_id(Id::Local(id)).unwrap();
996                let (oks, err) = bundle.collection.unwrap();
997                let oks = oks.into_vec();
998                self.insert_id(
999                    Id::Local(id),
1000                    CollectionBundle::from_collections(
1001                        oks.leave_dynamic(level + 1),
1002                        err.leave_dynamic(level + 1),
1003                    ),
1004                );
1005            }
1006        }
1007
1008        self.render_letfree_plan(object_id, plan.body, binding)
1009    }
1010}
1011
1012impl<'scope, T: RenderTimestamp + MaybeBucketByTime> Context<'scope, T> {
1013    /// Renders a non-recursive plan to a differential dataflow, producing the collection of
1014    /// results.
1015    ///
1016    /// The return type reflects the uncertainty about the data representation, perhaps
1017    /// as a stream of data, perhaps as an arrangement, perhaps as a stream of batches.
1018    ///
1019    /// # Panics
1020    ///
1021    /// Panics if the given plan contains any [`RecBind`]s. Recursive plans must be rendered using
1022    /// `render_recursive_plan` instead.
1023    fn render_plan(
1024        &mut self,
1025        object_id: GlobalId,
1026        plan: RenderPlan,
1027    ) -> CollectionBundle<'scope, T> {
1028        let mut in_let = false;
1029        for BindStage { lets, recs } in plan.binds {
1030            assert!(recs.is_empty());
1031
1032            let mut let_iter = lets.into_iter().peekable();
1033            while let Some(LetBind { id, value }) = let_iter.next() {
1034                // if we encounter a single let, the body is in a let
1035                in_let = true;
1036                let bundle =
1037                    self.scope
1038                        .clone()
1039                        .region_named(&format!("Binding({:?})", id), |region| {
1040                            let depends = value.depends();
1041                            let last = let_iter.peek().is_none();
1042                            let binding = BindingInfo::Let { id, last };
1043                            self.enter_region(region, Some(&depends))
1044                                .render_letfree_plan(object_id, value, binding)
1045                                .leave_region(self.scope)
1046                        });
1047                self.insert_id(Id::Local(id), bundle);
1048            }
1049        }
1050
1051        self.scope.clone().region_named("Main Body", |region| {
1052            let depends = plan.body.depends();
1053            self.enter_region(region, Some(&depends))
1054                .render_letfree_plan(object_id, plan.body, BindingInfo::Body { in_let })
1055                .leave_region(self.scope)
1056        })
1057    }
1058
1059    /// Renders a let-free plan to a differential dataflow, producing the collection of results.
1060    fn render_letfree_plan(
1061        &self,
1062        object_id: GlobalId,
1063        plan: LetFreePlan,
1064        binding: BindingInfo,
1065    ) -> CollectionBundle<'scope, T> {
1066        let (mut nodes, root_id, topological_order) = plan.destruct();
1067
1068        // Rendered collections by their `LirId`.
1069        let mut collections = BTreeMap::new();
1070
1071        // Mappings to send along.
1072        // To save overhead, we'll only compute mappings when we need to,
1073        // which means things get gated behind options. Unfortunately, that means we
1074        // have several `Option<...>` types that are _all_ `Some` or `None` together,
1075        // but there's no convenient way to express the invariant.
1076        let should_compute_lir_metadata = self.compute_logger.is_some();
1077        let mut lir_mapping_metadata = if should_compute_lir_metadata {
1078            Some(Vec::with_capacity(nodes.len()))
1079        } else {
1080            None
1081        };
1082
1083        let mut topo_iter = topological_order.into_iter().peekable();
1084        while let Some(lir_id) = topo_iter.next() {
1085            let node = nodes.remove(&lir_id).unwrap();
1086
1087            // TODO(mgree) need ExprHumanizer in DataflowDescription to get nice column names
1088            // ActiveComputeState can't have a catalog reference, so we'll need to capture the names
1089            // in some other structure and have that structure impl ExprHumanizer
1090            let metadata = if should_compute_lir_metadata {
1091                let operator = node.expr.humanize(&DummyHumanizer);
1092
1093                // mark the last operator in topo order with any binding decoration
1094                let operator = if topo_iter.peek().is_none() {
1095                    match &binding {
1096                        BindingInfo::Body { in_let: true } => format!("Returning {operator}"),
1097                        BindingInfo::Body { in_let: false } => operator,
1098                        BindingInfo::Let { id, last: true } => {
1099                            format!("With {id} = {operator}")
1100                        }
1101                        BindingInfo::Let { id, last: false } => {
1102                            format!("{id} = {operator}")
1103                        }
1104                        BindingInfo::LetRec { id, last: true } => {
1105                            format!("With Recursive {id} = {operator}")
1106                        }
1107                        BindingInfo::LetRec { id, last: false } => {
1108                            format!("{id} = {operator}")
1109                        }
1110                    }
1111                } else {
1112                    operator
1113                };
1114
1115                let operator_id_start = self.scope.worker().peek_identifier();
1116                Some((operator, operator_id_start))
1117            } else {
1118                None
1119            };
1120
1121            let mut bundle = self.render_plan_expr(node.expr, &collections);
1122
1123            if let Some((operator, operator_id_start)) = metadata {
1124                let operator_id_end = self.scope.worker().peek_identifier();
1125                let operator_span = (operator_id_start, operator_id_end);
1126
1127                if let Some(lir_mapping_metadata) = &mut lir_mapping_metadata {
1128                    lir_mapping_metadata.push((
1129                        lir_id,
1130                        LirMetadata::new(operator, node.parent, node.nesting, operator_span),
1131                    ))
1132                }
1133            }
1134
1135            self.log_operator_hydration(&mut bundle, lir_id);
1136
1137            collections.insert(lir_id, bundle);
1138        }
1139
1140        if let Some(lir_mapping_metadata) = lir_mapping_metadata {
1141            self.log_lir_mapping(object_id, lir_mapping_metadata);
1142        }
1143
1144        collections
1145            .remove(&root_id)
1146            .expect("LetFreePlan invariant (1)")
1147    }
1148
1149    /// Renders a [`render_plan::Expr`], producing the collection of results.
1150    ///
1151    /// # Panics
1152    ///
1153    /// Panics if any of the expr's inputs is not found in `collections`.
1154    /// Callers must ensure that input nodes have been rendered previously.
1155    fn render_plan_expr(
1156        &self,
1157        expr: render_plan::Expr,
1158        collections: &BTreeMap<LirId, CollectionBundle<'scope, T>>,
1159    ) -> CollectionBundle<'scope, T> {
1160        use render_plan::Expr::*;
1161
1162        let expect_input = |id| {
1163            collections
1164                .get(&id)
1165                .cloned()
1166                .unwrap_or_else(|| panic!("missing input collection: {id}"))
1167        };
1168
1169        match expr {
1170            Constant { rows } => {
1171                // Produce both rows and errs to avoid conditional dataflow construction.
1172                let (rows, errs) = match rows {
1173                    Ok(rows) => (rows, Vec::new()),
1174                    Err(e) => (Vec::new(), vec![e]),
1175                };
1176
1177                // We should advance times in constant collections to start from `as_of`.
1178                let as_of_frontier = self.as_of_frontier.clone();
1179                let until = self.until.clone();
1180                let ok_collection = rows
1181                    .into_iter()
1182                    .filter_map(move |(row, mut time, diff)| {
1183                        time.advance_by(as_of_frontier.borrow());
1184                        if !until.less_equal(&time) {
1185                            Some((
1186                                row,
1187                                <T as Refines<mz_repr::Timestamp>>::to_inner(time),
1188                                diff,
1189                            ))
1190                        } else {
1191                            None
1192                        }
1193                    })
1194                    .to_stream(self.scope)
1195                    .as_collection();
1196
1197                let mut error_time: mz_repr::Timestamp = Timestamp::minimum();
1198                error_time.advance_by(self.as_of_frontier.borrow());
1199                let err_collection = errs
1200                    .into_iter()
1201                    .map(move |e| {
1202                        (
1203                            DataflowErrorSer::from(e),
1204                            <T as Refines<mz_repr::Timestamp>>::to_inner(error_time),
1205                            Diff::ONE,
1206                        )
1207                    })
1208                    .to_stream(self.scope)
1209                    .as_collection();
1210
1211                CollectionBundle::from_collections(ok_collection, err_collection)
1212            }
1213            Get { id, keys, plan } => {
1214                // Recover the collection from `self` and then apply `mfp` to it.
1215                // If `mfp` happens to be trivial, we can just return the collection.
1216                let mut collection = self
1217                    .lookup_id(id)
1218                    .unwrap_or_else(|| panic!("Get({:?}) not found at render time", id));
1219                match plan {
1220                    mz_compute_types::plan::GetPlan::PassArrangements => {
1221                        // Assert that each of `keys` are present in `collection`.
1222                        assert!(
1223                            keys.arranged
1224                                .iter()
1225                                .all(|(key, _, _)| collection.arranged.contains_key(key))
1226                        );
1227                        assert!(keys.raw <= collection.collection.is_some());
1228                        // Retain only those keys we want to import.
1229                        collection.arranged.retain(|key, _value| {
1230                            keys.arranged.iter().any(|(key2, _, _)| key2 == key)
1231                        });
1232                        collection
1233                    }
1234                    mz_compute_types::plan::GetPlan::Arrangement(key, row, mfp) => {
1235                        let (oks, errs) = collection.as_collection_core(
1236                            mfp,
1237                            Some((key, row)),
1238                            self.until.clone(),
1239                            &self.config_set,
1240                        );
1241                        CollectionBundle::from_collections(oks, errs)
1242                    }
1243                    mz_compute_types::plan::GetPlan::Collection(mfp) => {
1244                        let (oks, errs) = collection.as_collection_core(
1245                            mfp,
1246                            None,
1247                            self.until.clone(),
1248                            &self.config_set,
1249                        );
1250                        CollectionBundle::from_collections(oks, errs)
1251                    }
1252                }
1253            }
1254            Mfp {
1255                input,
1256                mfp,
1257                input_key_val,
1258            } => {
1259                let input = expect_input(input);
1260                // If `mfp` is non-trivial, we should apply it and produce a collection.
1261                if mfp.is_identity() {
1262                    input
1263                } else {
1264                    let (oks, errs) = input.as_collection_core(
1265                        mfp,
1266                        input_key_val,
1267                        self.until.clone(),
1268                        &self.config_set,
1269                    );
1270                    CollectionBundle::from_collections(oks, errs)
1271                }
1272            }
1273            FlatMap {
1274                input_key,
1275                input,
1276                exprs,
1277                func,
1278                mfp_after: mfp,
1279            } => {
1280                let input = expect_input(input);
1281                self.render_flat_map(input_key, input, exprs, func, mfp)
1282            }
1283            Join { inputs, plan } => {
1284                let inputs = inputs.into_iter().map(expect_input).collect();
1285                match plan {
1286                    mz_compute_types::plan::join::JoinPlan::Linear(linear_plan) => {
1287                        self.render_join(inputs, linear_plan)
1288                    }
1289                    mz_compute_types::plan::join::JoinPlan::Delta(delta_plan) => {
1290                        self.render_delta_join(inputs, delta_plan)
1291                    }
1292                }
1293            }
1294            Reduce {
1295                input_key,
1296                input,
1297                key_val_plan,
1298                plan,
1299                mfp_after,
1300                temporal_bucketing_strategy,
1301            } => {
1302                let input = expect_input(input);
1303                let mfp_option = (!mfp_after.is_identity()).then_some(mfp_after);
1304                self.render_reduce(
1305                    input_key,
1306                    input,
1307                    key_val_plan,
1308                    plan,
1309                    mfp_option,
1310                    temporal_bucketing_strategy,
1311                )
1312            }
1313            TopK {
1314                input,
1315                top_k_plan,
1316                temporal_bucketing_strategy,
1317            } => {
1318                let input = expect_input(input);
1319                self.render_topk(input, top_k_plan, temporal_bucketing_strategy)
1320            }
1321            Negate { input } => {
1322                let input = expect_input(input);
1323                let (oks, errs) = input
1324                    .collection
1325                    .clone()
1326                    .expect("Negate input must be an unarranged collection");
1327                CollectionBundle::from_edge(oks.negate(), errs)
1328            }
1329            Threshold {
1330                input,
1331                threshold_plan,
1332            } => {
1333                let input = expect_input(input);
1334                self.render_threshold(input, threshold_plan)
1335            }
1336            Union {
1337                inputs,
1338                consolidate_output,
1339                temporal_bucketing_strategies,
1340            } => {
1341                let mut oks = Vec::new();
1342                let mut errs = Vec::new();
1343                for (input, strategy) in inputs.into_iter().zip_eq(temporal_bucketing_strategies) {
1344                    let (os, es) = expect_input(input)
1345                        .collection
1346                        .clone()
1347                        .expect("Union input must be an unarranged collection");
1348                    // Apply per-input temporal bucketing. No-op for `Direct`.
1349                    // Only consolidating Unions carry non-`Direct` strategies;
1350                    // see the `Union` arm of `lower_mir_expr_stack_safe`.
1351                    let os = if matches!(strategy, ArrangementStrategy::TemporalBucketing)
1352                        && ENABLE_COMPUTE_TEMPORAL_BUCKETING.get(&self.config_set)
1353                    {
1354                        let summary: mz_repr::Timestamp = TEMPORAL_BUCKETING_SUMMARY
1355                            .get(&self.config_set)
1356                            .try_into()
1357                            .expect("must fit");
1358                        let os = os.into_vec();
1359                        CollectionEdge::Vec(T::maybe_apply_temporal_bucketing(
1360                            os.inner,
1361                            self.as_of_frontier.clone(),
1362                            summary,
1363                        ))
1364                    } else {
1365                        os
1366                    };
1367                    oks.push(os);
1368                    errs.push(es);
1369                }
1370                let oks = CollectionEdge::concat_many(self.scope, oks);
1371                let oks = if consolidate_output {
1372                    oks.consolidate_named("UnionConsolidation")
1373                } else {
1374                    oks
1375                };
1376                let errs = differential_dataflow::collection::concatenate(self.scope, errs);
1377                CollectionBundle::from_edge(oks, errs)
1378            }
1379            ArrangeBy {
1380                input_key,
1381                input,
1382                input_mfp,
1383                forms: keys,
1384                strategy,
1385            } => {
1386                let input = expect_input(input);
1387                input.ensure_collections(
1388                    keys,
1389                    input_key,
1390                    input_mfp,
1391                    self.as_of_frontier.clone(),
1392                    self.until.clone(),
1393                    &self.config_set,
1394                    strategy,
1395                )
1396            }
1397        }
1398    }
1399
1400    fn log_dataflow_global_id(&self, dataflow_index: usize, global_id: GlobalId) {
1401        if let Some(logger) = &self.compute_logger {
1402            logger.log(&ComputeEvent::DataflowGlobal(DataflowGlobal {
1403                dataflow_index,
1404                global_id,
1405            }));
1406        }
1407    }
1408
1409    fn log_lir_mapping(&self, global_id: GlobalId, mapping: Vec<(LirId, LirMetadata)>) {
1410        if let Some(logger) = &self.compute_logger {
1411            logger.log(&ComputeEvent::LirMapping(LirMapping { global_id, mapping }));
1412        }
1413    }
1414
1415    fn log_operator_hydration(&self, bundle: &mut CollectionBundle<'scope, T>, lir_id: LirId) {
1416        // A `CollectionBundle` can contain more than one collection, which makes it not obvious to
1417        // which we should attach the logging operator.
1418        //
1419        // We could attach to each collection and track the lower bound of output frontiers.
1420        // However, that would be of limited use because we expect all collections to hydrate at
1421        // roughly the same time: The `ArrangeBy` operator is not fueled, so as soon as it sees the
1422        // frontier of the unarranged collection advance, it will perform all work necessary to
1423        // also advance its own frontier. We don't expect significant delays between frontier
1424        // advancements of the unarranged and arranged collections, so attaching the logging
1425        // operator to any one of them should produce accurate results.
1426        //
1427        // If the `CollectionBundle` contains both unarranged and arranged representations it is
1428        // beneficial to attach the logging operator to one of the arranged representation to avoid
1429        // unnecessary cloning of data. The unarranged collection feeds into the arrangements, so
1430        // if we attached the logging operator to it, we would introduce a fork in its output
1431        // stream, which would necessitate that all output data is cloned. In contrast, we can hope
1432        // that the output streams of the arrangements don't yet feed into anything else, so
1433        // attaching a (pass-through) logging operator does not introduce a fork.
1434
1435        match bundle.arranged.values_mut().next() {
1436            Some(arrangement) => {
1437                use ArrangementFlavor::*;
1438
1439                match arrangement {
1440                    Local(a, _) => {
1441                        a.stream = self.log_operator_hydration_inner(a.stream.clone(), lir_id);
1442                    }
1443                    Trace(_, a, _) => {
1444                        a.stream = self.log_operator_hydration_inner(a.stream.clone(), lir_id);
1445                    }
1446                }
1447            }
1448            None => {
1449                let (oks, _) = bundle
1450                    .collection
1451                    .as_mut()
1452                    .expect("CollectionBundle invariant");
1453                match oks {
1454                    CollectionEdge::Vec(c) => {
1455                        let stream = self.log_operator_hydration_inner(c.inner.clone(), lir_id);
1456                        *c = stream.as_collection();
1457                    }
1458                    CollectionEdge::Columnar(c) => {
1459                        let stream = self.log_operator_hydration_inner(c.inner.clone(), lir_id);
1460                        *c = stream.as_collection();
1461                    }
1462                }
1463            }
1464        }
1465    }
1466
1467    fn log_operator_hydration_inner<D>(
1468        &self,
1469        stream: Stream<'scope, T, D>,
1470        lir_id: LirId,
1471    ) -> Stream<'scope, T, D>
1472    where
1473        D: timely::Container + Clone + 'static,
1474    {
1475        let Some(logger) = self.compute_logger.clone() else {
1476            return stream.clone(); // hydration logging disabled
1477        };
1478
1479        let export_ids = self.export_ids.clone();
1480
1481        // Convert the dataflow as-of into a frontier we can compare with input frontiers.
1482        //
1483        // We (somewhat arbitrarily) define operators in iterative scopes to be hydrated when their
1484        // frontier advances to an outer time that's greater than the `as_of`. Comparing
1485        // `refine(as_of) < input_frontier` would find the moment when the first iteration was
1486        // complete, which is not what we want. We want `refine(as_of + 1) <= input_frontier`
1487        // instead.
1488        let mut hydration_frontier = Antichain::new();
1489        for time in self.as_of_frontier.iter() {
1490            if let Some(time) = time.try_step_forward() {
1491                hydration_frontier.insert(Refines::to_inner(time));
1492            }
1493        }
1494
1495        let name = format!("LogOperatorHydration ({lir_id})");
1496        stream.unary_frontier(Pipeline, &name, |_cap, _info| {
1497            let mut hydrated = false;
1498
1499            for &export_id in &export_ids {
1500                logger.log(&ComputeEvent::OperatorHydration(OperatorHydration {
1501                    export_id,
1502                    lir_id,
1503                    hydrated,
1504                }));
1505            }
1506
1507            move |(input, frontier), output| {
1508                // Pass through inputs.
1509                input.for_each(|cap, data| {
1510                    output.session(&cap).give_container(data);
1511                });
1512
1513                if hydrated {
1514                    return;
1515                }
1516
1517                if PartialOrder::less_equal(&hydration_frontier.borrow(), &frontier.frontier()) {
1518                    hydrated = true;
1519
1520                    for &export_id in &export_ids {
1521                        logger.log(&ComputeEvent::OperatorHydration(OperatorHydration {
1522                            export_id,
1523                            lir_id,
1524                            hydrated,
1525                        }));
1526                    }
1527                }
1528            }
1529        })
1530    }
1531}
1532
1533#[allow(dead_code)] // Some of the methods on this trait are unused, but useful to have.
1534/// A timestamp type that can be used for operations within MZ's dataflow layer.
1535pub trait RenderTimestamp: MzTimestamp + Default + Refines<mz_repr::Timestamp> {
1536    /// The system timestamp component of the timestamp.
1537    ///
1538    /// This is useful for manipulating the system time, as when delaying
1539    /// updates for subsequent cancellation, as with monotonic reduction.
1540    fn system_time(&mut self) -> &mut mz_repr::Timestamp;
1541    /// Effects a system delay in terms of the timestamp summary.
1542    fn system_delay(delay: mz_repr::Timestamp) -> <Self as Timestamp>::Summary;
1543    /// The event timestamp component of the timestamp.
1544    fn event_time(&self) -> mz_repr::Timestamp;
1545    /// The event timestamp component of the timestamp, as a mutable reference.
1546    fn event_time_mut(&mut self) -> &mut mz_repr::Timestamp;
1547    /// Effects an event delay in terms of the timestamp summary.
1548    fn event_delay(delay: mz_repr::Timestamp) -> <Self as Timestamp>::Summary;
1549    /// Steps the timestamp back so that logical compaction to the output will
1550    /// not conflate `self` with any historical times.
1551    fn step_back(&self) -> Self;
1552}
1553
1554/// Apply temporal bucketing to a stream when the timestamp type supports it.
1555///
1556/// Sibling to [`RenderTimestamp`]: bucketing is an arrangement-time concern, not a
1557/// general property of a render timestamp, so the dispatch lives in its own trait.
1558/// Total-ordered timestamps perform real bucketing; partially-ordered timestamps
1559/// (e.g. `Product<…>` in iterative scopes) implement this as a no-op.
1560pub trait MaybeBucketByTime: Timestamp {
1561    fn maybe_apply_temporal_bucketing<'scope, D>(
1562        stream: StreamVec<'scope, Self, (D, Self, Diff)>,
1563        as_of: Antichain<mz_repr::Timestamp>,
1564        summary: mz_repr::Timestamp,
1565    ) -> VecCollection<'scope, Self, D, Diff>
1566    where
1567        D: differential_dataflow::ExchangeData
1568            + crate::typedefs::MzData
1569            + differential_dataflow::Hashable;
1570}
1571
1572impl RenderTimestamp for mz_repr::Timestamp {
1573    fn system_time(&mut self) -> &mut mz_repr::Timestamp {
1574        self
1575    }
1576    fn system_delay(delay: mz_repr::Timestamp) -> <Self as Timestamp>::Summary {
1577        delay
1578    }
1579    fn event_time(&self) -> mz_repr::Timestamp {
1580        *self
1581    }
1582    fn event_time_mut(&mut self) -> &mut mz_repr::Timestamp {
1583        self
1584    }
1585    fn event_delay(delay: mz_repr::Timestamp) -> <Self as Timestamp>::Summary {
1586        delay
1587    }
1588    fn step_back(&self) -> Self {
1589        self.saturating_sub(1)
1590    }
1591}
1592
1593impl MaybeBucketByTime for mz_repr::Timestamp {
1594    fn maybe_apply_temporal_bucketing<'scope, D>(
1595        stream: StreamVec<'scope, Self, (D, Self, Diff)>,
1596        as_of: Antichain<mz_repr::Timestamp>,
1597        summary: mz_repr::Timestamp,
1598    ) -> VecCollection<'scope, Self, D, Diff>
1599    where
1600        D: differential_dataflow::ExchangeData
1601            + crate::typedefs::MzData
1602            + differential_dataflow::Hashable,
1603    {
1604        stream
1605            .bucket::<CapacityContainerBuilder<_>>(as_of, summary)
1606            .as_collection()
1607    }
1608}
1609
1610impl RenderTimestamp for Product<mz_repr::Timestamp, PointStamp<u64>> {
1611    fn system_time(&mut self) -> &mut mz_repr::Timestamp {
1612        &mut self.outer
1613    }
1614    fn system_delay(delay: mz_repr::Timestamp) -> <Self as Timestamp>::Summary {
1615        Product::new(delay, Default::default())
1616    }
1617    fn event_time(&self) -> mz_repr::Timestamp {
1618        self.outer
1619    }
1620    fn event_time_mut(&mut self) -> &mut mz_repr::Timestamp {
1621        &mut self.outer
1622    }
1623    fn event_delay(delay: mz_repr::Timestamp) -> <Self as Timestamp>::Summary {
1624        Product::new(delay, Default::default())
1625    }
1626    fn step_back(&self) -> Self {
1627        // It is necessary to step back both coordinates of a product,
1628        // and when one is a `PointStamp` that also means all coordinates
1629        // of the pointstamp.
1630        let inner = self.inner.clone();
1631        let mut vec = inner.into_inner();
1632        for item in vec.iter_mut() {
1633            *item = item.saturating_sub(1);
1634        }
1635        Product::new(self.outer.saturating_sub(1), PointStamp::new(vec))
1636    }
1637}
1638
1639impl MaybeBucketByTime for Product<mz_repr::Timestamp, PointStamp<u64>> {
1640    fn maybe_apply_temporal_bucketing<'scope, D>(
1641        stream: StreamVec<'scope, Self, (D, Self, Diff)>,
1642        _as_of: Antichain<mz_repr::Timestamp>,
1643        _summary: mz_repr::Timestamp,
1644    ) -> VecCollection<'scope, Self, D, Diff>
1645    where
1646        D: differential_dataflow::ExchangeData
1647            + crate::typedefs::MzData
1648            + differential_dataflow::Hashable,
1649    {
1650        // TODO: Implement bucketing on outer timestamp for iterative scopes.
1651        stream.as_collection()
1652    }
1653}
1654
1655/// A signal that can be awaited by operators to suspend them prior to startup.
1656///
1657/// Creating a signal also yields a token, dropping of which causes the signal to fire.
1658///
1659/// `StartSignal` is designed to be usable by both async and sync Timely operators.
1660///
1661///  * Async operators can simply `await` it.
1662///  * Sync operators should register an [`ActivateOnDrop`] value via [`StartSignal::drop_on_fire`]
1663///    and then check `StartSignal::has_fired()` on each activation.
1664#[derive(Clone)]
1665pub(crate) struct StartSignal {
1666    /// A future that completes when the signal fires.
1667    ///
1668    /// The inner type is `Infallible` because no data is ever expected on this channel. Instead the
1669    /// signal is activated by dropping the corresponding `Sender`.
1670    fut: futures::future::Shared<oneshot::Receiver<Infallible>>,
1671    /// A weak reference to the token, to register drop-on-fire values.
1672    token_ref: Weak<RefCell<Box<dyn Any>>>,
1673}
1674
1675impl StartSignal {
1676    /// Create a new `StartSignal` and a corresponding token that activates the signal when
1677    /// dropped.
1678    pub fn new() -> (Self, Rc<dyn Any>) {
1679        let (tx, rx) = oneshot::channel::<Infallible>();
1680        let token: Rc<RefCell<Box<dyn Any>>> = Rc::new(RefCell::new(Box::new(tx)));
1681        let signal = Self {
1682            fut: rx.shared(),
1683            token_ref: Rc::downgrade(&token),
1684        };
1685        (signal, token)
1686    }
1687
1688    pub fn has_fired(&self) -> bool {
1689        self.token_ref.strong_count() == 0
1690    }
1691
1692    /// Returns a Send-safe future that completes when the signal fires.
1693    ///
1694    /// Unlike `StartSignal` itself, the returned future does not retain a reference to the token,
1695    /// so it cannot be used for `drop_on_fire` or `has_fired` checks.
1696    pub fn into_send_future(self) -> impl Future<Output = ()> + Send {
1697        use futures::FutureExt;
1698        self.fut.map(|_| ())
1699    }
1700
1701    pub fn drop_on_fire(&self, to_drop: Box<dyn Any>) {
1702        if let Some(token) = self.token_ref.upgrade() {
1703            let mut token = token.borrow_mut();
1704            let inner = std::mem::replace(&mut *token, Box::new(()));
1705            *token = Box::new((inner, to_drop));
1706        }
1707    }
1708}
1709
1710impl Future for StartSignal {
1711    type Output = ();
1712
1713    fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
1714        self.fut.poll_unpin(cx).map(|_| ())
1715    }
1716}
1717
1718/// Extension trait to attach a `StartSignal` to operator outputs.
1719pub(crate) trait WithStartSignal {
1720    /// Delays data and progress updates until the start signal has fired.
1721    ///
1722    /// Note that this operator needs to buffer all incoming data, so it has some memory footprint,
1723    /// depending on the amount and shape of its inputs.
1724    fn with_start_signal(self, signal: StartSignal) -> Self;
1725}
1726
1727impl<'scope, Tr> WithStartSignal for Arranged<'scope, Tr>
1728where
1729    Tr: TraceReader<Time: RenderTimestamp> + Clone,
1730{
1731    fn with_start_signal(self, signal: StartSignal) -> Self {
1732        Arranged {
1733            stream: self.stream.with_start_signal(signal),
1734            trace: self.trace,
1735        }
1736    }
1737}
1738
1739impl<'scope, T: Timestamp, D> WithStartSignal for Stream<'scope, T, D>
1740where
1741    D: timely::Container + Clone + 'static,
1742{
1743    fn with_start_signal(self, signal: StartSignal) -> Self {
1744        let activations = self.scope().activations();
1745        self.unary(Pipeline, "StartSignal", |_cap, info| {
1746            let token = Box::new(ActivateOnDrop::new((), info.address, activations));
1747            signal.drop_on_fire(token);
1748
1749            let mut stash = Vec::new();
1750
1751            move |input, output| {
1752                // Stash incoming updates as long as the start signal has not fired.
1753                if !signal.has_fired() {
1754                    input.for_each(|cap, data| stash.push((cap, std::mem::take(data))));
1755                    return;
1756                }
1757
1758                // Release any data we might still have stashed.
1759                for (cap, mut data) in std::mem::take(&mut stash) {
1760                    output.session(&cap).give_container(&mut data);
1761                }
1762
1763                // Pass through all remaining input data.
1764                input.for_each(|cap, data| {
1765                    output.session(&cap).give_container(data);
1766                });
1767            }
1768        })
1769    }
1770}
1771
1772/// Suppress progress messages for times before the given `as_of`.
1773///
1774/// This operator exists specifically to work around a memory spike we'd otherwise see when
1775/// hydrating arrangements (database-issues#6368). The memory spike happens because when the `arrange_core`
1776/// operator observes a frontier advancement without data it inserts an empty batch into the spine.
1777/// When it later inserts the snapshot batch into the spine, an empty batch is already there and
1778/// the spine initiates a merge of these batches, which requires allocating a new batch the size of
1779/// the snapshot batch.
1780///
1781/// The strategy to avoid the spike is to prevent the insertion of that initial empty batch by
1782/// ensuring that the first frontier advancement downstream `arrange_core` operators observe is
1783/// beyond the `as_of`, so the snapshot data has already been collected.
1784///
1785/// To ensure this, this operator needs to take two measures:
1786///  * Keep around a minimum capability until the input announces progress beyond the `as_of`.
1787///  * Reclock all updates emitted at times not beyond the `as_of` to the minimum time.
1788///
1789/// The second measure requires elaboration: If we wouldn't reclock snapshot updates, they might
1790/// still be upstream of `arrange_core` operators when those get to know about us dropping the
1791/// minimum capability. The in-flight snapshot updates would hold back the input frontiers of
1792/// `arrange_core` operators to the `as_of`, which would cause them to insert empty batches.
1793fn suppress_early_progress<'scope, T: Timestamp, D>(
1794    stream: Stream<'scope, T, D>,
1795    as_of: Antichain<T>,
1796) -> Stream<'scope, T, D>
1797where
1798    D: Data + timely::Container,
1799{
1800    stream.unary_frontier(Pipeline, "SuppressEarlyProgress", |default_cap, _info| {
1801        let mut early_cap = Some(default_cap);
1802
1803        move |(input, frontier), output| {
1804            input.for_each_time(|data_cap, data| {
1805                if as_of.less_than(data_cap.time()) {
1806                    let mut session = output.session(&data_cap);
1807                    for data in data {
1808                        session.give_container(data);
1809                    }
1810                } else {
1811                    let cap = early_cap.as_ref().expect("early_cap can't be dropped yet");
1812                    let mut session = output.session(&cap);
1813                    for data in data {
1814                        session.give_container(data);
1815                    }
1816                }
1817            });
1818
1819            if !PartialOrder::less_equal(&frontier.frontier(), &as_of.borrow()) {
1820                early_cap.take();
1821            }
1822        }
1823    })
1824}
1825
1826/// Extension trait for [`Stream`] to selectively limit progress.
1827trait LimitProgress<T: Timestamp> {
1828    /// Limit the progress of the stream until its frontier reaches the given `upper` bound. Expects
1829    /// the implementation to observe times in data, and release capabilities based on the probe's
1830    /// frontier, after applying `slack` to round up timestamps.
1831    ///
1832    /// The implementation of this operator is subtle to avoid regressions in the rest of the
1833    /// system. Specifically joins hold back compaction on the other side of the join, so we need to
1834    /// make sure we release capabilities as soon as possible. This is why we only limit progress
1835    /// for times before the `upper`, which is the time until which the source can distinguish
1836    /// updates at the time of rendering. Once we make progress to the `upper`, we need to release
1837    /// our capability.
1838    ///
1839    /// This isn't perfect, and can result in regressions if on of the inputs lags behind. We could
1840    /// consider using the join of the uppers, i.e, use lower bound upper of all available inputs.
1841    ///
1842    /// Once the input frontier reaches `[]`, the implementation must release any capability to
1843    /// allow downstream operators to release resources.
1844    ///
1845    /// The implementation should limit the number of pending times to `limit` if it is `Some` to
1846    /// avoid unbounded memory usage.
1847    ///
1848    /// * `handle` is a probe installed on the dataflow's outputs as late as possible, but before
1849    ///   any timestamp rounding happens (c.f., `REFRESH EVERY` materialized views).
1850    /// * `slack_ms` is the number of milliseconds to round up timestamps to.
1851    /// * `name` is a human-readable name for the operator.
1852    /// * `limit` is the maximum number of pending times to keep around.
1853    /// * `upper` is the upper bound of the stream's frontier until which the implementation can
1854    ///   retain a capability.
1855    fn limit_progress(
1856        self,
1857        handle: MzProbeHandle<T>,
1858        slack_ms: u64,
1859        limit: Option<usize>,
1860        upper: Antichain<T>,
1861        name: String,
1862    ) -> Self;
1863}
1864
1865// TODO: We could make this generic over a `T` that can be converted to and from a u64 millisecond
1866// number.
1867impl<'scope, D, R> LimitProgress<mz_repr::Timestamp>
1868    for StreamVec<'scope, mz_repr::Timestamp, (D, mz_repr::Timestamp, R)>
1869where
1870    D: Clone + 'static,
1871    R: Clone + 'static,
1872{
1873    fn limit_progress(
1874        self,
1875        handle: MzProbeHandle<mz_repr::Timestamp>,
1876        slack_ms: u64,
1877        limit: Option<usize>,
1878        upper: Antichain<mz_repr::Timestamp>,
1879        name: String,
1880    ) -> Self {
1881        let scope = self.scope();
1882        let stream =
1883            self.unary_frontier(Pipeline, &format!("LimitProgress({name})"), |_cap, info| {
1884                // Times that we've observed on our input.
1885                let mut pending_times: BTreeSet<mz_repr::Timestamp> = BTreeSet::new();
1886                // Capability for the lower bound of `pending_times`, if any.
1887                let mut retained_cap: Option<Capability<mz_repr::Timestamp>> = None;
1888
1889                let activator = scope.activator_for(info.address);
1890                handle.activate(activator.clone());
1891
1892                move |(input, frontier), output| {
1893                    input.for_each(|cap, data| {
1894                        for time in data
1895                            .iter()
1896                            .flat_map(|(_, time, _)| u64::from(time).checked_add(slack_ms))
1897                        {
1898                            // `slack_ms == 0` means no rounding; otherwise round up to the next
1899                            // multiple of `slack_ms`. Avoids a divide-by-zero panic when the
1900                            // operator is configured without slack.
1901                            let rounded_time = if slack_ms == 0 {
1902                                time
1903                            } else {
1904                                (time / slack_ms).saturating_add(1).saturating_mul(slack_ms)
1905                            };
1906                            if !upper.less_than(&rounded_time.into()) {
1907                                pending_times.insert(rounded_time.into());
1908                            }
1909                        }
1910                        output.session(&cap).give_container(data);
1911                        if retained_cap.as_ref().is_none_or(|c| {
1912                            !c.time().less_than(cap.time()) && !upper.less_than(cap.time())
1913                        }) {
1914                            retained_cap = Some(cap.retain(0));
1915                        }
1916                    });
1917
1918                    handle.with_frontier(|f| {
1919                        while pending_times
1920                            .first()
1921                            .map_or(false, |retained_time| !f.less_than(&retained_time))
1922                        {
1923                            let _ = pending_times.pop_first();
1924                        }
1925                    });
1926
1927                    while limit.map_or(false, |limit| pending_times.len() > limit) {
1928                        let _ = pending_times.pop_first();
1929                    }
1930
1931                    match (retained_cap.as_mut(), pending_times.first()) {
1932                        (Some(cap), Some(first)) => cap.downgrade(first),
1933                        (_, None) => retained_cap = None,
1934                        _ => {}
1935                    }
1936
1937                    if frontier.is_empty() {
1938                        retained_cap = None;
1939                        pending_times.clear();
1940                    }
1941
1942                    if !pending_times.is_empty() {
1943                        tracing::debug!(
1944                            name,
1945                            info.global_id,
1946                            pending_times = %PendingTimesDisplay(pending_times.iter().cloned()),
1947                            frontier = ?frontier.frontier().get(0),
1948                            probe = ?handle.with_frontier(|f| f.get(0).cloned()),
1949                            ?upper,
1950                            "pending times",
1951                        );
1952                    }
1953                }
1954            });
1955        stream
1956    }
1957}
1958
1959/// A formatter for an iterator of timestamps that displays the first element, and subsequently
1960/// the difference between timestamps.
1961struct PendingTimesDisplay<T>(T);
1962
1963impl<T> std::fmt::Display for PendingTimesDisplay<T>
1964where
1965    T: IntoIterator<Item = mz_repr::Timestamp> + Clone,
1966{
1967    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1968        let mut iter = self.0.clone().into_iter();
1969        write!(f, "[")?;
1970        if let Some(first) = iter.next() {
1971            write!(f, "{}", first)?;
1972            let mut last = u64::from(first);
1973            for time in iter {
1974                write!(f, ", +{}", u64::from(time) - last)?;
1975                last = u64::from(time);
1976            }
1977        }
1978        write!(f, "]")?;
1979        Ok(())
1980    }
1981}
1982
1983/// Helper to merge pairs of datum iterators into a row or split a datum iterator
1984/// into two rows, given the arity of the first component.
1985#[derive(Clone, Copy, Debug)]
1986struct Pairer {
1987    split_arity: usize,
1988}
1989
1990impl Pairer {
1991    /// Creates a pairer with knowledge of the arity of first component in the pair.
1992    fn new(split_arity: usize) -> Self {
1993        Self { split_arity }
1994    }
1995
1996    /// Merges a pair of datum iterators creating a `Row` instance.
1997    fn merge<'a, I1, I2>(&self, first: I1, second: I2) -> Row
1998    where
1999        I1: IntoIterator<Item = Datum<'a>>,
2000        I2: IntoIterator<Item = Datum<'a>>,
2001    {
2002        SharedRow::pack(first.into_iter().chain(second))
2003    }
2004
2005    /// Splits a datum iterator into a pair of `Row` instances.
2006    fn split<'a>(&self, datum_iter: impl IntoIterator<Item = Datum<'a>>) -> (Row, Row) {
2007        let mut datum_iter = datum_iter.into_iter();
2008        let mut row_builder = SharedRow::get();
2009        let first = row_builder.pack_using(datum_iter.by_ref().take(self.split_arity));
2010        let second = row_builder.pack_using(datum_iter);
2011        (first, second)
2012    }
2013}