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