Skip to main content

mz_compute_types/plan/
lowering.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//! Lowering [`DataflowDescription`]s from MIR ([`MirRelationExpr`]) to LIR ([`LirRelationExpr`]).
11
12use std::collections::{BTreeMap, BTreeSet};
13
14use columnar::Len;
15use itertools::Itertools;
16use mz_expr::JoinImplementation::{DeltaQuery, Differential, IndexedFilter, Unimplemented};
17use mz_expr::{
18    AggregateExpr, Columns, Id, JoinInputMapper, MapFilterProject, MfpPlan, MirRelationExpr,
19    MirScalarExpr, OptimizedMirRelationExpr, SafeMfpPlan, TableFunc, permutation_for_arrangement,
20};
21use mz_ore::{assert_none, soft_assert_eq_or_log, soft_panic_or_log};
22use mz_repr::optimize::OptimizerFeatures;
23use mz_repr::{GlobalId, StableRow, Timestamp};
24
25use crate::dataflows::{BuildDesc, DataflowDescription, IndexImport, LirDataflowDescription};
26use crate::plan::join::{DeltaJoinPlan, JoinPlan, LinearJoinPlan};
27use crate::plan::reduce::{KeyValPlan, ReducePlan};
28use crate::plan::scalar::{
29    LirScalarExpr, lses_from_mses, mfp_mir_to_lir, mfp_mir_to_lir_plan, mfp_plan_mir_to_lir,
30};
31use crate::plan::threshold::ThresholdPlan;
32use crate::plan::top_k::TopKPlan;
33use crate::plan::{
34    ArrangementStrategy, AvailableCollections, GetPlan, LirId, LirRelationExpr, LirRelationNode,
35    LoweringMetrics,
36};
37
38/// Pick an [`ArrangementStrategy`] based on whether the input may contain future-stamped
39/// updates. Future updates are the only case where temporal bucketing pays off.
40///
41/// Any arrangement or consolidation that absorbs data that can have future updates should be
42/// guarded by a temporal bucketing operator.
43fn strategy_from_future(has_future_updates: bool) -> ArrangementStrategy {
44    if has_future_updates {
45        ArrangementStrategy::TemporalBucketing
46    } else {
47        ArrangementStrategy::Direct
48    }
49}
50
51/// The result of lowering a [`MirRelationExpr`] to a [`LirRelationExpr`].
52struct LoweredExpr {
53    /// The lowered plan.
54    plan: LirRelationExpr,
55    /// The arrangement keys that the plan is certain to produce.
56    keys: AvailableCollections,
57    /// Whether the plan's output may contain updates at future timestamps,
58    /// e.g., from a temporal MFP using `mz_now()`.
59    has_future_updates: bool,
60}
61
62pub(super) struct Context {
63    /// Known bindings to (possibly arranged) collections.
64    arrangements: BTreeMap<Id, AvailableCollections>,
65    /// Ids whose collections may contain updates at future timestamps,
66    /// e.g., from a temporal MFP using `mz_now()`.
67    has_future_updates: BTreeSet<Id>,
68    /// Tracks the next available `LirId`.
69    next_lir_id: LirId,
70    /// Information to print along with error messages.
71    debug_info: LirDebugInfo,
72    /// Whether to enable fusion of MFPs in reductions.
73    enable_reduce_mfp_fusion: bool,
74    /// Metrics recorded during lowering, if any are being collected.
75    metrics: Option<LoweringMetrics>,
76    /// Whether the current expression is subject to single-time (one-shot
77    /// `SELECT`) monotonic operator selection.
78    ///
79    /// Lowering locks in which arrangements a node makes available, and that set
80    /// changes with the chosen operator variant (e.g. a monotonic `TopK`/`Reduce`
81    /// arranges differently than its non-monotonic form). So the variant must be
82    /// picked here, during lowering, rather than by a later rewrite that would
83    /// leave the already-computed `AvailableCollections` describing the wrong shape.
84    ///
85    /// Initialized from the dataflow's `is_single_time()` and forced to `false`
86    /// while lowering the recursive bindings of a `LetRec`, whose values are not
87    /// restricted to a single time.
88    single_time: bool,
89    /// Global ids of the dataflow's source imports.
90    source_imports: BTreeSet<GlobalId>,
91    /// The MFPs pushed onto `Get::Collection` reads of source imports, as they were
92    /// before planning, keyed by the `Get`'s `LirId`.
93    ///
94    /// `refine_source_mfps` identifies common parts across sibling reads and pushes the
95    /// common part into the shared source MFP. Keeping the unplanned form here lets it
96    /// work on `mz_now()` predicates directly, so the composed source MFP is planned
97    /// only once.
98    source_get_mfps: BTreeMap<LirId, MapFilterProject<MirScalarExpr>>,
99}
100
101impl Context {
102    pub fn new(
103        debug_name: String,
104        features: &OptimizerFeatures,
105        metrics: Option<&LoweringMetrics>,
106    ) -> Self {
107        Self {
108            arrangements: Default::default(),
109            has_future_updates: Default::default(),
110            next_lir_id: LirId(1),
111            debug_info: LirDebugInfo {
112                debug_name,
113                id: GlobalId::Transient(0),
114            },
115            enable_reduce_mfp_fusion: features.enable_reduce_mfp_fusion,
116            metrics: metrics.cloned(),
117            // Set from the dataflow in `lower` before any expression is lowered.
118            single_time: false,
119            source_imports: Default::default(),
120            source_get_mfps: Default::default(),
121        }
122    }
123
124    fn allocate_lir_id(&mut self) -> LirId {
125        let id = self.next_lir_id;
126        self.next_lir_id = LirId(
127            self.next_lir_id
128                .0
129                .checked_add(1)
130                .expect("No LirId overflow"),
131        );
132        id
133    }
134
135    pub fn lower(
136        mut self,
137        desc: DataflowDescription<OptimizedMirRelationExpr>,
138    ) -> Result<LirDataflowDescription, String> {
139        // Sources might provide arranged forms of their data, in the future.
140        // Indexes provide arranged forms of their data.
141        for IndexImport {
142            desc: index_desc,
143            typ,
144            ..
145        } in desc.index_imports.values()
146        {
147            let key = lses_from_mses(&index_desc.key);
148            // TODO[btv] - We should be told the permutation by
149            // `index_desc`, and it should have been generated
150            // at the same point the thinning logic was.
151            //
152            // We should for sure do that soon, but it requires
153            // a bit of a refactor, so for now we just
154            // _assume_ that they were both generated by `permutation_for_arrangement`,
155            // and recover it here.
156            let (permutation, thinning) = permutation_for_arrangement(&key, typ.arity());
157            let index_keys = self
158                .arrangements
159                .entry(Id::Global(index_desc.on_id))
160                .or_insert_with(AvailableCollections::default);
161            index_keys.arranged.push((key, permutation, thinning));
162        }
163        for id in desc.source_imports.keys() {
164            self.arrangements
165                .entry(Id::Global(*id))
166                .or_insert_with(AvailableCollections::new_raw);
167            self.source_imports.insert(*id);
168        }
169
170        // One-shot `SELECT` dataflows run at a single time, which lets us select
171        // monotonic operator variants during lowering (see the `TopK` and `Reduce`
172        // arms), so that `AvailableCollections` reflect the final operator variant.
173        self.single_time = desc.is_single_time();
174
175        // Build each object in order, registering the arrangements it forms.
176        let mut objects_to_build = Vec::with_capacity(desc.objects_to_build.len());
177        for build in desc.objects_to_build {
178            self.debug_info.id = build.id;
179            let LoweredExpr {
180                plan,
181                keys,
182                has_future_updates,
183            } = self.lower_mir_expr(&build.plan)?;
184
185            self.arrangements.insert(Id::Global(build.id), keys);
186            if has_future_updates {
187                self.has_future_updates.insert(Id::Global(build.id));
188            }
189            objects_to_build.push(BuildDesc { id: build.id, plan });
190        }
191
192        // Choose, for each sink, which form of its `from` collection the renderer consumes.
193        // `None` means the unarranged collection. Otherwise we name an arrangement key, so the
194        // renderer no longer has to pick one arbitrarily at runtime. The choice is possible here
195        // because the loop above registered the arrangements of every built object.
196        let mut sink_exports = desc.sink_exports;
197        for sink in sink_exports.values_mut() {
198            let available = self
199                .arrangements
200                .get(&Id::Global(sink.from))
201                .ok_or_else(|| {
202                    format!("sink source {} is neither imported nor built", sink.from)
203                })?;
204            // Prefer the unarranged collection when it exists, even if arrangements exist too.
205            // The renderer reads `bundle.collection` whenever it is present, and reading it
206            // avoids reconstructing full rows from an arrangement's key and value.
207            sink.from_arrangement = if available.raw {
208                None
209            } else {
210                // `AvailableCollections` holds at least one form, so without `raw` there is an
211                // arrangement. `arbitrary_arrangement` asserts this.
212                let (key, permutation, thinning) = available
213                    .arbitrary_arrangement()
214                    .expect("non-raw collection has an arrangement");
215                let mut mfp = MapFilterProject::<LirScalarExpr>::new(sink.from_desc.arity());
216                mfp.permute_fn(|c| permutation[c], thinning.len() + key.len());
217                let mfp = mfp
218                    .into_plan()
219                    .map_err(|e| format!("planning sink {} MFP: {e}", sink.from))?;
220                Some((key.clone(), mfp))
221            };
222        }
223
224        let mut dataflow = DataflowDescription {
225            source_imports: desc.source_imports,
226            index_imports: desc.index_imports,
227            objects_to_build,
228            index_exports: desc.index_exports,
229            sink_exports,
230            as_of: desc.as_of,
231            until: desc.until,
232            initial_storage_as_of: desc.initial_storage_as_of,
233            refresh_schedule: desc.refresh_schedule,
234            debug_name: desc.debug_name,
235            time_dependence: desc.time_dependence,
236        };
237
238        // Refining: identify the common parts in the MFPs pushed onto a
239        // source's reads and hoist the shared prefix into the source itself.
240        self.refine_source_mfps(&mut dataflow);
241
242        dataflow.try_map_source_operators(MfpPlan::create_from)
243    }
244
245    /// Identifies common parts of the `MapFilterProject`s pushed onto sibling `Get::Collection`
246    /// reads of each imported source, hoisting the shared prefix into the
247    /// source's own MFP.
248    ///
249    /// The reads' MFPs are lowering artifacts (MIR sees only `Get(GlobalId)`),
250    /// so this belongs in lowering. It works on the reads' unplanned MFPs stashed
251    /// in [`Self::source_get_mfps`]: a pushed-down temporal predicate then arrives
252    /// in the same form as its copy already in the source's MFP, so `optimize`
253    /// merges the two, and the source MFP is planned only once, at the end of
254    /// lowering.
255    fn refine_source_mfps(&mut self, dataflow: &mut DataflowDescription<LirRelationExpr>) {
256        for (source_id, source_import) in dataflow.source_imports.iter_mut() {
257            let source = &mut source_import.desc;
258            let source_id = *source_id;
259            let mut identity_present = false;
260
261            // Collect the unplanned MFPs pushed onto this source's
262            // `Get::Collection` reads. Also note identity reads, which block pushdown.
263            let mut taken: Vec<(LirId, MapFilterProject<MirScalarExpr>)> = Vec::new();
264            for build_desc in dataflow.objects_to_build.iter() {
265                let mut todo = vec![&build_desc.plan];
266                while let Some(expression) = todo.pop() {
267                    let node = &expression.node;
268                    if let LirRelationNode::Get { id, plan, .. } = node {
269                        if *id == Id::Global(source_id) {
270                            match plan {
271                                GetPlan::Collection(_) => {
272                                    let mfp = self
273                                        .source_get_mfps
274                                        .remove(&expression.lir_id)
275                                        .expect("stashed MFP for source Get::Collection");
276                                    taken.push((expression.lir_id, mfp));
277                                }
278                                GetPlan::PassArrangements => {
279                                    identity_present = true;
280                                }
281                                GetPlan::Arrangement(..) => {
282                                    panic!("Surprising `GetPlan` for imported source: {:?}", plan);
283                                }
284                            }
285                        }
286                    } else {
287                        todo.extend(node.children());
288                    }
289                }
290            }
291
292            // Direct exports of sources are possible, and prevent pushdown.
293            identity_present |= dataflow
294                .index_exports
295                .values()
296                .any(|(x, _)| x.on_id == source_id);
297            identity_present |= dataflow.sink_exports.values().any(|x| x.from == source_id);
298
299            if identity_present || taken.is_empty() {
300                // Nothing to push down. The reads already carry their final LIR
301                // MFPs, so leave them untouched.
302                continue;
303            }
304
305            // Extract the common prefix and push it into the source's MFP.
306            let mut mfp_refs: Vec<&mut MapFilterProject<MirScalarExpr>> =
307                taken.iter_mut().map(|(_, mfp)| mfp).collect();
308            let common = MapFilterProject::extract_common(&mut mfp_refs[..]);
309            let mut source_mfp = if let Some(mfp) = source.arguments.operators.take() {
310                MapFilterProject::compose(mfp, common)
311            } else {
312                common
313            };
314            source_mfp.optimize();
315            source.arguments.operators = Some(source_mfp);
316
317            // Convert each residual MFP back to an LIR `MfpPlan` once, and
318            // install it on the corresponding read by `LirId`.
319            let replacements: BTreeMap<LirId, MfpPlan<LirScalarExpr>> = taken
320                .into_iter()
321                .map(|(lir_id, mir_mfp)| (lir_id, mfp_mir_to_lir_plan(mir_mfp)))
322                .collect();
323
324            for build_desc in dataflow.objects_to_build.iter_mut() {
325                let mut todo = vec![&mut build_desc.plan];
326                while let Some(expression) = todo.pop() {
327                    if let Some(replacement) = replacements.get(&expression.lir_id) {
328                        if let LirRelationNode::Get {
329                            plan: GetPlan::Collection(mfp_plan),
330                            ..
331                        } = &mut expression.node
332                        {
333                            *mfp_plan = replacement.clone();
334                        } else {
335                            panic!(
336                                "LirId {:?} was a GetPlan::Collection but is now {:?}",
337                                expression.lir_id, expression.node
338                            );
339                        }
340                    }
341                    todo.extend(expression.node.children_mut());
342                }
343            }
344        }
345    }
346
347    /// This method converts a MirRelationExpr into a plan that can be directly rendered.
348    ///
349    /// The rough structure is that we repeatedly extract map/filter/project operators
350    /// from each expression we see, bundle them up as a `MapFilterProject` object, and
351    /// then produce a plan for the combination of that with the next operator.
352    ///
353    /// The method accesses `self.arrangements`, which it will locally add to and remove from for
354    /// `Let` bindings (by the end of the call it should contain the same bindings as when it
355    /// started).
356    ///
357    /// The result of the method is both a `LirRelationExpr`, but also a list of arrangements that
358    /// are certain to be produced, which can be relied on by the next steps in the plan.
359    /// Each of the arrangement keys is associated with an MFP that must be applied if that
360    /// arrangement is used, to back out the permutation associated with that arrangement.
361    ///
362    /// An empty list of arrangement keys indicates that only a `Collection` stream can
363    /// be assumed to exist.
364    fn lower_mir_expr(&mut self, expr: &MirRelationExpr) -> Result<LoweredExpr, String> {
365        // This function is recursive and can overflow its stack, so grow it if
366        // needed. The growth here is unbounded. Our general solution for this problem
367        // is to use [`ore::stack::RecursionGuard`] to additionally limit the stack
368        // depth. That however requires upstream error handling. This function is
369        // currently called by the Coordinator after calls to `catalog_transact`,
370        // and thus are not allowed to fail. Until that allows errors, we choose
371        // to allow the unbounded growth here. We are though somewhat protected by
372        // higher levels enforcing their own limits on stack depth (in the parser,
373        // transformer/desugarer, and planner).
374        mz_ore::stack::maybe_grow(|| self.lower_mir_expr_stack_safe(expr))
375    }
376
377    fn lower_mir_expr_stack_safe(&mut self, expr: &MirRelationExpr) -> Result<LoweredExpr, String> {
378        // Extract a maximally large MapFilterProject from `expr`.
379        // We will then try and push this in to the resulting expression.
380        //
381        // Importantly, `mfp` may contain temporal operators and not be a "safe" MFP.
382        // While we would eventually like all plan stages to be able to absorb such
383        // general operators, not all of them can.
384        let (mut mfp, expr) = MapFilterProject::extract_from_expression(expr);
385        // We attempt to plan what we have remaining, in the context of `mfp`.
386        // We may not be able to do this, and must wrap some operators with a `Mfp` stage.
387        let LoweredExpr {
388            mut plan,
389            mut keys,
390            mut has_future_updates,
391        } = match expr {
392            // These operators should have been extracted from the expression.
393            MirRelationExpr::Map { .. } => {
394                panic!("This operator should have been extracted");
395            }
396            MirRelationExpr::Filter { .. } => {
397                panic!("This operator should have been extracted");
398            }
399            MirRelationExpr::Project { .. } => {
400                panic!("This operator should have been extracted");
401            }
402            // These operators may not have been extracted, and need to result in a `LirRelationExpr`.
403            MirRelationExpr::Constant { rows, typ: _ } => {
404                let lir_id = self.allocate_lir_id();
405                let node = LirRelationNode::Constant {
406                    rows: rows.clone().map(|rows| {
407                        rows.into_iter()
408                            .map(|(row, diff)| (StableRow(row), Timestamp::MIN, diff))
409                            .collect()
410                    }),
411                };
412                // The plan, not arranged in any way.
413                LoweredExpr {
414                    plan: node.as_plan(lir_id),
415                    keys: AvailableCollections::new_raw(),
416                    has_future_updates: false,
417                }
418            }
419            MirRelationExpr::Get { id, typ: _, .. } => {
420                // This stage can absorb arbitrary MFP operators.
421                let mut mfp = mfp.take();
422                // If `mfp` is the identity, we can surface all imported arrangements.
423                // Otherwise, we apply `mfp` and promise no arrangements.
424                let mut in_keys = self
425                    .arrangements
426                    .get(id)
427                    .cloned()
428                    .unwrap_or_else(AvailableCollections::new_raw);
429
430                // Seek out an arrangement key that might be constrained to a literal.
431                // Note: this code has very little use nowadays, as its job was mostly taken over
432                // by `LiteralConstraints` (see in the below longer comment).
433                let key_val = in_keys
434                    .arranged
435                    .iter()
436                    .filter_map(|key| {
437                        mfp.literal_constraints(
438                            &key.0.iter().map(MirScalarExpr::from).collect_vec(),
439                        )
440                        .map(|val| {
441                            if let Some(metrics) = &self.metrics {
442                                metrics.inc_literal_constraints("get");
443                            }
444                            (key.clone(), val)
445                        })
446                    })
447                    .max_by_key(|(key, _val)| key.0.len());
448
449                // A source-import `Get::Collection`'s unplanned MFP, retained for
450                // `refine_source_mfps`. Stashed by `LirId` once the id is allocated below.
451                let mut source_get_mfp: Option<MapFilterProject<MirScalarExpr>> = None;
452
453                // Determine the plan of action for the `Get` stage.
454                let plan = if let Some(((key, permutation, thinning), val)) = &key_val {
455                    // This code path used to handle looking up literals from indexes, but it's
456                    // mostly deprecated, as this is nowadays performed by the `LiteralConstraints`
457                    // MIR transform instead. However, it's still called in a couple of tricky
458                    // special cases:
459                    // - `LiteralConstraints` handles only Gets of global ids, so this code still
460                    //   gets to handle Filters on top of Gets of local ids.
461                    // - Lowering does a `MapFilterProject::extract_from_expression`, while
462                    //   `LiteralConstraints` does
463                    //   `MapFilterProject::extract_non_errors_from_expr_mut`.
464                    // - It might happen that new literal constraint optimization opportunities
465                    //   appear somewhere near the end of the MIR optimizer after
466                    //   `LiteralConstraints` has already run.
467                    // (Also note that a similar literal constraint handling machinery is also
468                    // present when handling the leftover MFP after this big match.)
469                    mfp.permute_fn(|c| permutation[c], thinning.len() + key.len());
470                    in_keys.arranged = vec![(key.clone(), permutation.clone(), thinning.clone())];
471                    GetPlan::Arrangement(
472                        key.clone(),
473                        Some(StableRow(val.clone())),
474                        mfp_mir_to_lir_plan(mfp),
475                    )
476                } else if !mfp.is_identity() {
477                    // We need to ensure a collection exists, which means we must form it.
478                    if let Some((key, permutation, thinning)) =
479                        in_keys.arbitrary_arrangement().cloned()
480                    {
481                        mfp.permute_fn(|c| permutation[c], thinning.len() + key.len());
482                        in_keys.arranged =
483                            vec![(key.clone(), permutation.clone(), thinning.clone())];
484                        GetPlan::Arrangement(key.clone(), None, mfp_mir_to_lir_plan(mfp))
485                    } else {
486                        // `into_plan` optimizes first too, so this changes nothing about
487                        // the plan; it makes the stashed MFP the one that was planned.
488                        mfp.optimize();
489                        if let Id::Global(gid) = id {
490                            if self.source_imports.contains(gid) {
491                                source_get_mfp = Some(mfp.clone());
492                            }
493                        }
494                        let mir_plan = mfp.into_plan().expect("MFP planning failed");
495                        GetPlan::Collection(mfp_plan_mir_to_lir(mir_plan))
496                    }
497                } else {
498                    // By default, just pass input arrangements through.
499                    GetPlan::PassArrangements
500                };
501
502                let out_keys = if let GetPlan::PassArrangements = plan {
503                    in_keys.clone()
504                } else {
505                    AvailableCollections::new_raw()
506                };
507
508                // Even with a non-temporal MFP, we must propagate `has_future_updates`
509                // from the underlying binding — applying an MFP doesn't drop future-
510                // timestamped updates that already exist on the input.
511                //
512                // Note that global Gets from different dataflows can't have future updates, because
513                // both indexes and materialized views hold back future updates.
514                let has_future_updates = self.has_future_updates.contains(id)
515                    || match &plan {
516                        GetPlan::Arrangement(_, _, mfp_plan) | GetPlan::Collection(mfp_plan) => {
517                            mfp_plan.has_temporal_bounds()
518                        }
519                        GetPlan::PassArrangements => false,
520                    };
521
522                let lir_id = self.allocate_lir_id();
523                if let Some(mfp) = source_get_mfp {
524                    self.source_get_mfps.insert(lir_id, mfp);
525                }
526                let node = LirRelationNode::Get {
527                    id: id.clone(),
528                    keys: in_keys,
529                    plan,
530                };
531                // Return the plan, and any keys if an identity `mfp`.
532                LoweredExpr {
533                    plan: node.as_plan(lir_id),
534                    keys: out_keys,
535                    has_future_updates,
536                }
537            }
538            MirRelationExpr::Let { id, value, body } => {
539                // It would be unfortunate to have a non-trivial `mfp` here, as we hope
540                // that they would be pushed down. I am not sure if we should take the
541                // initiative to push down the `mfp` ourselves.
542
543                // Plan the value using only the initial arrangements, but
544                // introduce any resulting arrangements bound to `id`.
545                let LoweredExpr {
546                    plan: value,
547                    keys: v_keys,
548                    has_future_updates: v_future,
549                } = self.lower_mir_expr(value)?;
550                let pre_existing = self.arrangements.insert(Id::Local(*id), v_keys);
551                assert_none!(pre_existing);
552                if v_future {
553                    self.has_future_updates.insert(Id::Local(*id));
554                }
555                // Plan the body using initial and `value` arrangements,
556                // and then remove reference to the value arrangements.
557                let LoweredExpr {
558                    plan: body,
559                    keys: b_keys,
560                    has_future_updates: b_future,
561                } = self.lower_mir_expr(body)?;
562                self.arrangements.remove(&Id::Local(*id));
563                self.has_future_updates.remove(&Id::Local(*id));
564                // Return the plan, and any `body` arrangements.
565                let lir_id = self.allocate_lir_id();
566                LoweredExpr {
567                    plan: LirRelationNode::Let {
568                        id: id.clone(),
569                        value: Box::new(value),
570                        body: Box::new(body),
571                    }
572                    .as_plan(lir_id),
573                    keys: b_keys,
574                    has_future_updates: b_future,
575                }
576            }
577            MirRelationExpr::LetRec {
578                ids,
579                values,
580                limits,
581                body,
582            } => {
583                assert_eq!(ids.len(), values.len());
584                assert_eq!(ids.len(), limits.len());
585                // Plan the values using only the available arrangements, but
586                // introduce any resulting arrangements bound to each `id`.
587                // Arrangements made available cannot be used by prior bindings,
588                // as we cannot circulate an arrangement through a `Variable` yet.
589                let mut lir_values = Vec::with_capacity(values.len());
590                let mut any_v_future = false;
591                // The recursive bindings of a `LetRec` are not restricted to a single
592                // time, so single-time monotonic selection must not apply to them. Only
593                // the `body`, lowered below, inherits the enclosing scope's flag.
594                let outer_single_time = self.single_time;
595                self.single_time = false;
596                for (id, value) in ids.iter().zip_eq(values) {
597                    let LoweredExpr {
598                        plan: mut lir_value,
599                        keys: mut v_keys,
600                        has_future_updates: v_future,
601                    } = self.lower_mir_expr(value)?;
602                    any_v_future |= v_future;
603                    // If `v_keys` does not contain an unarranged collection, we must form it.
604                    if !v_keys.raw {
605                        // Choose an "arbitrary" arrangement; TODO: prefer a specific one.
606                        let (input_key, permutation, thinning) =
607                            v_keys.arbitrary_arrangement().unwrap();
608                        let mut input_mfp = MapFilterProject::new(value.arity());
609                        input_mfp.permute_fn(|c| permutation[c], thinning.len() + input_key.len());
610                        let input_key = Some(input_key.clone());
611
612                        let forms = AvailableCollections::new_raw();
613
614                        // We just want to insert an `ArrangeBy` to form an unarranged collection,
615                        // but there is a complication: We shouldn't break the invariant (created by
616                        // `NormalizeLets`, and relied upon by the rendering) that there isn't
617                        // anything between two `LetRec`s. So if `lir_value` is itself a `LetRec`,
618                        // then we insert the `ArrangeBy` on the `body` of the inner `LetRec`,
619                        // instead of on top of the inner `LetRec`.
620                        //
621                        // We forward `v_future` for honesty; bucketing has no observable effect
622                        // inside an iterative scope, but the field should reflect reality.
623                        lir_value = match lir_value {
624                            LirRelationExpr {
625                                node:
626                                    LirRelationNode::LetRec {
627                                        ids,
628                                        values,
629                                        limits,
630                                        body,
631                                    },
632                                lir_id,
633                            } => {
634                                let inner_lir_id = self.allocate_lir_id();
635                                LirRelationNode::LetRec {
636                                    ids,
637                                    values,
638                                    limits,
639                                    body: Box::new(
640                                        LirRelationNode::ArrangeBy {
641                                            input_key,
642                                            input: body,
643                                            input_mfp: mfp_mir_to_lir_plan(input_mfp),
644                                            forms,
645                                            strategy: strategy_from_future(v_future),
646                                        }
647                                        .as_plan(inner_lir_id),
648                                    ),
649                                }
650                                .as_plan(lir_id)
651                            }
652                            lir_value => {
653                                let lir_id = self.allocate_lir_id();
654                                LirRelationNode::ArrangeBy {
655                                    input_key,
656                                    input: Box::new(lir_value),
657                                    input_mfp: mfp_mir_to_lir_plan(input_mfp),
658                                    forms,
659                                    strategy: strategy_from_future(v_future),
660                                }
661                                .as_plan(lir_id)
662                            }
663                        };
664                        v_keys.raw = true;
665                    }
666                    let pre_existing = self.arrangements.insert(Id::Local(*id), v_keys);
667                    assert_none!(pre_existing);
668                    if v_future {
669                        self.has_future_updates.insert(Id::Local(*id));
670                    }
671                    lir_values.push(lir_value);
672                }
673                // As we exit the iterative scope, we must leave all arrangements behind,
674                // as they reference a timestamp coordinate that must be stripped off.
675                for id in ids.iter() {
676                    self.arrangements
677                        .insert(Id::Local(*id), AvailableCollections::new_raw());
678                }
679                // Plan the body using initial and `value` arrangements,
680                // and then remove reference to the value arrangements.
681                self.single_time = outer_single_time;
682                let LoweredExpr {
683                    plan: body,
684                    keys: b_keys,
685                    has_future_updates: b_future,
686                } = self.lower_mir_expr(body)?;
687                for id in ids.iter() {
688                    self.arrangements.remove(&Id::Local(*id));
689                    self.has_future_updates.remove(&Id::Local(*id));
690                }
691                // Return the plan, and any `body` arrangements.
692                //
693                // The body's `b_future` alone can under-report: an earlier binding may only
694                // inherit `has_future_updates` via a Variable to a *later* binding, which the
695                // sequential sweep can't observe at the time the earlier binding is lowered.
696                // A precise fix would require a fixpoint (or the MIR `Analysis` framework with
697                // a `true ⊑ false` lattice). As a cheap correct alternative, OR with the
698                // bindings' future flags: any cross-binding propagation must originate from a
699                // local temporal predicate inside *some* binding, so the OR captures it
700                // without forcing bucketing on a fully non-temporal LetRec.
701                let lir_id = self.allocate_lir_id();
702                LoweredExpr {
703                    plan: LirRelationNode::LetRec {
704                        ids: ids.clone(),
705                        values: lir_values,
706                        limits: limits.clone(),
707                        body: Box::new(body),
708                    }
709                    .as_plan(lir_id),
710                    keys: b_keys,
711                    has_future_updates: b_future || any_v_future,
712                }
713            }
714            MirRelationExpr::FlatMap {
715                input: flat_map_input,
716                func,
717                exprs,
718            } => {
719                // A `FlatMap UnnestList` that comes after the `Reduce` of a window function can be
720                // fused into the lowered `Reduce`.
721                //
722                // In theory, we could have implemented this also as an MIR transform. However, this
723                // is more of a physical optimization, which are sometimes unpleasant to make a part
724                // of the MIR pipeline. The specific problem here with putting this into the MIR
725                // pipeline would be that we'd need to modify MIR's semantics: MIR's Reduce
726                // currently always emits exactly 1 row per group, but the fused Reduce-FlatMap can
727                // emit multiple rows per group. Such semantic changes of MIR are very scary, since
728                // various parts of the optimizer assume that Reduce emits only 1 row per group, and
729                // it would be very hard to hunt down all these parts. (For example, key inference
730                // infers the group key as a unique key.)
731                let fused_with_reduce = 'fusion: {
732                    if !matches!(func, TableFunc::UnnestList { .. }) {
733                        break 'fusion None;
734                    }
735                    // We might have a Project of a single col between the FlatMap and the
736                    // Reduce. (It projects away the grouping keys of the Reduce, and keeps the
737                    // result of the window function.)
738                    let (maybe_reduce, num_grouping_keys) = if let MirRelationExpr::Project {
739                        input: project_input,
740                        outputs: projection,
741                    } = &**flat_map_input
742                    {
743                        // We want this to be a single column, because we'll want to deal with only
744                        // one aggregation in the `Reduce`. (The aggregation of a window function
745                        // always stands alone currently: we plan them separately from other
746                        // aggregations, and Reduces are never fused. When window functions are
747                        // fused with each other, they end up in one aggregation. When there are
748                        // multiple window functions in the same SELECT, but can't be fused, they
749                        // end up in different Reduces.)
750                        if let &[single_col] = &**projection {
751                            (project_input, single_col)
752                        } else {
753                            break 'fusion None;
754                        }
755                    } else {
756                        (flat_map_input, 0)
757                    };
758                    if let MirRelationExpr::Reduce {
759                        input,
760                        group_key,
761                        aggregates,
762                        monotonic,
763                        expected_group_size,
764                    } = &**maybe_reduce
765                    {
766                        if group_key.len() != num_grouping_keys
767                            || aggregates.len() != 1
768                            || !aggregates[0].func.can_fuse_with_unnest_list()
769                        {
770                            break 'fusion None;
771                        }
772                        // At the beginning, `non_fused_mfp_above_flat_map` will be the original MFP
773                        // above the FlatMap. Later, we'll mutate this to be the residual MFP that
774                        // didn't get fused into the `Reduce`.
775                        let non_fused_mfp_above_flat_map = &mut mfp;
776                        let reduce_output_arity = num_grouping_keys + 1;
777                        // We are fusing away the list that the FlatMap would have been unnesting,
778                        // so the column that had that list disappears, so we have to permute the
779                        // MFP above the FlatMap with this column disappearance.
780                        let tweaked_mfp = {
781                            let mut mfp = non_fused_mfp_above_flat_map.clone();
782                            if mfp.demand().contains(&0) {
783                                // I don't think this can happen currently that this MFP would
784                                // refer to the list column, because both the list column and the
785                                // MFP were constructed by the HIR-to-MIR lowering, so it's not just
786                                // some random MFP that we are seeing here. But anyhow, it's better
787                                // to check this here for robustness against future code changes.
788                                break 'fusion None;
789                            }
790                            let permutation: BTreeMap<_, _> =
791                                (1..mfp.input_arity).map(|col| (col, col - 1)).collect();
792                            mfp.permute_fn(|c| permutation[&c], mfp.input_arity - 1);
793                            mfp
794                        };
795                        // We now put together the project that was before the FlatMap, and the
796                        // tweaked version of the MFP that was after the FlatMap.
797                        // (Part of this MFP might be fused into the Reduce.)
798                        let mut project_and_tweaked_mfp = {
799                            let mut mfp = MapFilterProject::new(reduce_output_arity);
800                            mfp = mfp.project(vec![num_grouping_keys]);
801                            mfp = MapFilterProject::compose(mfp, tweaked_mfp);
802                            mfp
803                        };
804                        let fused = self.lower_reduce(
805                            input,
806                            group_key,
807                            aggregates,
808                            monotonic,
809                            expected_group_size,
810                            &mut project_and_tweaked_mfp,
811                            true,
812                        )?;
813                        // Update the residual MFP.
814                        *non_fused_mfp_above_flat_map = project_and_tweaked_mfp;
815                        Some(fused)
816                    } else {
817                        break 'fusion None;
818                    }
819                };
820                if let Some(fused_with_reduce) = fused_with_reduce {
821                    fused_with_reduce
822                } else {
823                    // Couldn't fuse it with a `Reduce`, so lower as a normal `FlatMap`.
824                    let LoweredExpr {
825                        plan: input,
826                        keys,
827                        has_future_updates: input_future,
828                    } = self.lower_mir_expr(flat_map_input)?;
829                    // This stage can absorb arbitrary MFP instances.
830                    let mut mfp = mfp.take();
831                    let mut exprs = exprs.clone();
832                    // Prefer the unarranged collection when present: it presents input columns
833                    // in logical order, so no permutation is required.
834                    let input_key = if keys.raw {
835                        None
836                    } else if let Some((k, permutation, thinning)) = keys.arbitrary_arrangement() {
837                        // Reading from this arrangement exposes input columns in arrangement
838                        // order (key columns followed by thinned value columns). We must
839                        // permute every reference to an input column accordingly: the
840                        // `expr`s feeding the table function arguments, and the `mfp` running
841                        // after the table function (which still references input columns at
842                        // positions `0..input_arity`).
843                        //
844                        // The renderer hands the `mfp` the *whole* arranged row and appends the
845                        // table-function output after it. The arranged row can be wider than the
846                        // logical input row when the key is not a set of distinct columns (an
847                        // expression, functional, or repeated-column key carries extra key
848                        // values). So the table-function output columns at positions
849                        // `input_arity..` must be shifted to land after the arranged row, and the
850                        // `mfp`'s new input arity must reflect the arranged width.
851                        for expr in &mut exprs {
852                            expr.permute(permutation);
853                        }
854                        let input_arity = permutation.len();
855                        let arranged_arity = thinning.len() + k.len();
856                        let output_arity = mfp.input_arity - input_arity;
857                        mfp.permute_fn(
858                            |c| {
859                                if c < input_arity {
860                                    permutation[c]
861                                } else {
862                                    arranged_arity + (c - input_arity)
863                                }
864                            },
865                            arranged_arity + output_arity,
866                        );
867                        Some(k.clone())
868                    } else {
869                        None
870                    };
871
872                    let lir_id = self.allocate_lir_id();
873                    // The absorbed `mfp` may contain temporal predicates, which can
874                    // introduce future-stamped updates that aren't present on the input.
875                    let has_future_updates = input_future || mfp.has_temporal_predicates();
876                    // Return the plan, and no arrangements.
877                    LoweredExpr {
878                        plan: LirRelationNode::FlatMap {
879                            input_key,
880                            input: Box::new(input),
881                            exprs: lses_from_mses(&exprs),
882                            func: func.clone(),
883                            mfp_after: mfp_mir_to_lir_plan(mfp),
884                        }
885                        .as_plan(lir_id),
886                        keys: AvailableCollections::new_raw(),
887                        has_future_updates,
888                    }
889                }
890            }
891            MirRelationExpr::Join {
892                inputs,
893                equivalences,
894                implementation,
895            } => {
896                // Plan each of the join inputs independently.
897                // The `plans` get surfaced upwards, and the `input_keys` should
898                // be used as part of join planning / to validate the existing
899                // plans / to aid in indexed seeding of update streams.
900                let mut plans = Vec::new();
901                let mut input_keys = Vec::new();
902                let mut input_arities = Vec::new();
903                let mut input_futures = Vec::new();
904                for input in inputs.iter() {
905                    let LoweredExpr {
906                        plan,
907                        keys,
908                        has_future_updates: input_future,
909                    } = self.lower_mir_expr(input)?;
910                    input_arities.push(input.arity());
911                    plans.push(plan);
912                    input_keys.push(keys);
913                    input_futures.push(input_future);
914                }
915                let any_input_future = input_futures.iter().any(|&f| f);
916
917                let input_mapper =
918                    JoinInputMapper::new_from_input_arities(input_arities.iter().copied());
919
920                // Extract temporal predicates as joins cannot currently absorb them.
921                let (plan, missing) = match implementation {
922                    IndexedFilter(_coll_id, _idx_id, key, _val) => {
923                        // Start with the constant input. (This used to be important before database-issues#4016
924                        // was fixed.)
925                        let start: usize = 1;
926                        let order = vec![(0usize, key.clone(), None)];
927                        // All columns of the constant input will be part of the arrangement key.
928                        let source_arrangement = (
929                            (0..key.len())
930                                .map(LirScalarExpr::column)
931                                .collect::<Vec<_>>(),
932                            (0..key.len()).collect::<Vec<_>>(),
933                            Vec::<usize>::new(),
934                        );
935                        let (ljp, missing) = LinearJoinPlan::create_from(
936                            start,
937                            Some(&source_arrangement),
938                            equivalences,
939                            &order,
940                            input_mapper,
941                            &mut mfp,
942                            &input_keys,
943                        );
944                        (JoinPlan::Linear(ljp), missing)
945                    }
946                    Differential((start, start_arr, _start_characteristic), order) => {
947                        let source_arrangement = start_arr.as_ref().and_then(|key| {
948                            let key = lses_from_mses(key);
949                            input_keys[*start]
950                                .arranged
951                                .iter()
952                                .find(|(k, _, _)| k == &key)
953                                .clone()
954                        });
955                        let (ljp, missing) = LinearJoinPlan::create_from(
956                            *start,
957                            source_arrangement,
958                            equivalences,
959                            order,
960                            input_mapper,
961                            &mut mfp,
962                            &input_keys,
963                        );
964                        (JoinPlan::Linear(ljp), missing)
965                    }
966                    DeltaQuery(orders) => {
967                        let (djp, missing) = DeltaJoinPlan::create_from(
968                            equivalences,
969                            orders,
970                            input_mapper,
971                            &mut mfp,
972                            &input_keys,
973                        );
974                        (JoinPlan::Delta(djp), missing)
975                    }
976                    // Other plans are errors, and should be reported as such.
977                    Unimplemented => return Err("unimplemented join".to_string()),
978                };
979                // The renderer will expect certain arrangements to exist; if any of those are not available, the join planning functions above should have returned them in
980                // `missing`. We thus need to plan them here so they'll exist.
981                let is_delta = matches!(plan, JoinPlan::Delta(_));
982                for ((((input_plan, input_keys), missing), arity), input_future) in plans
983                    .iter_mut()
984                    .zip_eq(input_keys.iter())
985                    .zip_eq(missing)
986                    .zip_eq(input_arities.iter().cloned())
987                    .zip_eq(input_futures.iter().copied())
988                {
989                    if missing != Default::default() {
990                        if is_delta {
991                            // join_implementation.rs produced a sub-optimal plan here;
992                            // we shouldn't plan delta joins at all if not all of the required
993                            // arrangements are available. Soft panic in CI and log an error in
994                            // production to increase the chances that we will catch all situations
995                            // that violate this constraint.
996                            soft_panic_or_log!("Arrangements depended on by delta join alarmingly absent: {:?}
997Dataflow info: {}
998This is not expected to cause incorrect results, but could indicate a performance issue in Materialize.", missing, self.debug_info);
999                        } else {
1000                            soft_panic_or_log!("Arrangements depended on by a non-delta join are absent: {:?}
1001Dataflow info: {}
1002This is not expected to cause incorrect results, but could indicate a performance issue in Materialize.", missing, self.debug_info);
1003                            // Nowadays MIR transforms take care to insert MIR ArrangeBys for each
1004                            // Join input. (Earlier, they were missing in the following cases:
1005                            //  - They were const-folded away for constant inputs. This is not
1006                            //    happening since
1007                            //    https://github.com/MaterializeInc/materialize/pull/16351
1008                            //  - They were not being inserted for the constant input of
1009                            //    `IndexedFilter`s. This was fixed in
1010                            //    https://github.com/MaterializeInc/materialize/pull/20920
1011                            //  - They were not being inserted for the first input of Differential
1012                            //    joins. This was fixed in
1013                            //    https://github.com/MaterializeInc/materialize/pull/16099)
1014                        }
1015                        let lir_id = self.allocate_lir_id();
1016                        let raw_plan = std::mem::replace(
1017                            input_plan,
1018                            LirRelationNode::Constant {
1019                                rows: Ok(Vec::new()),
1020                            }
1021                            .as_plan(lir_id),
1022                        );
1023                        *input_plan =
1024                            self.arrange_by(raw_plan, missing, input_keys, arity, input_future);
1025                    }
1026                }
1027                // Return the plan, and no arrangements.
1028                // Both linear and delta join planning extract temporal predicates back into the
1029                // residual `mfp` (see `LinearJoinPlan::create_from` / `DeltaJoinPlan::create_from`),
1030                // so the absorbed MFP cannot introduce future updates — the join's output future
1031                // flag is just the OR of its inputs.
1032                let lir_id = self.allocate_lir_id();
1033                LoweredExpr {
1034                    plan: LirRelationNode::Join {
1035                        inputs: plans,
1036                        plan,
1037                    }
1038                    .as_plan(lir_id),
1039                    keys: AvailableCollections::new_raw(),
1040                    has_future_updates: any_input_future,
1041                }
1042            }
1043            MirRelationExpr::Reduce {
1044                input,
1045                group_key,
1046                aggregates,
1047                monotonic,
1048                expected_group_size,
1049            } => {
1050                if aggregates
1051                    .iter()
1052                    .any(|agg| agg.func.can_fuse_with_unnest_list())
1053                {
1054                    // This case should have been handled at the `MirRelationExpr::FlatMap` case
1055                    // above. But that has a pretty complicated pattern matching, so it's not
1056                    // unthinkable that it fails.
1057                    soft_panic_or_log!(
1058                        "Window function performance issue: `reduce_unnest_list_fusion` failed"
1059                    );
1060                }
1061                self.lower_reduce(
1062                    input,
1063                    group_key,
1064                    aggregates,
1065                    monotonic,
1066                    expected_group_size,
1067                    &mut mfp,
1068                    false,
1069                )?
1070            }
1071            MirRelationExpr::TopK {
1072                input,
1073                group_key,
1074                order_key,
1075                limit,
1076                offset,
1077                monotonic,
1078                expected_group_size,
1079            } => {
1080                let arity = input.arity();
1081                let LoweredExpr {
1082                    plan: input,
1083                    keys,
1084                    has_future_updates: input_future,
1085                } = self.lower_mir_expr(input)?;
1086
1087                let mut top_k_plan = TopKPlan::create_from(
1088                    group_key.clone(),
1089                    order_key.clone(),
1090                    *offset,
1091                    limit
1092                        .as_ref()
1093                        .map(|limit| LirScalarExpr::try_from(limit).expect("lowerable MIR")),
1094                    arity,
1095                    *monotonic,
1096                    *expected_group_size,
1097                );
1098
1099                // For single-time dataflows, upgrade to the monotonic variant with
1100                // mandatory consolidation. `refine_single_time_consolidation` later
1101                // relaxes `must_consolidate` where the input is physically monotonic.
1102                if self.single_time {
1103                    top_k_plan.as_monotonic(true);
1104                }
1105
1106                // We don't have an MFP here -- install an operator to permute the
1107                // input, if necessary.
1108                let input = if !keys.raw {
1109                    self.arrange_by(
1110                        input,
1111                        AvailableCollections::new_raw(),
1112                        &keys,
1113                        arity,
1114                        // `new_raw` means no arrangement, so no bucketing is needed
1115                        false,
1116                    )
1117                } else {
1118                    input
1119                };
1120                // Return the plan, and the keys it produces. `MonotonicTop1` arranges its
1121                // output by the group key (see `render_top1_monotonic`), so a downstream
1122                // consumer keyed the same way can reuse that arrangement instead of forcing
1123                // another `ArrangeBy`.
1124                let out_keys = match &top_k_plan {
1125                    TopKPlan::MonotonicTop1(_) => {
1126                        let key = group_key
1127                            .iter()
1128                            .map(|c| LirScalarExpr::column(*c))
1129                            .collect::<Vec<_>>();
1130                        let (permutation, thinning) = permutation_for_arrangement(&key, arity);
1131                        AvailableCollections::new_arranged(vec![(key, permutation, thinning)])
1132                    }
1133                    // MonotonicTopK / Basic key their arrangements by (hash, group_key), which is
1134                    // not reusable by a group-key consumer, so they advertise no arrangement.
1135                    TopKPlan::MonotonicTopK(_) | TopKPlan::Basic(_) => {
1136                        AvailableCollections::new_raw()
1137                    }
1138                };
1139                let temporal_bucketing_strategy = strategy_from_future(input_future);
1140                let lir_id = self.allocate_lir_id();
1141                LoweredExpr {
1142                    plan: LirRelationNode::TopK {
1143                        input: Box::new(input),
1144                        top_k_plan,
1145                        temporal_bucketing_strategy,
1146                    }
1147                    .as_plan(lir_id),
1148                    keys: out_keys,
1149                    has_future_updates: false,
1150                }
1151            }
1152            MirRelationExpr::Negate { input } => {
1153                let arity = input.arity();
1154                let LoweredExpr {
1155                    plan: input,
1156                    keys,
1157                    has_future_updates: input_future,
1158                } = self.lower_mir_expr(input)?;
1159
1160                // We don't have an MFP here -- install an operator to permute the
1161                // input, if necessary.
1162                let input = if !keys.raw {
1163                    self.arrange_by(
1164                        input,
1165                        AvailableCollections::new_raw(),
1166                        &keys,
1167                        arity,
1168                        // `new_raw` means no arrangement, so no bucketing is needed
1169                        false,
1170                    )
1171                } else {
1172                    input
1173                };
1174                // Return the plan, and no arrangements.
1175                let lir_id = self.allocate_lir_id();
1176                LoweredExpr {
1177                    plan: LirRelationNode::Negate {
1178                        input: Box::new(input),
1179                    }
1180                    .as_plan(lir_id),
1181                    keys: AvailableCollections::new_raw(),
1182                    has_future_updates: input_future,
1183                }
1184            }
1185            MirRelationExpr::Threshold { input } => {
1186                let LoweredExpr {
1187                    plan,
1188                    keys,
1189                    has_future_updates: input_future,
1190                } = self.lower_mir_expr(input)?;
1191                let arity = input.arity();
1192                let (threshold_plan, required_arrangement) = ThresholdPlan::create_from(arity);
1193
1194                let plan = if !keys
1195                    .arranged
1196                    .iter()
1197                    .any(|(key, _, _)| key == &required_arrangement.0)
1198                {
1199                    self.arrange_by(
1200                        plan,
1201                        AvailableCollections::new_arranged(vec![required_arrangement]),
1202                        &keys,
1203                        arity,
1204                        input_future,
1205                    )
1206                } else {
1207                    plan
1208                };
1209
1210                let output_keys = threshold_plan.keys();
1211                // Return the plan, and any produced keys.
1212                let lir_id = self.allocate_lir_id();
1213                LoweredExpr {
1214                    plan: LirRelationNode::Threshold {
1215                        input: Box::new(plan),
1216                        threshold_plan,
1217                    }
1218                    .as_plan(lir_id),
1219                    keys: output_keys,
1220                    // Threshold builds its own output arrangement whose
1221                    // MergeBatcher absorbs future-stamped updates, so no
1222                    // future updates flow out.
1223                    has_future_updates: false,
1224                }
1225            }
1226            MirRelationExpr::Union { base, inputs } => {
1227                let arity = base.arity();
1228                let mut lowered_inputs = Vec::with_capacity(1 + inputs.len());
1229                lowered_inputs.push(self.lower_mir_expr(base)?);
1230                for input in inputs.iter() {
1231                    lowered_inputs.push(self.lower_mir_expr(input)?);
1232                }
1233
1234                // A Union with any `Negate` input should consolidate its
1235                // output. The lowering is the only place where this decision
1236                // can be coupled with the per-input bucketing strategy.
1237                let consolidate_output = lowered_inputs
1238                    .iter()
1239                    .any(|l| matches!(l.plan.node, LirRelationNode::Negate { .. }));
1240
1241                // Per-input bucketing strategies: only meaningful when the
1242                // Union consolidates its output, since bucketing only pays off
1243                // ahead of a downstream consolidator.
1244                let temporal_bucketing_strategies: Vec<ArrangementStrategy> = if consolidate_output
1245                {
1246                    lowered_inputs
1247                        .iter()
1248                        .map(|l| strategy_from_future(l.has_future_updates))
1249                        .collect()
1250                } else {
1251                    lowered_inputs
1252                        .iter()
1253                        .map(|_| ArrangementStrategy::Direct)
1254                        .collect()
1255                };
1256
1257                let has_future_updates = if consolidate_output {
1258                    // The MergeBatcher will hold back future updates (regardless of whether we are
1259                    // bucketing here or not).
1260                    false
1261                } else {
1262                    lowered_inputs.iter().any(|l| l.has_future_updates)
1263                };
1264
1265                let plans = lowered_inputs
1266                    .into_iter()
1267                    .map(
1268                        |LoweredExpr {
1269                             plan,
1270                             keys,
1271                             has_future_updates: _,
1272                         }| {
1273                            // We don't have an MFP here -- install an operator to permute the
1274                            // input, if necessary.
1275                            if !keys.raw {
1276                                self.arrange_by(
1277                                    plan,
1278                                    AvailableCollections::new_raw(),
1279                                    &keys,
1280                                    arity,
1281                                    // `new_raw` means no arrangement, so no bucketing is needed
1282                                    false,
1283                                )
1284                            } else {
1285                                plan
1286                            }
1287                        },
1288                    )
1289                    .collect();
1290                // Return the plan and no arrangements.
1291                let lir_id = self.allocate_lir_id();
1292                LoweredExpr {
1293                    plan: LirRelationNode::Union {
1294                        inputs: plans,
1295                        consolidate_output,
1296                        temporal_bucketing_strategies,
1297                    }
1298                    .as_plan(lir_id),
1299                    keys: AvailableCollections::new_raw(),
1300                    has_future_updates,
1301                }
1302            }
1303            MirRelationExpr::ArrangeBy { input, keys } => {
1304                let input_mir = input;
1305                let LoweredExpr {
1306                    plan: input,
1307                    keys: mut input_keys,
1308                    has_future_updates: input_has_future_updates,
1309                } = self.lower_mir_expr(input)?;
1310                // Fill the `types` in `input_keys` if not already present.
1311                let arity = input_mir.arity();
1312
1313                // Determine keys that are not present in `input_keys`.
1314                let new_keys = keys
1315                    .iter()
1316                    .filter(|k1| {
1317                        !input_keys.arranged.iter().any(|(k2, _, _)| {
1318                            k1.len() == k2.len()
1319                                && k1
1320                                    .iter()
1321                                    .zip_eq(k2)
1322                                    .all(|(e1, e2)| *e1 == MirScalarExpr::from(e2))
1323                        })
1324                    })
1325                    .cloned()
1326                    .collect::<Vec<_>>();
1327                if new_keys.is_empty() {
1328                    LoweredExpr {
1329                        plan: input,
1330                        keys: input_keys,
1331                        has_future_updates: input_has_future_updates,
1332                    }
1333                } else {
1334                    let mut new_keys = new_keys
1335                        .iter()
1336                        .map(|k| {
1337                            let k = lses_from_mses(k);
1338                            let (permutation, thinning) = permutation_for_arrangement(&k, arity);
1339                            (k, permutation, thinning)
1340                        })
1341                        .collect::<Vec<_>>();
1342                    let forms = AvailableCollections {
1343                        raw: input_keys.raw,
1344                        arranged: new_keys.clone(),
1345                    };
1346                    let (input_key, input_mfp) = if let Some((input_key, permutation, thinning)) =
1347                        input_keys.arbitrary_arrangement()
1348                    {
1349                        let mut mfp = MapFilterProject::new(arity);
1350                        mfp.permute_fn(|c| permutation[c], thinning.len() + input_key.len());
1351                        (Some(input_key.clone()), mfp)
1352                    } else {
1353                        (None, MapFilterProject::new(arity))
1354                    };
1355                    input_keys.arranged.append(&mut new_keys);
1356                    input_keys.arranged.sort_by(|k1, k2| k1.0.cmp(&k2.0));
1357
1358                    // Return the plan and extended keys.
1359                    let lir_id = self.allocate_lir_id();
1360                    let strategy = strategy_from_future(input_has_future_updates);
1361                    assert!(!forms.arranged.is_empty()); // i.e., we do build an arrangement
1362                    let has_future_updates = false;
1363                    LoweredExpr {
1364                        plan: LirRelationNode::ArrangeBy {
1365                            input_key,
1366                            input: Box::new(input),
1367                            input_mfp: mfp_mir_to_lir_plan(input_mfp),
1368                            forms,
1369                            strategy,
1370                        }
1371                        .as_plan(lir_id),
1372                        keys: input_keys,
1373                        has_future_updates,
1374                    }
1375                }
1376            }
1377        };
1378
1379        // If the plan stage did not absorb all linear operators, introduce a new stage to implement them.
1380        if !mfp.is_identity() {
1381            // Check if this MFP introduces future updates.
1382            let mfp_is_temporal = mfp.has_temporal_predicates();
1383            has_future_updates = has_future_updates || mfp_is_temporal;
1384            // Seek out an arrangement key that might be constrained to a literal.
1385            // TODO: Improve key selection heuristic.
1386            let key_val = keys
1387                .arranged
1388                .iter()
1389                .filter_map(|(key, permutation, thinning)| {
1390                    // `key` refers to input columns, so `mfp` must not be permuted yet.
1391                    mfp.literal_constraints(&key.iter().map(MirScalarExpr::from).collect_vec())
1392                        .map(|val| {
1393                            if let Some(metrics) = &self.metrics {
1394                                metrics.inc_literal_constraints("mfp");
1395                            }
1396                            (key.clone(), permutation, thinning, val)
1397                        })
1398                })
1399                .max_by_key(|(key, _, _, _)| key.len());
1400
1401            // Input key selection strategy:
1402            // (1) If we can read a key at a particular value, do so
1403            // (2) Otherwise, if there is a key that causes the MFP to be the identity, and
1404            // therefore allows us to avoid discarding the arrangement, use that.
1405            // (3) Otherwise, if there is _some_ key, use that,
1406            // (4) Otherwise just read the raw collection.
1407            let input_key_val = if let Some((key, permutation, thinning, val)) = key_val {
1408                mfp.permute_fn(|c| permutation[c], thinning.len() + key.len());
1409
1410                Some((key, Some(val)))
1411            } else if let Some((key, permutation, thinning)) =
1412                keys.arranged.iter().find(|(key, permutation, thinning)| {
1413                    let mut mfp = mfp.clone();
1414                    mfp.permute_fn(|c| permutation[c], thinning.len() + key.len());
1415                    mfp.is_identity()
1416                })
1417            {
1418                mfp.permute_fn(|c| permutation[c], thinning.len() + key.len());
1419                Some((key.clone(), None))
1420            } else if let Some((key, permutation, thinning)) = keys.arbitrary_arrangement() {
1421                mfp.permute_fn(|c| permutation[c], thinning.len() + key.len());
1422                Some((key.clone(), None))
1423            } else {
1424                None
1425            };
1426
1427            if mfp.is_identity() {
1428                // We have discovered a key
1429                // whose permutation causes the MFP to actually
1430                // be the identity! We can keep it around,
1431                // but without its permutation this time,
1432                // and with a trivial thinning of the right length.
1433                let (key, val) = input_key_val.unwrap();
1434                let (_old_key, old_permutation, old_thinning) = keys
1435                    .arranged
1436                    .iter_mut()
1437                    .find(|(key2, _, _)| key2 == &key)
1438                    .unwrap();
1439                *old_permutation = (0..mfp.input_arity).collect();
1440                let old_thinned_arity = old_thinning.len();
1441                *old_thinning = (0..old_thinned_arity).collect();
1442                // Get rid of all other forms, as this is now the only one known to be valid.
1443                // TODO[btv] we can probably save the other arrangements too, if we adjust their permutations.
1444                // This is not hard to do, but leaving it for a quick follow-up to avoid making the present diff too unwieldy.
1445                keys.arranged.retain(|(key2, _, _)| key2 == &key);
1446                keys.raw = false;
1447
1448                // Creating a LirRelationExpr::Mfp node is now logically unnecessary, but we
1449                // should do so anyway when `val` is populated, so that
1450                // the `key_val` optimization gets applied.
1451                let lir_id = self.allocate_lir_id();
1452                if val.is_some() {
1453                    plan = LirRelationNode::Mfp {
1454                        input: Box::new(plan),
1455                        mfp: mfp_mir_to_lir_plan(mfp),
1456                        input_key_val: Some((key.clone(), val.map(StableRow))),
1457                    }
1458                    .as_plan(lir_id)
1459                }
1460            } else {
1461                let lir_id = self.allocate_lir_id();
1462                plan = LirRelationNode::Mfp {
1463                    input: Box::new(plan),
1464                    mfp: mfp_mir_to_lir_plan(mfp),
1465                    input_key_val: input_key_val.map(|(key, val)| (key, val.map(StableRow))),
1466                }
1467                .as_plan(lir_id);
1468                keys = AvailableCollections::new_raw();
1469            }
1470        }
1471
1472        Ok(LoweredExpr {
1473            plan,
1474            keys,
1475            has_future_updates,
1476        })
1477    }
1478
1479    /// Lowers a `Reduce` with the given fields and an `mfp_on_top`, which is the MFP that is
1480    /// originally on top of the `Reduce`. This MFP, or a part of it, might be fused into the
1481    /// `Reduce`, in which case `mfp_on_top` is mutated to be the residual MFP, i.e., what was not
1482    /// fused.
1483    fn lower_reduce(
1484        &mut self,
1485        input: &MirRelationExpr,
1486        group_key: &Vec<MirScalarExpr>,
1487        aggregates: &Vec<AggregateExpr>,
1488        monotonic: &bool,
1489        expected_group_size: &Option<u64>,
1490        mfp_on_top: &mut MapFilterProject,
1491        fused_unnest_list: bool,
1492    ) -> Result<LoweredExpr, String> {
1493        let input_arity = input.arity();
1494        let LoweredExpr {
1495            plan: input,
1496            keys,
1497            has_future_updates: input_future,
1498        } = self.lower_mir_expr(input)?;
1499        let (input_key, permutation_and_new_arity) =
1500            if let Some((input_key, permutation, thinning)) = keys.arbitrary_arrangement() {
1501                (
1502                    Some(input_key.clone()),
1503                    Some((permutation.clone(), thinning.len() + input_key.len())),
1504                )
1505            } else {
1506                (None, None)
1507            };
1508        let key_val_plan = KeyValPlan::new(
1509            input_arity,
1510            group_key,
1511            aggregates,
1512            permutation_and_new_arity,
1513        );
1514        let mut reduce_plan = ReducePlan::create_from(
1515            aggregates.clone(),
1516            *monotonic,
1517            *expected_group_size,
1518            fused_unnest_list,
1519        );
1520
1521        // For single-time dataflows, upgrade a hierarchical reduce to its monotonic
1522        // variant with mandatory consolidation. `refine_single_time_consolidation`
1523        // later relaxes `must_consolidate` where the input is physically monotonic.
1524        // Selecting the variant before computing `keys` below keeps the advertised
1525        // `AvailableCollections` consistent with the final plan. `Reduce::keys()` is
1526        // the same for every hierarchical sub-variant, so the advertisement is in fact
1527        // identical either way.
1528        if self.single_time {
1529            if let ReducePlan::Hierarchical(hierarchical) = &mut reduce_plan {
1530                hierarchical.as_monotonic(true);
1531            }
1532        }
1533
1534        // Return the plan, and the keys it produces.
1535        let mfp_after;
1536        let output_arity;
1537        if self.enable_reduce_mfp_fusion {
1538            (mfp_after, *mfp_on_top, output_arity) =
1539                reduce_plan.extract_mfp_after(mfp_on_top.clone(), group_key.len());
1540        } else {
1541            (mfp_after, output_arity) = (
1542                MapFilterProject::new(mfp_on_top.input_arity),
1543                group_key.len() + aggregates.len(),
1544            );
1545        }
1546        soft_assert_eq_or_log!(
1547            mfp_on_top.input_arity,
1548            output_arity,
1549            "Output arity of reduce must match input arity for MFP on top of it"
1550        );
1551        let output_keys = reduce_plan.keys(group_key.len(), output_arity);
1552        let lir_id = self.allocate_lir_id();
1553        // `Reduce` builds its own input arrangement inside `render_reduce` (via `KeyValPlan`),
1554        // bypassing `ensure_collections`. So we can't piggy-back on an upstream `ArrangeBy`'s
1555        // strategy to request temporal bucketing on a temporal-MFP-fed input: there is no such
1556        // `ArrangeBy`. Instead we record the strategy directly on the `Reduce` node, and
1557        // `render_reduce` applies bucketing to the keyed `(key, val)` stream itself.
1558        let temporal_bucketing_strategy = strategy_from_future(input_future);
1559        // (This can't currently happen due to `extract_mfp_after` separating out any temporal part.)
1560        let has_future_updates = mfp_after.has_temporal_predicates();
1561        Ok(LoweredExpr {
1562            plan: LirRelationNode::Reduce {
1563                input_key,
1564                input: Box::new(input),
1565                key_val_plan,
1566                plan: reduce_plan,
1567                mfp_after: SafeMfpPlan::from_mfp(mfp_mir_to_lir(mfp_after)),
1568                temporal_bucketing_strategy,
1569            }
1570            .as_plan(lir_id),
1571            keys: output_keys,
1572            has_future_updates,
1573        })
1574    }
1575
1576    /// Replace the plan with another one
1577    /// that has the collection in some additional forms.
1578    pub fn arrange_by(
1579        &mut self,
1580        plan: LirRelationExpr,
1581        collections: AvailableCollections,
1582        old_collections: &AvailableCollections,
1583        arity: usize,
1584        has_future_updates: bool,
1585    ) -> LirRelationExpr {
1586        if let LirRelationExpr {
1587            node:
1588                LirRelationNode::ArrangeBy {
1589                    input_key,
1590                    input,
1591                    input_mfp,
1592                    mut forms,
1593                    strategy,
1594                },
1595            lir_id,
1596        } = plan
1597        {
1598            forms.raw |= collections.raw;
1599            forms.arranged.extend(collections.arranged);
1600            forms.arranged.sort_by(|k1, k2| k1.0.cmp(&k2.0));
1601            forms.arranged.dedup_by(|k1, k2| k1.0 == k2.0);
1602            LirRelationNode::ArrangeBy {
1603                input_key,
1604                input,
1605                input_mfp,
1606                forms,
1607                strategy,
1608            }
1609            .as_plan(lir_id)
1610        } else {
1611            let (input_key, input_mfp) = if let Some((input_key, permutation, thinning)) =
1612                old_collections.arbitrary_arrangement()
1613            {
1614                let mut mfp = MapFilterProject::new(arity);
1615                mfp.permute_fn(|c| permutation[c], thinning.len() + input_key.len());
1616                (Some(input_key.clone()), mfp)
1617            } else {
1618                (None, MapFilterProject::new(arity))
1619            };
1620            let lir_id = self.allocate_lir_id();
1621
1622            LirRelationNode::ArrangeBy {
1623                input_key,
1624                input: Box::new(plan),
1625                input_mfp: mfp_mir_to_lir_plan(input_mfp),
1626                forms: collections,
1627                strategy: strategy_from_future(has_future_updates),
1628            }
1629            .as_plan(lir_id)
1630        }
1631    }
1632}
1633
1634/// Various bits of state to print along with error messages during LIR planning,
1635/// to aid debugging.
1636#[derive(Clone, Debug)]
1637pub struct LirDebugInfo {
1638    debug_name: String,
1639    id: GlobalId,
1640}
1641
1642impl std::fmt::Display for LirDebugInfo {
1643    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1644        write!(f, "Debug name: {}; id: {}", self.debug_name, self.id)
1645    }
1646}