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}