Skip to main content

mz_transform/
dataflow.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//! Whole-dataflow optimization
11//!
12//! A dataflow may contain multiple views, each of which may only be
13//! optimized locally. However, information like demand and predicate
14//! pushdown can be applied across views once we understand the context
15//! in which the views will be executed.
16
17use std::collections::{BTreeMap, BTreeSet};
18
19use itertools::Itertools;
20use mz_compute_types::dataflows::{BuildDesc, DataflowDesc, DataflowDescription, IndexImport};
21use mz_expr::{
22    AccessStrategy, CollectionPlan, Id, JoinImplementation, LocalId, MapFilterProject,
23    MirRelationExpr, MirScalarExpr, RECURSION_LIMIT,
24};
25use mz_ore::stack::{CheckedRecursion, RecursionGuard, RecursionLimitError};
26use mz_ore::{assert_none, soft_assert_eq_or_log, soft_assert_or_log, soft_panic_or_log};
27use mz_repr::GlobalId;
28use mz_repr::explain::{DeltaJoinIndexUsageType, IndexUsageType, UsedIndexes};
29#[cfg(any(test, feature = "proptest"))]
30use proptest_derive::Arbitrary;
31use serde::{Deserialize, Serialize};
32
33use crate::monotonic::MonotonicFlag;
34use crate::notice::RawOptimizerNotice;
35use crate::{IndexOracle, Optimizer, TransformCtx, TransformError};
36
37/// Optimizes the implementation of each dataflow.
38///
39/// Inlines views, performs a full optimization pass including physical
40/// planning using the supplied indexes, propagates filtering and projection
41/// information to dataflow sources and lifts monotonicity information.
42#[mz_ore::instrument(
43    target = "optimizer",
44    level = "debug",
45    fields(path.segment ="global")
46)]
47pub fn optimize_dataflow(
48    dataflow: &mut DataflowDesc,
49    transform_ctx: &mut TransformCtx,
50    fast_path_optimizer: bool,
51) -> Result<(), TransformError> {
52    // Inline views that are used in only one other view.
53    inline_views(dataflow)?;
54
55    if fast_path_optimizer {
56        optimize_dataflow_relations(
57            dataflow,
58            &Optimizer::fast_path_optimizer(transform_ctx),
59            transform_ctx,
60        )?;
61    } else {
62        // Logical optimization pass after view inlining
63        optimize_dataflow_relations(
64            dataflow,
65            #[allow(deprecated)]
66            &Optimizer::logical_optimizer(transform_ctx),
67            transform_ctx,
68        )?;
69
70        optimize_dataflow_filters(dataflow)?;
71        // TODO: when the linear operator contract ensures that propagated
72        // predicates are always applied, projections and filters can be removed
73        // from where they come from. Once projections and filters can be removed,
74        // TODO: it would be useful for demand to be optimized after filters
75        // that way demand only includes the columns that are still necessary after
76        // the filters are applied.
77        optimize_dataflow_demand(dataflow)?;
78
79        // A smaller logical optimization pass after projections and filters are
80        // pushed down across views.
81        optimize_dataflow_relations(
82            dataflow,
83            &Optimizer::logical_cleanup_pass(transform_ctx, false),
84            transform_ctx,
85        )?;
86
87        // Physical optimization pass
88        optimize_dataflow_relations(
89            dataflow,
90            &Optimizer::physical_optimizer(transform_ctx),
91            transform_ctx,
92        )?;
93
94        optimize_dataflow_monotonic(dataflow, transform_ctx)?;
95    }
96
97    prune_and_annotate_dataflow_index_imports(
98        dataflow,
99        transform_ctx.indexes,
100        transform_ctx.df_meta,
101    )?;
102
103    prune_dataflow_source_imports(dataflow);
104
105    // Warning: If you want to add a transform call here, consider it very carefully whether it
106    // could accidentally invalidate information that we already derived above in
107    // `optimize_dataflow_monotonic`, `prune_and_annotate_dataflow_index_imports`, or
108    // `prune_dataflow_source_imports`. A transform here that drops the last reference to an import
109    // puts back exactly the discrepancy the two prunes just removed.
110
111    mz_repr::explain::trace_plan(dataflow);
112
113    Ok(())
114}
115
116/// Inline views used in one other view, and in no exported objects.
117#[mz_ore::instrument(
118    target = "optimizer",
119    level = "debug",
120    fields(path.segment = "inline_views")
121)]
122fn inline_views(dataflow: &mut DataflowDesc) -> Result<(), TransformError> {
123    // We cannot inline anything whose `BuildDesc::id` appears in either the
124    // `index_exports` or `sink_exports` of `dataflow`, because we lose our
125    // ability to name it.
126
127    // A view can / should be in-lined in another view if it is only used by
128    // one subsequent view. If there are two distinct views that have not
129    // themselves been merged, then too bad and it doesn't get inlined.
130
131    // Starting from the *last* object to build, walk backwards and inline
132    // any view that is neither referenced by a `index_exports` nor
133    // `sink_exports` nor more than two remaining objects to build.
134
135    for index in (0..dataflow.objects_to_build.len()).rev() {
136        // Capture the name used by others to reference this view.
137        let global_id = dataflow.objects_to_build[index].id;
138        // Determine if any exports directly reference this view.
139        let mut occurs_in_export = false;
140        for (_gid, sink_desc) in dataflow.sink_exports.iter() {
141            if sink_desc.from == global_id {
142                occurs_in_export = true;
143            }
144        }
145        for (_, (index_desc, _)) in dataflow.index_exports.iter() {
146            if index_desc.on_id == global_id {
147                occurs_in_export = true;
148            }
149        }
150        // Count the number of subsequent views that reference this view.
151        let mut occurrences_in_later_views = Vec::new();
152        for other in (index + 1)..dataflow.objects_to_build.len() {
153            if dataflow.objects_to_build[other]
154                .plan
155                .depends_on()
156                .contains(&global_id)
157            {
158                occurrences_in_later_views.push(other);
159            }
160        }
161        // Inline if the view is referenced in one view and no exports.
162        if !occurs_in_export && occurrences_in_later_views.len() == 1 {
163            let other = occurrences_in_later_views[0];
164            // We can remove this view and insert it in the later view,
165            // but are not able to relocate the later view `other`.
166
167            // When splicing in the `index` view, we need to create disjoint
168            // identifiers for the Let's `body` and `value`, as well as a new
169            // identifier for the binding itself. Following `NormalizeLets`, we
170            // go with the binding first, then the value, then the body.
171            let mut id_gen = crate::IdGen::default();
172            let new_local = LocalId::new(id_gen.allocate_id());
173            // Use the same `id_gen` to assign new identifiers to `index`.
174            crate::normalize_lets::renumber_bindings(
175                dataflow.objects_to_build[index].plan.as_inner_mut(),
176                &mut id_gen,
177            )?;
178            // Assign new identifiers to the other relation.
179            crate::normalize_lets::renumber_bindings(
180                dataflow.objects_to_build[other].plan.as_inner_mut(),
181                &mut id_gen,
182            )?;
183            // Install the `new_local` name wherever `global_id` was used.
184            dataflow.objects_to_build[other]
185                .plan
186                .as_inner_mut()
187                .visit_pre_mut(|expr| {
188                    if let MirRelationExpr::Get { id, .. } = expr {
189                        if id == &Id::Global(global_id) {
190                            *id = Id::Local(new_local);
191                        }
192                    }
193                });
194
195            // With identifiers rewritten, we can replace `other` with
196            // a `MirRelationExpr::Let` binding, whose value is `index` and
197            // whose body is `other`.
198            let body = dataflow.objects_to_build[other]
199                .plan
200                .as_inner_mut()
201                .take_dangerous();
202            let value = dataflow.objects_to_build[index]
203                .plan
204                .as_inner_mut()
205                .take_dangerous();
206            *dataflow.objects_to_build[other].plan.as_inner_mut() = MirRelationExpr::Let {
207                id: new_local,
208                value: Box::new(value),
209                body: Box::new(body),
210            };
211            dataflow.objects_to_build.remove(index);
212        }
213    }
214
215    mz_repr::explain::trace_plan(dataflow);
216
217    Ok(())
218}
219
220/// Performs either the logical or the physical optimization pass on the
221/// dataflow using the supplied set of indexes.
222#[mz_ore::instrument(
223    target = "optimizer",
224    level = "debug",
225    fields(path.segment = optimizer.name)
226)]
227fn optimize_dataflow_relations(
228    dataflow: &mut DataflowDesc,
229    optimizer: &Optimizer,
230    ctx: &mut TransformCtx,
231) -> Result<(), TransformError> {
232    // Re-optimize each dataflow
233    for object in dataflow.objects_to_build.iter_mut() {
234        // Re-run all optimizations on the composite views.
235        ctx.set_global_id(object.id);
236        optimizer.transform(object.plan.as_inner_mut(), ctx)?;
237        ctx.reset_global_id();
238    }
239
240    mz_repr::explain::trace_plan(dataflow);
241
242    Ok(())
243}
244
245/// Pushes demand information from published outputs to dataflow inputs,
246/// projecting away unnecessary columns.
247///
248/// Dataflows that exist for the sake of generating plan explanations do not
249/// have published outputs. In this case, we push demand information from views
250/// not depended on by other views to dataflow inputs.
251#[mz_ore::instrument(
252    target = "optimizer",
253    level = "debug",
254    fields(path.segment ="demand")
255)]
256fn optimize_dataflow_demand(dataflow: &mut DataflowDesc) -> Result<(), TransformError> {
257    // Maps id -> union of known columns demanded from the source/view with the
258    // corresponding id.
259    let mut demand = BTreeMap::new();
260
261    if dataflow.index_exports.is_empty() && dataflow.sink_exports.is_empty() {
262        // In the absence of any exports, just demand all columns from views
263        // that are not depended on by another view, which is currently the last
264        // object in `objects_to_build`.
265
266        // A DataflowDesc without exports is currently created in the context of
267        // EXPLAIN outputs. This ensures that the output has all the columns of
268        // the original explainee.
269        if let Some(build_desc) = dataflow.objects_to_build.iter_mut().rev().next() {
270            demand
271                .entry(Id::Global(build_desc.id))
272                .or_insert_with(BTreeSet::new)
273                .extend(0..build_desc.plan.as_inner_mut().arity());
274        }
275    } else {
276        // Demand all columns of inputs to sinks.
277        for (_id, sink) in dataflow.sink_exports.iter() {
278            let input_id = sink.from;
279            demand
280                .entry(Id::Global(input_id))
281                .or_insert_with(BTreeSet::new)
282                .extend(0..dataflow.arity_of(&input_id));
283        }
284
285        // Demand all columns of inputs to exported indexes.
286        for (_id, (desc, _typ)) in dataflow.index_exports.iter() {
287            let input_id = desc.on_id;
288            demand
289                .entry(Id::Global(input_id))
290                .or_insert_with(BTreeSet::new)
291                .extend(0..dataflow.arity_of(&input_id));
292        }
293    }
294
295    optimize_dataflow_demand_inner(
296        dataflow
297            .objects_to_build
298            .iter_mut()
299            .rev()
300            .map(|build_desc| (Id::Global(build_desc.id), build_desc.plan.as_inner_mut())),
301        &mut demand,
302    )?;
303
304    mz_repr::explain::trace_plan(dataflow);
305
306    Ok(())
307}
308
309/// Pushes demand through views in `view_sequence` in order, removing
310/// columns not demanded.
311///
312/// This method is made public for the sake of testing.
313/// TODO: make this private once we allow multiple exports per dataflow.
314pub fn optimize_dataflow_demand_inner<'a, I>(
315    view_sequence: I,
316    demand: &mut BTreeMap<Id, BTreeSet<usize>>,
317) -> Result<(), TransformError>
318where
319    I: Iterator<Item = (Id, &'a mut MirRelationExpr)>,
320{
321    // Maps id -> The projection that was pushed down on the view with the
322    // corresponding id.
323    let mut applied_projection = BTreeMap::new();
324    // Collect the mutable references to views after pushing projection down
325    // in order to run cleanup actions on them in a second loop.
326    let mut view_refs = Vec::new();
327    let projection_pushdown = crate::movement::ProjectionPushdown::default();
328    for (id, view) in view_sequence {
329        if let Some(columns) = demand.get(&id) {
330            let projection_pushed_down = columns.iter().map(|c| *c).collect();
331            // Push down the projection consisting of the entries of `columns`
332            // in increasing order.
333            projection_pushdown.action(view, &projection_pushed_down, demand)?;
334            let new_type = view.typ();
335            applied_projection.insert(id, (projection_pushed_down, new_type));
336        }
337        view_refs.push(view);
338    }
339
340    for view in view_refs {
341        // Update `Get` nodes to reflect any columns that have been projected away.
342        projection_pushdown.update_projection_around_get(view, &applied_projection);
343    }
344
345    Ok(())
346}
347
348/// Pushes predicate to dataflow inputs.
349#[mz_ore::instrument(
350    target = "optimizer",
351    level = "debug",
352    fields(path.segment ="filters")
353)]
354fn optimize_dataflow_filters(dataflow: &mut DataflowDesc) -> Result<(), TransformError> {
355    // Contains id -> predicates map, describing those predicates that
356    // can (but need not) be applied to the collection named by `id`.
357    let mut predicates = BTreeMap::<Id, BTreeSet<mz_expr::MirScalarExpr>>::new();
358
359    // Propagate predicate information from outputs to inputs.
360    optimize_dataflow_filters_inner(
361        dataflow
362            .objects_to_build
363            .iter_mut()
364            .rev()
365            .map(|build_desc| (Id::Global(build_desc.id), build_desc.plan.as_inner_mut())),
366        &mut predicates,
367    )?;
368
369    // Push predicate information into the SourceDesc.
370    for (source_id, source_import) in dataflow.source_imports.iter_mut() {
371        let source = &mut source_import.desc;
372        if let Some(list) = predicates.remove(&Id::Global(*source_id)) {
373            if !list.is_empty() {
374                // Canonicalize the order of predicates, for stable plans.
375                let mut list = list.into_iter().collect::<Vec<_>>();
376                list.sort();
377                // Install no-op predicate information if none exists.
378                if source.arguments.operators.is_none() {
379                    source.arguments.operators = Some(MapFilterProject::new(source.typ.arity()));
380                }
381                // Add any predicates that can be pushed to the source.
382                if let Some(operator) = source.arguments.operators.take() {
383                    source.arguments.operators = Some(operator.filter(list));
384                    source.arguments.operators.as_mut().map(|x| x.optimize());
385                }
386            }
387        }
388    }
389
390    mz_repr::explain::trace_plan(dataflow);
391
392    Ok(())
393}
394
395/// Pushes filters down through views in `view_sequence` in order.
396///
397/// This method is made public for the sake of testing.
398/// TODO: make this private once we allow multiple exports per dataflow.
399pub fn optimize_dataflow_filters_inner<'a, I>(
400    view_iter: I,
401    predicates: &mut BTreeMap<Id, BTreeSet<mz_expr::MirScalarExpr>>,
402) -> Result<(), TransformError>
403where
404    I: Iterator<Item = (Id, &'a mut MirRelationExpr)>,
405{
406    let transform = crate::predicate_pushdown::PredicatePushdown::default();
407    for (id, view) in view_iter {
408        if let Some(list) = predicates.get(&id).clone() {
409            if !list.is_empty() {
410                *view = view.take_dangerous().filter(list.iter().cloned());
411            }
412        }
413        transform.action(view, predicates)?;
414    }
415    Ok(())
416}
417
418/// Propagates information about monotonic inputs through operators,
419/// using [`mz_repr::optimize::OptimizerFeatures`] from `ctx` for [`crate::analysis::Analysis`].
420#[mz_ore::instrument(
421    target = "optimizer",
422    level = "debug",
423    fields(path.segment ="monotonic")
424)]
425pub fn optimize_dataflow_monotonic(
426    dataflow: &mut DataflowDesc,
427    ctx: &mut TransformCtx,
428) -> Result<(), TransformError> {
429    let mut monotonic_ids = BTreeSet::new();
430    for (source_id, source_import) in dataflow.source_imports.iter() {
431        if source_import.monotonic {
432            monotonic_ids.insert(source_id.clone());
433        }
434    }
435    for (
436        _index_id,
437        IndexImport {
438            desc: index_desc,
439            monotonic,
440            ..
441        },
442    ) in dataflow.index_imports.iter()
443    {
444        if *monotonic {
445            monotonic_ids.insert(index_desc.on_id.clone());
446        }
447    }
448
449    let monotonic_flag = MonotonicFlag::default();
450
451    for build_desc in dataflow.objects_to_build.iter_mut() {
452        monotonic_flag.transform(build_desc.plan.as_inner_mut(), ctx, &monotonic_ids)?;
453    }
454
455    mz_repr::explain::trace_plan(dataflow);
456
457    Ok(())
458}
459
460/// Determine whether we require snapshots from our durable source imports.
461/// (For example, these can often be skipped for simple subscribe queries.)
462pub fn optimize_dataflow_snapshot(dataflow: &mut DataflowDesc) -> Result<(), TransformError> {
463    // For every global id, true iff we need a snapshot for that global ID.
464    // This is computed bottom-up: subscribes may or may not require a snapshot from their inputs,
465    // index exports definitely do, and objects-to-build require a snapshot from their inputs if
466    // either they need to provide a snapshot as output or they may need snapshots internally, eg. to
467    // compute a join.
468    let mut downstream_requires_snapshot = BTreeMap::new();
469
470    for (_id, export) in &dataflow.sink_exports {
471        *downstream_requires_snapshot
472            .entry(Id::Global(export.from))
473            .or_default() |= export.with_snapshot;
474    }
475    for (_id, (export, _typ)) in &dataflow.index_exports {
476        *downstream_requires_snapshot
477            .entry(Id::Global(export.on_id))
478            .or_default() |= true;
479    }
480    for BuildDesc { id: _, plan } in dataflow.objects_to_build.iter().rev() {
481        // For now, we treat all intermediate nodes as potentially requiring a snapshot.
482        // Walk the AST, marking anything depended on by a compute object as snapshot-required.
483        let mut todo = vec![(true, &plan.0)];
484        while let Some((requires_snapshot, expr)) = todo.pop() {
485            match expr {
486                MirRelationExpr::Get { id, .. } => {
487                    *downstream_requires_snapshot.entry(*id).or_default() |= requires_snapshot;
488                }
489                other => {
490                    todo.extend(other.children().rev().map(|c| (true, c)));
491                }
492            }
493        }
494    }
495    for (id, import) in &mut dataflow.source_imports {
496        let with_snapshot = downstream_requires_snapshot
497            .entry(Id::Global(*id))
498            .or_default();
499
500        // As above, fetch the snapshot if there are any transformations on the raw source data.
501        // (And we'll always need to check for things like temporal filters, since those allow
502        // snapshot data to affect diffs at times past the as-of.)
503        *with_snapshot |= import.desc.arguments.operators.is_some();
504
505        import.with_snapshot = *with_snapshot;
506    }
507    for (_id, import) in &mut dataflow.index_imports {
508        let with_snapshot = downstream_requires_snapshot
509            .entry(Id::Global(import.desc.on_id))
510            .or_default();
511
512        import.with_snapshot = *with_snapshot;
513    }
514
515    Ok(())
516}
517
518/// Restricts the sources imported by `dataflow` to only the ones its exports read.
519///
520/// The counterpart to [`prune_and_annotate_dataflow_index_imports`] for source imports. Imports are
521/// collected before the global pipeline runs, from the plans as they were written, so a transform
522/// can drop the last `Get` of one, for instance by folding a selection to a constant.
523///
524/// An import that survives that is not free. Every worker builds a `persist_source` for it and
525/// decodes a shard into a stream nobody consumes, and the controller takes a read hold that pins
526/// the collection's `since` for as long as the dataflow lives. Both are read off the import list
527/// directly, so pruning is what reclaims them.
528///
529/// A third consumer, the wall-clock dependence a dataflow reports, is the one whose wrong answer
530/// does real damage: it earns a dataflow whose exports can never change again an expiration, which
531/// pins their output frontier at the expiration time rather than letting it reach the empty
532/// antichain, so nothing downstream learns the collection is final.
533/// `ComputeController::determine_time_dependence` derives that from the read set rather than from
534/// the import list, so it does not depend on this pass having run. `create_dataflow` also reports a
535/// list this pass left loose.
536///
537/// The input plans should be normalized with `NormalizeLets`, for the same reason
538/// [`prune_and_annotate_dataflow_index_imports`] wants them to be: an unused `Let` binding can
539/// otherwise keep alive a `Get` that nothing reads.
540#[mz_ore::instrument(
541    target = "optimizer",
542    level = "debug",
543    fields(path.segment = "source_imports")
544)]
545fn prune_dataflow_source_imports(dataflow: &mut DataflowDesc) {
546    // NOTE: A description with no exports has no answer to "what do the exports read", and pruning
547    // everything is the wrong one. `EXPLAIN` builds a peek description without its index export,
548    // see the conditional `export_index` in `mz_adapter::optimize::peek`. Such a description is
549    // explained and then dropped, never installed, so leaving its import list alone costs nothing.
550    if dataflow.index_exports.is_empty() && dataflow.sink_exports.is_empty() {
551        return;
552    }
553
554    let used = dataflow.used_import_ids();
555    dataflow.source_imports.retain(|id, _| used.contains(id));
556}
557
558/// Restricts the indexes imported by `dataflow` to only the ones it needs.
559/// It also adds to the `DataflowMetainfo` how each index will be used.
560/// It also annotates global `Get`s with whether they will be reads from Persist or an index, plus
561/// their index usage types.
562///
563/// The input `dataflow` should import all indexes belonging to all views/sources/tables it
564/// references.
565///
566/// The input plans should be normalized with `NormalizeLets`! Otherwise, we might find dangling
567/// `ArrangeBy`s at the top of unused Let bindings.
568#[mz_ore::instrument(
569    target = "optimizer",
570    level = "debug",
571    fields(path.segment = "index_imports")
572)]
573fn prune_and_annotate_dataflow_index_imports(
574    dataflow: &mut DataflowDesc,
575    indexes: &dyn IndexOracle,
576    dataflow_metainfo: &mut DataflowMetainfo,
577) -> Result<(), TransformError> {
578    // Preparation.
579    // Let's save the unique keys of the sources. This will inform which indexes to choose for full
580    // scans. (We can't get this info from `source_imports`, because `source_imports` only has those
581    // sources that are not getting an indexed read.)
582    let mut source_keys = BTreeMap::new();
583    for build_desc in dataflow.objects_to_build.iter() {
584        build_desc
585            .plan
586            .as_inner()
587            .visit_pre(|expr: &MirRelationExpr| match expr {
588                MirRelationExpr::Get {
589                    id: Id::Global(global_id),
590                    typ,
591                    ..
592                } => {
593                    source_keys.entry(*global_id).or_insert_with(|| {
594                        typ.keys
595                            .iter()
596                            .map(|key| {
597                                key.iter()
598                                    // Convert the Vec<usize> key to Vec<MirScalarExpr>, so that
599                                    // later we can more easily compare index keys to these keys.
600                                    .map(|col_idx| MirScalarExpr::column(*col_idx))
601                                    .collect()
602                            })
603                            .collect()
604                    });
605                }
606                _ => {}
607            });
608    }
609
610    // This will be a mapping of
611    // (ids used by exports and objects to build) ->
612    // (arrangement keys and usage types on that id that have been requested)
613    let mut index_reqs_by_id = BTreeMap::new();
614
615    // Go through the MIR plans of `objects_to_build` and collect which arrangements are requested
616    // for which we also have an available index.
617    for build_desc in dataflow.objects_to_build.iter_mut() {
618        CollectIndexRequests::new(&source_keys, indexes, &mut index_reqs_by_id)
619            .collect_index_reqs(build_desc.plan.as_inner_mut())?;
620    }
621
622    // Collect index usages by `sink_exports`.
623    // A sink export sometimes wants to directly use an imported index. I know of one case where
624    // this happens: The dataflow for a SUBSCRIBE on an indexed view won't have any
625    // `objects_to_build`, but will want to directly read from the index and write to a sink.
626    for (_sink_id, sink_desc) in dataflow.sink_exports.iter() {
627        // First, let's see if there exists an index on the id that the sink wants. If not, there is
628        // nothing we can do here.
629        if let Some((idx_id, arbitrary_idx_key)) = indexes.indexes_on(sink_desc.from).next() {
630            // If yes, then we'll add a request of _some_ index: If we already collected an index
631            // request on this id, then use that, otherwise use the above arbitrarily picked index.
632            let requested_idxs = index_reqs_by_id
633                .entry(sink_desc.from)
634                .or_insert_with(Vec::new);
635            if let Some((already_req_idx_id, already_req_key, _)) = requested_idxs.get(0) {
636                requested_idxs.push((
637                    *already_req_idx_id,
638                    already_req_key.clone(),
639                    IndexUsageType::SinkExport,
640                ));
641            } else {
642                requested_idxs.push((
643                    idx_id,
644                    arbitrary_idx_key.to_owned(),
645                    IndexUsageType::SinkExport,
646                ));
647            }
648        }
649    }
650
651    // Collect index usages by `index_exports`.
652    for (_id, (index_desc, _)) in dataflow.index_exports.iter() {
653        // First, let's see if there exists an index on the id that the exported index is on. If
654        // not, there is nothing we can do here.
655        if let Some((idx_id, arbitrary_index_key)) = indexes.indexes_on(index_desc.on_id).next() {
656            // If yes, then we'll add an index request of some index: If we already collected an
657            // index request on this id, then use that, otherwise use the above arbitrarily picked
658            // index.
659            let requested_idxs = index_reqs_by_id
660                .entry(index_desc.on_id)
661                .or_insert_with(Vec::new);
662            if let Some((already_req_idx_id, already_req_key, _)) = requested_idxs.get(0) {
663                requested_idxs.push((
664                    *already_req_idx_id,
665                    already_req_key.clone(),
666                    IndexUsageType::IndexExport,
667                ));
668            } else {
669                // This is surprising: Actually, an index creation dataflow always has a plan in
670                // `objects_to_build` that will have a Get of the object that the index is on (see
671                // `DataflowDescription::export_index`). Therefore, we should have already requested
672                // an index usage when seeing that Get in `CollectIndexRequests`.
673                soft_panic_or_log!(
674                    "We are seeing an index export on an id that's not mentioned in `objects_to_build`"
675                );
676                requested_idxs.push((
677                    idx_id,
678                    arbitrary_index_key.to_owned(),
679                    IndexUsageType::IndexExport,
680                ));
681            }
682        }
683    }
684
685    // By now, `index_reqs_by_id` has all ids that we think might benefit from having an index on.
686    // Moreover, for each of these ids, if any index exists on it, then we should have already
687    // picked one. If not, then we have a bug somewhere. In that case, do a soft panic, and add an
688    // Unknown usage, picking an arbitrary index.
689    for (id, index_reqs) in index_reqs_by_id.iter_mut() {
690        if index_reqs.is_empty() {
691            // Try to pick an arbitrary index to be fully scanned.
692            if let Some((idx_id, key)) = indexes.indexes_on(*id).next() {
693                soft_panic_or_log!(
694                    "prune_and_annotate_dataflow_index_imports didn't find any index for an id, even though one exists
695id: {}, key: {:?}",
696                    id,
697                    key
698                );
699                index_reqs.push((idx_id, key.to_owned(), IndexUsageType::Unknown));
700            }
701        }
702    }
703
704    // Adjust FullScans to not introduce a new index dependency if there is also some non-FullScan
705    // request on the same id.
706    // `full_scan_changes` saves the changes that we do: Each (Get id, index id) entry indicates
707    // that if a Get has that id, then any full scan index accesses on it should be changed to use
708    // the indicated index id.
709    let mut full_scan_changes = BTreeMap::new();
710    for (get_id, index_reqs) in index_reqs_by_id.iter_mut() {
711        // Let's choose a non-FullScan access (if exists).
712        if let Some((picked_idx, picked_idx_key)) = choose_index(
713            &source_keys,
714            get_id,
715            &index_reqs
716                .iter()
717                .filter_map(|(idx_id, key, usage_type)| match usage_type {
718                    IndexUsageType::FullScan => None,
719                    _ => Some((*idx_id, key.clone())),
720                })
721                .collect_vec(),
722        ) {
723            // Found a non-FullScan access. Modify all FullScans to use the same index as that one.
724            for (idx_id, key, usage_type) in index_reqs {
725                match usage_type {
726                    IndexUsageType::FullScan => {
727                        full_scan_changes.insert(get_id, picked_idx);
728                        *idx_id = picked_idx;
729                        key.clone_from(&picked_idx_key);
730                    }
731                    _ => {}
732                }
733            }
734        }
735    }
736    // Apply the above full scan changes to also the Gets.
737    for build_desc in dataflow.objects_to_build.iter_mut() {
738        build_desc
739            .plan
740            .as_inner_mut()
741            .visit_pre_mut(|expr: &mut MirRelationExpr| {
742                match expr {
743                    MirRelationExpr::Get {
744                        id: Id::Global(global_id),
745                        typ: _,
746                        access_strategy: persist_or_index,
747                    } => {
748                        if let Some(new_idx_id) = full_scan_changes.get(global_id) {
749                            match persist_or_index {
750                                AccessStrategy::UnknownOrLocal => {
751                                    // Should have been already filled by `collect_index_reqs`.
752                                    unreachable!()
753                                }
754                                AccessStrategy::Persist => {
755                                    // We already know that it's an indexed access.
756                                    unreachable!()
757                                }
758                                AccessStrategy::SameDataflow => {
759                                    // We have not added such annotations yet.
760                                    unreachable!()
761                                }
762                                AccessStrategy::Index(accesses) => {
763                                    for (idx_id, usage_type) in accesses {
764                                        if matches!(usage_type, IndexUsageType::FullScan) {
765                                            *idx_id = *new_idx_id;
766                                        }
767                                    }
768                                }
769                            }
770                        }
771                    }
772                    _ => {}
773                }
774            });
775    }
776
777    // Annotate index imports by their usage types
778    dataflow_metainfo.index_usage_types = BTreeMap::new();
779    for (
780        index_id,
781        IndexImport {
782            desc: index_desc,
783            typ: _,
784            monotonic: _,
785            with_snapshot: _,
786        },
787    ) in dataflow.index_imports.iter_mut()
788    {
789        // A sanity check that we are not importing an index that we are also exporting.
790        assert!(
791            !dataflow
792                .index_exports
793                .iter()
794                .map(|(exported_index_id, _)| exported_index_id)
795                .any(|exported_index_id| exported_index_id == index_id)
796        );
797
798        let mut new_usage_types = Vec::new();
799        // Let's see whether something has requested an index on this object that this imported
800        // index is on.
801        if let Some(index_reqs) = index_reqs_by_id.get(&index_desc.on_id) {
802            for (req_idx_id, req_key, req_usage_type) in index_reqs {
803                if req_idx_id == index_id {
804                    soft_assert_eq_or_log!(*req_key, index_desc.key);
805                    new_usage_types.push(req_usage_type.clone());
806                }
807            }
808        }
809        if !new_usage_types.is_empty() {
810            dataflow_metainfo
811                .index_usage_types
812                .insert(*index_id, new_usage_types);
813        }
814    }
815
816    // Prune index imports to only those that are used
817    dataflow
818        .index_imports
819        .retain(|id, _index_import| dataflow_metainfo.index_usage_types.contains_key(id));
820
821    // Determine AccessStrategy::SameDataflow accesses. These were classified as
822    // AccessStrategy::Persist inside collect_index_reqs, so now we check these, and if the id is of
823    // a collection that we are building ourselves, then we adjust the access strategy.
824    let mut objects_to_build_ids = BTreeSet::new();
825    for BuildDesc { id, plan: _ } in dataflow.objects_to_build.iter() {
826        objects_to_build_ids.insert(id.clone());
827    }
828    for build_desc in dataflow.objects_to_build.iter_mut() {
829        build_desc
830            .plan
831            .as_inner_mut()
832            .visit_pre_mut(|expr: &mut MirRelationExpr| match expr {
833                MirRelationExpr::Get {
834                    id: Id::Global(global_id),
835                    typ: _,
836                    access_strategy,
837                } => match access_strategy {
838                    AccessStrategy::Persist => {
839                        if objects_to_build_ids.contains(global_id) {
840                            *access_strategy = AccessStrategy::SameDataflow;
841                        }
842                    }
843                    _ => {}
844                },
845                _ => {}
846            });
847    }
848
849    // A sanity check that all Get annotations indicate indexes that are present in `index_imports`.
850    for build_desc in dataflow.objects_to_build.iter() {
851        build_desc
852            .plan
853            .as_inner()
854            .visit_pre(|expr: &MirRelationExpr| match expr {
855                MirRelationExpr::Get {
856                    id: Id::Global(_),
857                    typ: _,
858                    access_strategy: AccessStrategy::Index(accesses),
859                } => {
860                    for (idx_id, _) in accesses {
861                        soft_assert_or_log!(
862                            dataflow.index_imports.contains_key(idx_id),
863                            "Dangling Get index annotation"
864                        );
865                    }
866                }
867                _ => {}
868            });
869    }
870
871    mz_repr::explain::trace_plan(dataflow);
872
873    Ok(())
874}
875
876/// Pick an index from a given Vec of index keys.
877///
878/// Currently, we pick as follows:
879///  - If there is an index on a unique key, then we pick that. (It might be better distributed, and
880///    is less likely to get dropped than other indexes.)
881///  - Otherwise, we pick an arbitrary index.
882///
883/// TODO: There are various edge cases where a better choice would be possible:
884/// - Some indexes might be less skewed than others. (Although, picking a unique key tries to
885///   capture this already.)
886/// - Some indexes might have an error, while others don't.
887///   <https://github.com/MaterializeInc/database-issues/issues/4455>
888/// - Some indexes might have more extra data in their keys (because of being on more complicated
889///   expressions than just column references), which won't be used in a full scan.
890fn choose_index(
891    source_keys: &BTreeMap<GlobalId, BTreeSet<Vec<MirScalarExpr>>>,
892    id: &GlobalId,
893    indexes: &Vec<(GlobalId, Vec<MirScalarExpr>)>,
894) -> Option<(GlobalId, Vec<MirScalarExpr>)> {
895    match source_keys.get(id) {
896        None => indexes.iter().next().cloned(), // pick an arbitrary index
897        Some(coll_keys) => match indexes
898            .iter()
899            .find(|(_idx_id, key)| coll_keys.contains(&*key))
900        {
901            Some((idx_id, key)) => Some((*idx_id, key.clone())),
902            None => indexes.iter().next().cloned(), // pick an arbitrary index
903        },
904    }
905}
906
907#[derive(Debug)]
908struct CollectIndexRequests<'a> {
909    /// We were told about these unique keys on sources.
910    source_keys: &'a BTreeMap<GlobalId, BTreeSet<Vec<MirScalarExpr>>>,
911    /// We were told about these indexes being available.
912    indexes_available: &'a dyn IndexOracle,
913    /// We'll be collecting index requests here.
914    index_reqs_by_id:
915        &'a mut BTreeMap<GlobalId, Vec<(GlobalId, Vec<MirScalarExpr>, IndexUsageType)>>,
916    /// As we recurse down a MirRelationExpr, we'll need to keep track of the context of the
917    /// current node (see docs on `IndexUsageContext` about what context we keep).
918    /// Moreover, we need to propagate this context from cte uses to cte definitions.
919    /// `context_across_lets` will keep track of the contexts that reached each use of a LocalId
920    /// added together.
921    context_across_lets: BTreeMap<LocalId, Vec<IndexUsageContext>>,
922    recursion_guard: RecursionGuard,
923}
924
925impl<'a> CheckedRecursion for CollectIndexRequests<'a> {
926    fn recursion_guard(&self) -> &RecursionGuard {
927        &self.recursion_guard
928    }
929}
930
931impl<'a> CollectIndexRequests<'a> {
932    fn new(
933        source_keys: &'a BTreeMap<GlobalId, BTreeSet<Vec<MirScalarExpr>>>,
934        indexes_available: &'a dyn IndexOracle,
935        index_reqs_by_id: &'a mut BTreeMap<
936            GlobalId,
937            Vec<(GlobalId, Vec<MirScalarExpr>, IndexUsageType)>,
938        >,
939    ) -> CollectIndexRequests<'a> {
940        CollectIndexRequests {
941            source_keys,
942            indexes_available,
943            index_reqs_by_id,
944            context_across_lets: BTreeMap::new(),
945            recursion_guard: RecursionGuard::with_limit(RECURSION_LIMIT),
946        }
947    }
948
949    pub fn collect_index_reqs(
950        &mut self,
951        expr: &mut MirRelationExpr,
952    ) -> Result<(), RecursionLimitError> {
953        assert!(self.context_across_lets.is_empty());
954        self.collect_index_reqs_inner(
955            expr,
956            &IndexUsageContext::from_usage_type(IndexUsageType::PlanRootNoArrangement),
957        )?;
958        assert!(self.context_across_lets.is_empty());
959        // Sanity check that we don't have any `DeltaJoinIndexUsageType::Unknown` remaining.
960        for (_id, index_reqs) in self.index_reqs_by_id.iter() {
961            for (_, _, index_usage_type) in index_reqs {
962                soft_assert_or_log!(
963                    !matches!(
964                        index_usage_type,
965                        IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::Unknown)
966                    ),
967                    "Delta join Unknown index usage remained"
968                );
969            }
970        }
971        Ok(())
972    }
973
974    fn collect_index_reqs_inner(
975        &mut self,
976        expr: &mut MirRelationExpr,
977        contexts: &Vec<IndexUsageContext>,
978    ) -> Result<(), RecursionLimitError> {
979        self.checked_recur_mut(|this| {
980            // If an index exists on `on_id`, this function picks an index to be fully scanned.
981            let pick_index_for_full_scan = |on_id: &GlobalId| {
982                // Note that the choice we make here might be modified later at the
983                // "Adjust FullScans to not introduce a new index dependency".
984                choose_index(
985                    this.source_keys,
986                    on_id,
987                    &this
988                        .indexes_available
989                        .indexes_on(*on_id)
990                        .map(|(idx_id, key)| (idx_id, key.iter().cloned().collect_vec()))
991                        .collect_vec(),
992                )
993            };
994
995            // See comment on `IndexUsageContext`.
996            Ok(match expr {
997                MirRelationExpr::Join {
998                    inputs,
999                    implementation,
1000                    ..
1001                } => {
1002                    match implementation {
1003                        JoinImplementation::Differential(..) => {
1004                            for input in inputs {
1005                                this.collect_index_reqs_inner(
1006                                    input,
1007                                    &IndexUsageContext::from_usage_type(
1008                                        IndexUsageType::DifferentialJoin,
1009                                    ),
1010                                )?;
1011                            }
1012                        }
1013                        JoinImplementation::DeltaQuery(..) => {
1014                            // For Delta joins, the first input is special, see
1015                            // https://github.com/MaterializeInc/database-issues/issues/2115
1016                            this.collect_index_reqs_inner(
1017                                &mut inputs[0],
1018                                &IndexUsageContext::from_usage_type(IndexUsageType::DeltaJoin(
1019                                    DeltaJoinIndexUsageType::Unknown,
1020                                )),
1021                            )?;
1022                            for input in &mut inputs[1..] {
1023                                this.collect_index_reqs_inner(
1024                                    input,
1025                                    &IndexUsageContext::from_usage_type(IndexUsageType::DeltaJoin(
1026                                        DeltaJoinIndexUsageType::Lookup,
1027                                    )),
1028                                )?;
1029                            }
1030                        }
1031                        JoinImplementation::IndexedFilter(_coll_id, idx_id, ..) => {
1032                            for input in inputs {
1033                                this.collect_index_reqs_inner(
1034                                    input,
1035                                    &IndexUsageContext::from_usage_type(IndexUsageType::Lookup(
1036                                        *idx_id,
1037                                    )),
1038                                )?;
1039                            }
1040                        }
1041                        JoinImplementation::Unimplemented => {
1042                            soft_panic_or_log!(
1043                                "CollectIndexRequests encountered an Unimplemented join"
1044                            );
1045                        }
1046                    }
1047                }
1048                MirRelationExpr::ArrangeBy { input, keys } => {
1049                    let ctx = &IndexUsageContext::add_keys(contexts, keys);
1050                    this.collect_index_reqs_inner(input, ctx)?;
1051                }
1052                MirRelationExpr::Get {
1053                    id: Id::Global(global_id),
1054                    access_strategy: persist_or_index,
1055                    ..
1056                } => {
1057                    this.index_reqs_by_id
1058                        .entry(*global_id)
1059                        .or_insert_with(Vec::new);
1060                    // If the context is empty, it means we didn't see an operator that would
1061                    // specifically want to use an index for this Get. However, let's still try to
1062                    // find an index for a full scan.
1063                    let mut try_full_scan = contexts.is_empty();
1064                    let mut index_accesses = Vec::new();
1065                    for context in contexts {
1066                        match &context.requested_keys {
1067                            None => {
1068                                // We have some index usage context, but didn't see an `ArrangeBy`.
1069                                try_full_scan = true;
1070                                match context.usage_type {
1071                                    IndexUsageType::FullScan
1072                                    | IndexUsageType::SinkExport
1073                                    | IndexUsageType::IndexExport => {
1074                                        // Not possible, because these don't go through
1075                                        // IndexUsageContext at all.
1076                                        unreachable!()
1077                                    }
1078                                    // You can find more info on why the following join cases
1079                                    // shouldn't happen in comments of the Join lowering to LIR.
1080                                    IndexUsageType::Lookup(_) => soft_panic_or_log!(
1081                                        "CollectIndexRequests encountered \
1082                                         an IndexedFilter join without an ArrangeBy"
1083                                    ),
1084                                    IndexUsageType::DifferentialJoin => soft_panic_or_log!(
1085                                        "CollectIndexRequests encountered \
1086                                         a Differential join without an ArrangeBy"
1087                                    ),
1088                                    IndexUsageType::DeltaJoin(_) => soft_panic_or_log!(
1089                                        "CollectIndexRequests encountered \
1090                                         a Delta join without an ArrangeBy"
1091                                    ),
1092                                    IndexUsageType::PlanRootNoArrangement => {
1093                                        // This is ok: the entire plan is a `Get`, with not even an
1094                                        // `ArrangeBy`. Note that if an index exists, the usage will
1095                                        // be saved as `FullScan` (NOT as `PlanRootNoArrangement`),
1096                                        // because we are going into the `try_full_scan` if.
1097                                    }
1098                                    IndexUsageType::FastPathLimit => {
1099                                        // These are created much later, not even inside
1100                                        // `prune_and_annotate_dataflow_index_imports`.
1101                                        unreachable!()
1102                                    }
1103                                    IndexUsageType::DanglingArrangeBy => {
1104                                        // Not possible, because we create `DanglingArrangeBy`
1105                                        // only when we see an `ArrangeBy`.
1106                                        unreachable!()
1107                                    }
1108                                    IndexUsageType::Unknown => {
1109                                        // These are added only after `CollectIndexRequests` has run.
1110                                        unreachable!()
1111                                    }
1112                                }
1113                            }
1114                            Some(requested_keys) => {
1115                                for requested_key in requested_keys {
1116                                    match this.indexes_available.indexes_on(*global_id).find(
1117                                        |(available_idx_id, available_key)| {
1118                                            match context.usage_type {
1119                                                IndexUsageType::Lookup(req_idx_id) => {
1120                                                    // `LiteralConstraints` already picked an index
1121                                                    // by id. Let's use that one.
1122                                                    assert!(
1123                                                        !(available_idx_id == &req_idx_id
1124                                                            && available_key != &requested_key)
1125                                                    );
1126                                                    available_idx_id == &req_idx_id
1127                                                }
1128                                                _ => available_key == &requested_key,
1129                                            }
1130                                        },
1131                                    ) {
1132                                        Some((idx_id, key)) => {
1133                                            let usage = context.usage_type.clone();
1134                                            this.index_reqs_by_id
1135                                                .get_mut(global_id)
1136                                                .unwrap()
1137                                                .push((idx_id, key.to_owned(), usage.clone()));
1138                                            index_accesses.push((idx_id, usage));
1139                                        }
1140                                        None => {
1141                                            // If there is a key requested for which we don't have an
1142                                            // index, then we might still be able to do a full scan of a
1143                                            // differently keyed index.
1144                                            try_full_scan = true;
1145                                        }
1146                                    }
1147                                }
1148                                if requested_keys.is_empty() {
1149                                    // It's a bit weird if an MIR ArrangeBy is not requesting any
1150                                    // key, but let's try a full scan in that case anyhow.
1151                                    try_full_scan = true;
1152                                }
1153                            }
1154                        }
1155                    }
1156                    if try_full_scan {
1157                        // Keep in mind that when having 2 contexts coming from 2 uses of a Let,
1158                        // this code can't distinguish between the case when there is 1 ArrangeBy at the
1159                        // top of the Let, or when the 2 uses each have an `ArrangeBy`. In both cases,
1160                        // we'll add only 1 full scan, which would be wrong in the latter case. However,
1161                        // the latter case can't currently happen until we do
1162                        // https://github.com/MaterializeInc/database-issues/issues/6363
1163                        // Also note that currently we are deduplicating index usage types when
1164                        // printing index usages in EXPLAIN.
1165                        if let Some((idx_id, key)) = pick_index_for_full_scan(global_id) {
1166                            this.index_reqs_by_id.get_mut(global_id).unwrap().push((
1167                                idx_id,
1168                                key.to_owned(),
1169                                IndexUsageType::FullScan,
1170                            ));
1171                            index_accesses.push((idx_id, IndexUsageType::FullScan));
1172                        }
1173                    }
1174                    if index_accesses.is_empty() {
1175                        *persist_or_index = AccessStrategy::Persist;
1176                    } else {
1177                        *persist_or_index = AccessStrategy::Index(index_accesses);
1178                    }
1179                }
1180                MirRelationExpr::Get {
1181                    id: Id::Local(local_id),
1182                    ..
1183                } => {
1184                    // Add the current context to the vector of contexts of `local_id`.
1185                    // (The unwrap is safe, because the Let and LetRec cases start with inserting an
1186                    // empty entry.)
1187                    this.context_across_lets
1188                        .get_mut(local_id)
1189                        .unwrap()
1190                        .extend(contexts.iter().cloned());
1191                    // No recursive call here, because Get has no inputs.
1192                }
1193                MirRelationExpr::Let { id, value, body } => {
1194                    let shadowed_context = this.context_across_lets.insert(id.clone(), Vec::new());
1195                    // No shadowing in MIR
1196                    assert_none!(shadowed_context);
1197                    // We go backwards: Recurse on the body and then the value.
1198                    this.collect_index_reqs_inner(body, contexts)?;
1199                    // The above call filled in the entry for `id` in `context_across_lets` (if it
1200                    // was referenced). Anyhow, at least an empty entry should exist, because we started
1201                    // above with inserting it.
1202                    this.collect_index_reqs_inner(value, &this.context_across_lets[id].clone())?;
1203                    // Clean up the id from the saved contexts.
1204                    this.context_across_lets.remove(id);
1205                }
1206                MirRelationExpr::LetRec {
1207                    ids,
1208                    values,
1209                    limits: _,
1210                    body,
1211                } => {
1212                    for id in ids.iter() {
1213                        let shadowed_context =
1214                            this.context_across_lets.insert(id.clone(), Vec::new());
1215                        // No shadowing in MIR
1216                        assert_none!(shadowed_context);
1217                    }
1218                    // We go backwards: Recurse on the body first.
1219                    this.collect_index_reqs_inner(body, contexts)?;
1220                    // Reset the contexts of the ids (of the current LetRec), because an arrangement
1221                    // from a value can't be used in the body.
1222                    for id in ids.iter() {
1223                        *this.context_across_lets.get_mut(id).unwrap() = Vec::new();
1224                    }
1225                    // Recurse on the values in reverse order.
1226                    // Note that we do only one pass, i.e., we won't see context through a Get that
1227                    // refers to the previous iteration. But this is ok, because we can't reuse
1228                    // arrangements across iterations anyway.
1229                    for (id, value) in ids.iter().rev().zip_eq(values.iter_mut().rev()) {
1230                        this.collect_index_reqs_inner(
1231                            value,
1232                            &this.context_across_lets[id].clone(),
1233                        )?;
1234                    }
1235                    // Clean up the ids from the saved contexts.
1236                    for id in ids {
1237                        this.context_across_lets.remove(id);
1238                    }
1239                }
1240                _ => {
1241                    // Nothing interesting at this node, recurse with the empty context (regardless of
1242                    // what context we got from above).
1243                    let empty_context = Vec::new();
1244                    for child in expr.children_mut() {
1245                        this.collect_index_reqs_inner(child, &empty_context)?;
1246                    }
1247                }
1248            })
1249        })
1250    }
1251}
1252
1253/// This struct will save info about parent nodes as we are descending down a `MirRelationExpr`.
1254/// We always start with filling in `usage_type` when we see an operation that uses an arrangement,
1255/// and then we fill in `requested_keys` when we see an `ArrangeBy`. So, the pattern that we are
1256/// looking for is
1257/// ```text
1258/// <operation that uses an index>
1259///   ArrangeBy <requested_keys>
1260///     Get <global_id>
1261/// ```
1262/// When we reach a `Get` to a global id, we access this context struct to see if the rest of the
1263/// pattern is present above the `Get`.
1264///
1265/// Note that we usually put this struct in a Vec, because we track context across local let
1266/// bindings, which means that a node can have multiple parents.
1267#[derive(Debug, Clone)]
1268struct IndexUsageContext {
1269    usage_type: IndexUsageType,
1270    requested_keys: Option<BTreeSet<Vec<MirScalarExpr>>>,
1271}
1272
1273impl IndexUsageContext {
1274    pub fn from_usage_type(usage_type: IndexUsageType) -> Vec<Self> {
1275        vec![IndexUsageContext {
1276            usage_type,
1277            requested_keys: None,
1278        }]
1279    }
1280
1281    // Add the keys of an ArrangeBy into the contexts.
1282    // Soft_panics if haven't already seen something that indicates what the index will be used for.
1283    pub fn add_keys(
1284        old_contexts: &Vec<IndexUsageContext>,
1285        keys_to_add: &Vec<Vec<MirScalarExpr>>,
1286    ) -> Vec<IndexUsageContext> {
1287        let old_contexts = if old_contexts.is_empty() {
1288            // No join above us, and we are not at the root. Why does this ArrangeBy even exist?
1289            soft_panic_or_log!("CollectIndexRequests encountered a dangling ArrangeBy");
1290            // Anyhow, let's create a context with a `DanglingArrangeBy` index usage, so that we
1291            // have a place to note down the requested keys below.
1292            IndexUsageContext::from_usage_type(IndexUsageType::DanglingArrangeBy)
1293        } else {
1294            old_contexts.clone()
1295        };
1296        old_contexts
1297            .into_iter()
1298            .flat_map(|old_context| {
1299                if !matches!(
1300                    old_context.usage_type,
1301                    IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::Unknown)
1302                ) {
1303                    // If it's not an unknown delta join usage, then we simply note down the new
1304                    // keys into `requested_keys`.
1305                    let mut context = old_context.clone();
1306                    if context.requested_keys.is_none() {
1307                        context.requested_keys = Some(BTreeSet::new());
1308                    }
1309                    context
1310                        .requested_keys
1311                        .as_mut()
1312                        .unwrap()
1313                        .extend(keys_to_add.iter().cloned());
1314                    Some(context).into_iter().chain(None)
1315                } else {
1316                    // If it's an unknown delta join usage, then we need to figure out whether this
1317                    // is a full scan or a lookup.
1318                    //
1319                    // `source_key` in `DeltaPathPlan` determines which arrangement we are going to
1320                    // scan when starting the rendering of a delta path. This is the one for which
1321                    // we want a `DeltaJoinIndexUsageType::FirstInputFullScan`.
1322                    //
1323                    // However, `DeltaPathPlan` is an LIR concept, and here we need to figure out
1324                    // the `source_key` based on the MIR plan. We do this by doing the same as
1325                    // `DeltaJoinPlan::create_from`: choose the smallest key (by `Ord`).
1326                    let source_key = keys_to_add
1327                        .iter()
1328                        .min()
1329                        .expect("ArrangeBy below a delta join has at least one key");
1330                    let full_scan_context = IndexUsageContext {
1331                        requested_keys: Some(BTreeSet::from([source_key.clone()])),
1332                        usage_type: IndexUsageType::DeltaJoin(
1333                            DeltaJoinIndexUsageType::FirstInputFullScan,
1334                        ),
1335                    };
1336                    let lookup_keys = keys_to_add
1337                        .into_iter()
1338                        .filter(|key| *key != source_key)
1339                        .cloned()
1340                        .collect_vec();
1341                    if lookup_keys.is_empty() {
1342                        Some(full_scan_context).into_iter().chain(None)
1343                    } else {
1344                        let lookup_context = IndexUsageContext {
1345                            requested_keys: Some(lookup_keys.into_iter().collect()),
1346                            usage_type: IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::Lookup),
1347                        };
1348                        Some(full_scan_context)
1349                            .into_iter()
1350                            .chain(Some(lookup_context))
1351                    }
1352                }
1353            })
1354            .collect()
1355    }
1356}
1357
1358/// Extra information about the dataflow. This is not going to be shipped, but has to be processed
1359/// in other ways, e.g., showing notices to the user, or saving meta-information to the catalog.
1360#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1361#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
1362pub struct DataflowMetainfo<Notice = RawOptimizerNotice> {
1363    /// Notices that the optimizer wants to show to users.
1364    /// For pushing a new element, use [`Self::push_optimizer_notice_dedup`].
1365    pub optimizer_notices: Vec<Notice>,
1366    /// What kind of operation (full scan, lookup, ...) will access each index. Computed by
1367    /// `prune_and_annotate_dataflow_index_imports`.
1368    pub index_usage_types: BTreeMap<GlobalId, Vec<IndexUsageType>>,
1369}
1370
1371impl<Notice> Default for DataflowMetainfo<Notice> {
1372    fn default() -> Self {
1373        DataflowMetainfo {
1374            optimizer_notices: Vec::new(),
1375            index_usage_types: BTreeMap::new(),
1376        }
1377    }
1378}
1379
1380impl<Notice> DataflowMetainfo<Notice> {
1381    /// Create a [`UsedIndexes`] instance by resolving each `id` in the
1382    /// `index_ids` iterator against an entry expected to exist in the
1383    /// [`DataflowMetainfo::index_usage_types`].
1384    pub fn used_indexes<T>(&self, df_desc: &DataflowDescription<T>) -> UsedIndexes {
1385        UsedIndexes::new(
1386            df_desc
1387                .index_imports
1388                .iter()
1389                .map(|(id, _)| {
1390                    let entry = self.index_usage_types.get(id).cloned();
1391                    // If an entry does not exist, mark the usage type for this
1392                    // index as `Unknown`.
1393                    //
1394                    // This should never happen if this method is called after
1395                    // running `prune_and_annotate_dataflow_index_imports` on
1396                    // the dataflow (this happens at the end of the
1397                    // `optimize_dataflow` call).
1398                    let index_usage_type = entry.unwrap_or_else(|| vec![IndexUsageType::Unknown]);
1399
1400                    (*id, index_usage_type)
1401                })
1402                .collect(),
1403        )
1404    }
1405}
1406
1407impl DataflowMetainfo<RawOptimizerNotice> {
1408    /// Pushes a [`RawOptimizerNotice`] into [`Self::optimizer_notices`], but
1409    /// only if the exact same notice is not already present.
1410    pub fn push_optimizer_notice_dedup<T>(&mut self, notice: T)
1411    where
1412        T: Into<RawOptimizerNotice>,
1413    {
1414        let notice = notice.into();
1415        if !self.optimizer_notices.contains(&notice) {
1416            self.optimizer_notices.push(notice);
1417        }
1418    }
1419}
1420
1421#[cfg(test)]
1422mod tests {
1423    use mz_compute_types::sinks::{
1424        ComputeSinkConnection, ComputeSinkDesc, SubscribeSinkConnection,
1425    };
1426    use mz_expr::OptimizedMirRelationExpr;
1427    use mz_repr::{RelationDesc, ReprRelationType, ReprScalarType, SqlRelationType};
1428
1429    use super::*;
1430
1431    const READ: GlobalId = GlobalId::User(1);
1432    const UNREAD: GlobalId = GlobalId::User(2);
1433    const VIEW: GlobalId = GlobalId::Transient(1);
1434    const SINK: GlobalId = GlobalId::Transient(2);
1435
1436    fn typ() -> ReprRelationType {
1437        ReprRelationType::new(vec![ReprScalarType::Int64.nullable(false)])
1438    }
1439
1440    /// A dataflow importing `READ` and `UNREAD` and building `VIEW` from `plan`. It has no exports
1441    /// until `export_subscribe` adds one.
1442    fn dataflow(plan: MirRelationExpr) -> DataflowDesc {
1443        let mut df = DataflowDesc::new("test".to_string());
1444        df.import_source(READ, SqlRelationType::from_repr(&typ()), false);
1445        df.import_source(UNREAD, SqlRelationType::from_repr(&typ()), false);
1446        df.objects_to_build.push(BuildDesc {
1447            id: VIEW,
1448            plan: OptimizedMirRelationExpr::declare_optimized(plan),
1449        });
1450        df
1451    }
1452
1453    fn export_subscribe(df: &mut DataflowDesc) {
1454        df.export_sink(
1455            SINK,
1456            ComputeSinkDesc {
1457                from: VIEW,
1458                from_desc: RelationDesc::new(SqlRelationType::from_repr(&typ()), ["c"]),
1459                connection: ComputeSinkConnection::Subscribe(SubscribeSinkConnection {
1460                    output: Vec::new(),
1461                }),
1462                with_snapshot: true,
1463                up_to: Default::default(),
1464                non_null_assertions: Vec::new(),
1465                refresh_schedule: None,
1466            },
1467        );
1468    }
1469
1470    fn get(id: GlobalId) -> MirRelationExpr {
1471        MirRelationExpr::Get {
1472            id: Id::Global(id),
1473            typ: typ(),
1474            access_strategy: AccessStrategy::Persist,
1475        }
1476    }
1477
1478    fn constant() -> MirRelationExpr {
1479        MirRelationExpr::Constant {
1480            rows: Ok(Vec::new()),
1481            typ: typ(),
1482        }
1483    }
1484
1485    #[mz_ore::test]
1486    fn prune_drops_the_import_no_export_reads() {
1487        let mut df = dataflow(get(READ));
1488        export_subscribe(&mut df);
1489
1490        prune_dataflow_source_imports(&mut df);
1491
1492        assert_eq!(df.imported_source_ids().collect::<Vec<_>>(), vec![READ]);
1493    }
1494
1495    /// The shape this prune exists for: the optimizer folded the export to a constant, so neither
1496    /// import is read any more even though both are still listed.
1497    #[mz_ore::test]
1498    fn prune_drops_every_import_of_a_constant_export() {
1499        let mut df = dataflow(constant());
1500        export_subscribe(&mut df);
1501
1502        prune_dataflow_source_imports(&mut df);
1503
1504        assert_eq!(df.imported_source_ids().count(), 0);
1505    }
1506
1507    /// A description with no exports is one `EXPLAIN` builds and never installs. Pruning it against
1508    /// an empty set of exports would strip every import, so the prune leaves it alone.
1509    #[mz_ore::test]
1510    fn prune_leaves_an_export_less_description_alone() {
1511        let mut df = dataflow(get(READ));
1512
1513        prune_dataflow_source_imports(&mut df);
1514
1515        assert_eq!(
1516            df.imported_source_ids().collect::<Vec<_>>(),
1517            vec![READ, UNREAD]
1518        );
1519    }
1520}