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