Skip to main content

mz_compute_types/
dataflows.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//! Types for describing dataflows.
11
12use std::collections::{BTreeMap, BTreeSet};
13use std::fmt;
14
15use mz_expr::{
16    CollectionPlan, MapFilterProject, MfpPlan, MirRelationExpr, MirScalarExpr,
17    OptimizedMirRelationExpr,
18};
19use mz_ore::collections::CollectionExt;
20use mz_ore::soft_assert_or_log;
21use mz_repr::refresh_schedule::RefreshSchedule;
22use mz_repr::{GlobalId, ReprRelationType, SqlRelationType, Timestamp};
23use mz_storage_types::time_dependence::TimeDependence;
24use serde::{Deserialize, Serialize};
25use timely::progress::Antichain;
26
27use crate::plan::LirRelationExpr;
28use crate::plan::render_plan::RenderPlan;
29use crate::plan::scalar::{LirScalarExpr, lses_from_mses};
30use crate::sinks::{ComputeSinkConnection, ComputeSinkDesc};
31use crate::sources::{SourceInstanceArguments, SourceInstanceDesc};
32
33/// A description of a dataflow to construct and results to surface.
34#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
35pub struct DataflowDescription<P, S: 'static = (), O = MapFilterProject> {
36    /// Sources instantiations made available to the dataflow pair with monotonicity information.
37    pub source_imports: BTreeMap<GlobalId, SourceImport<S, O>>,
38    /// Indexes made available to the dataflow.
39    /// (id of index, import)
40    pub index_imports: BTreeMap<GlobalId, IndexImport>,
41    /// Views and indexes to be built and stored in the local context.
42    /// Objects must be built in the specific order, as there may be
43    /// dependencies of later objects on prior identifiers.
44    pub objects_to_build: Vec<BuildDesc<P>>,
45    /// Indexes to be made available to be shared with other dataflows
46    /// (id of new index, description of index, relationtype of base source/view/table)
47    pub index_exports: BTreeMap<GlobalId, (IndexDesc<MirScalarExpr>, ReprRelationType)>,
48    /// sinks to be created
49    /// (id of new sink, description of sink)
50    pub sink_exports: BTreeMap<GlobalId, ComputeSinkDesc<S>>,
51    /// An optional frontier to which inputs should be advanced.
52    ///
53    /// If this is set, it should override the default setting determined by
54    /// the upper bound of `since` frontiers contributing to the dataflow.
55    /// It is an error for this to be set to a frontier not beyond that default.
56    pub as_of: Option<Antichain<Timestamp>>,
57    /// Frontier beyond which the dataflow should not execute.
58    /// Specifically, updates at times greater or equal to this frontier are suppressed.
59    /// This is often set to `as_of + 1` to enable "batch" computations.
60    /// Note that frontier advancements might still happen to times that are after the `until`,
61    /// only data is suppressed. (This is consistent with how frontier advancements can also
62    /// happen before the `as_of`.)
63    pub until: Antichain<Timestamp>,
64    /// The initial as_of when the collection is first created. Filled only for materialized views.
65    /// Note that this doesn't change upon restarts.
66    pub initial_storage_as_of: Option<Antichain<Timestamp>>,
67    /// The schedule of REFRESH materialized views.
68    pub refresh_schedule: Option<RefreshSchedule>,
69    /// Human-readable name
70    pub debug_name: String,
71    /// Description of how the dataflow's progress relates to wall-clock time. None for unknown.
72    pub time_dependence: Option<TimeDependence>,
73}
74
75impl<P, S, O> DataflowDescription<P, S, O> {
76    /// Tests if the dataflow refers to a single timestamp, namely
77    /// that `as_of` has a single coordinate and that the `until`
78    /// value corresponds to the `as_of` value plus one, or `as_of`
79    /// is the maximum timestamp and is thus single.
80    pub fn is_single_time(&self) -> bool {
81        // TODO: this would be much easier to check if `until` was a strict lower bound,
82        // and we would be testing that `until == as_of`.
83
84        let until = &self.until;
85
86        // IF `as_of` is not set at all this can't be a single time dataflow.
87        let Some(as_of) = self.as_of.as_ref() else {
88            return false;
89        };
90        // Ensure that as_of <= until.
91        soft_assert_or_log!(
92            timely::PartialOrder::less_equal(as_of, until),
93            "expected `as_of ≤ until`, got `{as_of:?} ≰ {until:?}`",
94        );
95        // IF `as_of` is not a single timestamp this can't be a single time dataflow.
96        let Some(as_of) = as_of.as_option() else {
97            return false;
98        };
99        // With `as_of = MAX` the `as_of <= until` invariant leaves only two valid
100        // frontiers, and both are handled correctly by the comparison below:
101        //   * `until = {}` (unbounded) yields `None == None`, so single time. `MAX`
102        //     is the last timestamp, so an unbounded dataflow starting there sees
103        //     exactly one time.
104        //   * `until = {MAX}` is the empty range produced by e.g.
105        //     `SUBSCRIBE ... AS OF MAX UP TO MAX`. `try_step_forward` on `MAX`
106        //     overflows to `None`, so `None == Some(MAX)` is false: not single time.
107        as_of.try_step_forward().as_ref() == until.as_option()
108    }
109}
110
111impl LirDataflowDescription {
112    /// Check invariants expected to be true about `DataflowDescription`s.
113    pub fn check_invariants(&self) -> Result<(), String> {
114        let mut plans: Vec<_> = self.objects_to_build.iter().map(|o| &o.plan).collect();
115        let mut lir_ids = BTreeSet::new();
116
117        while let Some(plan) = plans.pop() {
118            let lir_id = plan.lir_id;
119            if !lir_ids.insert(lir_id) {
120                return Err(format!(
121                    "duplicate `LirId` in `DataflowDescription`: {lir_id}"
122                ));
123            }
124            plans.extend(plan.node.children());
125        }
126
127        Ok(())
128    }
129}
130
131impl DataflowDescription<OptimizedMirRelationExpr, ()> {
132    /// Imports a previously exported index.
133    ///
134    /// This method makes available an index previously exported as `id`, identified
135    /// to the query by `description` (which names the view the index arranges, and
136    /// the keys by which it is arranged).
137    pub fn import_index(
138        &mut self,
139        id: GlobalId,
140        desc: IndexDesc<MirScalarExpr>,
141        typ: ReprRelationType,
142        monotonic: bool,
143    ) {
144        self.index_imports.insert(
145            id,
146            IndexImport {
147                desc,
148                typ,
149                monotonic,
150                with_snapshot: true,
151            },
152        );
153    }
154
155    /// Imports a source and makes it available as `id`.
156    pub fn import_source(&mut self, id: GlobalId, typ: SqlRelationType, monotonic: bool) {
157        // Import the source with no linear operators applied to it.
158        // They may be populated by whole-dataflow optimization.
159        // Similarly, we require the snapshot by default, though optimization may choose to skip it.
160        self.source_imports.insert(
161            id,
162            SourceImport {
163                desc: SourceInstanceDesc {
164                    storage_metadata: (),
165                    arguments: SourceInstanceArguments { operators: None },
166                    typ,
167                },
168                monotonic,
169                with_snapshot: true,
170                upper: Antichain::new(),
171            },
172        );
173    }
174
175    /// Binds to `id` the relation expression `plan`.
176    pub fn insert_plan(&mut self, id: GlobalId, plan: OptimizedMirRelationExpr) {
177        self.objects_to_build.push(BuildDesc { id, plan });
178    }
179
180    /// Exports as `id` an index described by `description`.
181    ///
182    /// Future uses of `import_index` in other dataflow descriptions may use `id`,
183    /// as long as this dataflow has not been terminated in the meantime.
184    pub fn export_index(
185        &mut self,
186        id: GlobalId,
187        description: IndexDesc<MirScalarExpr>,
188        on_type: ReprRelationType,
189    ) {
190        // We first create a "view" named `id` that ensures that the
191        // data are correctly arranged and available for export.
192        self.insert_plan(
193            id,
194            OptimizedMirRelationExpr::declare_optimized(MirRelationExpr::ArrangeBy {
195                input: Box::new(MirRelationExpr::global_get(
196                    description.on_id,
197                    on_type.clone(),
198                )),
199                keys: vec![description.key.clone()],
200            }),
201        );
202        self.index_exports.insert(id, (description, on_type));
203    }
204
205    /// Exports as `id` a sink described by `description`.
206    pub fn export_sink(&mut self, id: GlobalId, description: ComputeSinkDesc<()>) {
207        self.sink_exports.insert(id, description);
208    }
209
210    /// Returns true iff `id` is already imported.
211    pub fn is_imported(&self, id: &GlobalId) -> bool {
212        self.objects_to_build.iter().any(|bd| &bd.id == id)
213            || self.index_imports.keys().any(|i| i == id)
214            || self.source_imports.keys().any(|i| i == id)
215    }
216
217    /// The number of columns associated with an identifier in the dataflow.
218    pub fn arity_of(&self, id: &GlobalId) -> usize {
219        for (source_id, source_import) in self.source_imports.iter() {
220            let source = &source_import.desc;
221            if source_id == id {
222                return source.typ.arity();
223            }
224        }
225        for IndexImport { desc, typ, .. } in self.index_imports.values() {
226            if &desc.on_id == id {
227                return typ.arity();
228            }
229        }
230        for desc in self.objects_to_build.iter() {
231            if &desc.id == id {
232                return desc.plan.arity();
233            }
234        }
235        panic!("GlobalId {} not found in MirDataflowDescription", id);
236    }
237
238    /// Calls r and s on any sub-members of those types in self. Halts at the first error return.
239    pub fn visit_children<R, S, E>(&mut self, r: R, s: S) -> Result<(), E>
240    where
241        R: Fn(&mut OptimizedMirRelationExpr) -> Result<(), E>,
242        S: Fn(&mut MirScalarExpr) -> Result<(), E>,
243    {
244        for BuildDesc { plan, .. } in &mut self.objects_to_build {
245            r(plan)?;
246        }
247        for source_import in self.source_imports.values_mut() {
248            let Some(mfp) = source_import.desc.arguments.operators.as_mut() else {
249                continue;
250            };
251            for expr in mfp.expressions.iter_mut() {
252                s(expr)?;
253            }
254            for (_, expr) in mfp.predicates.iter_mut() {
255                s(expr)?;
256            }
257        }
258        Ok(())
259    }
260}
261
262impl<P, S, O> DataflowDescription<P, S, O> {
263    /// Applies `f` to the operators of every source import, failing on the
264    /// first error.
265    pub fn try_map_source_operators<O2, E>(
266        self,
267        mut f: impl FnMut(O) -> Result<O2, E>,
268    ) -> Result<DataflowDescription<P, S, O2>, E> {
269        let DataflowDescription {
270            source_imports,
271            index_imports,
272            objects_to_build,
273            index_exports,
274            sink_exports,
275            as_of,
276            until,
277            initial_storage_as_of,
278            refresh_schedule,
279            debug_name,
280            time_dependence,
281        } = self;
282        let source_imports = source_imports
283            .into_iter()
284            .map(|(id, import)| {
285                let SourceImport {
286                    desc:
287                        SourceInstanceDesc {
288                            arguments: SourceInstanceArguments { operators },
289                            storage_metadata,
290                            typ,
291                        },
292                    monotonic,
293                    with_snapshot,
294                    upper,
295                } = import;
296                let desc = SourceInstanceDesc {
297                    arguments: SourceInstanceArguments {
298                        operators: operators.map(&mut f).transpose()?,
299                    },
300                    storage_metadata,
301                    typ,
302                };
303                let import = SourceImport {
304                    desc,
305                    monotonic,
306                    with_snapshot,
307                    upper,
308                };
309                Ok((id, import))
310            })
311            .collect::<Result<_, E>>()?;
312        Ok(DataflowDescription {
313            source_imports,
314            index_imports,
315            objects_to_build,
316            index_exports,
317            sink_exports,
318            as_of,
319            until,
320            initial_storage_as_of,
321            refresh_schedule,
322            debug_name,
323            time_dependence,
324        })
325    }
326
327    /// Creates a new dataflow description with a human-readable name.
328    pub fn new(name: String) -> Self {
329        Self {
330            source_imports: Default::default(),
331            index_imports: Default::default(),
332            objects_to_build: Vec::new(),
333            index_exports: Default::default(),
334            sink_exports: Default::default(),
335            as_of: Default::default(),
336            until: Antichain::new(),
337            initial_storage_as_of: None,
338            refresh_schedule: None,
339            debug_name: name,
340            time_dependence: None,
341        }
342    }
343
344    /// Sets the `as_of` frontier to the supplied argument.
345    ///
346    /// This method allows the dataflow to indicate a frontier up through
347    /// which all times should be advanced. This can be done for at least
348    /// two reasons: 1. correctness and 2. performance.
349    ///
350    /// Correctness may require an `as_of` to ensure that historical detail
351    /// is consolidated at representative times that do not present specific
352    /// detail that is not specifically correct. For example, updates may be
353    /// compacted to times that are no longer the source times, but instead
354    /// some byproduct of when compaction was executed; we should not present
355    /// those specific times as meaningfully different from other equivalent
356    /// times.
357    ///
358    /// Performance may benefit from an aggressive `as_of` as it reduces the
359    /// number of distinct moments at which collections vary. Differential
360    /// dataflow will refresh its outputs at each time its inputs change and
361    /// to moderate that we can minimize the volume of distinct input times
362    /// as much as possible.
363    ///
364    /// Generally, one should consider setting `as_of` at least to the `since`
365    /// frontiers of contributing data sources and as aggressively as the
366    /// computation permits.
367    pub fn set_as_of(&mut self, as_of: Antichain<Timestamp>) {
368        self.as_of = Some(as_of);
369    }
370
371    /// Records the initial `as_of` of the storage collection associated with a materialized view.
372    pub fn set_initial_as_of(&mut self, initial_as_of: Antichain<Timestamp>) {
373        self.initial_storage_as_of = Some(initial_as_of);
374    }
375
376    /// Identifiers of imported objects (indexes and sources).
377    pub fn import_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
378        self.imported_index_ids().chain(self.imported_source_ids())
379    }
380
381    /// Identifiers of imported indexes.
382    pub fn imported_index_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
383        self.index_imports.keys().copied()
384    }
385
386    /// Identifiers of imported sources.
387    pub fn imported_source_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
388        self.source_imports.keys().copied()
389    }
390
391    /// Whether `id` names an import of this dataflow, index or source.
392    pub fn is_import(&self, id: &GlobalId) -> bool {
393        self.index_imports.contains_key(id) || self.source_imports.contains_key(id)
394    }
395
396    /// Identifiers of exported objects (indexes and sinks).
397    pub fn export_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
398        self.exported_index_ids().chain(self.exported_sink_ids())
399    }
400
401    /// Identifiers of exported indexes.
402    pub fn exported_index_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
403        self.index_exports.keys().copied()
404    }
405
406    /// Identifiers of exported sinks.
407    pub fn exported_sink_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
408        self.sink_exports.keys().copied()
409    }
410
411    /// Identifiers of exported persist sinks.
412    pub fn persist_sink_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
413        self.sink_exports
414            .iter()
415            .filter_map(|(id, desc)| match desc.connection {
416                ComputeSinkConnection::MaterializedView(_) => Some(*id),
417                _ => None,
418            })
419    }
420
421    /// Identifiers of exported subscribe sinks.
422    pub fn subscribe_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
423        self.sink_exports
424            .iter()
425            .filter_map(|(id, desc)| match desc.connection {
426                ComputeSinkConnection::Subscribe(_) => Some(*id),
427                _ => None,
428            })
429    }
430
431    /// Identifiers of exported copy to sinks.
432    pub fn copy_to_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
433        self.sink_exports
434            .iter()
435            .filter_map(|(id, desc)| match desc.connection {
436                ComputeSinkConnection::CopyToS3Oneshot(_) => Some(*id),
437                _ => None,
438            })
439    }
440
441    /// Produce a `Display`able value containing the import IDs of this dataflow.
442    pub fn display_import_ids(&self) -> impl fmt::Display + '_ {
443        use mz_ore::str::{bracketed, separated};
444        bracketed("[", "]", separated(", ", self.import_ids()))
445    }
446
447    /// Produce a `Display`able value containing the export IDs of this dataflow.
448    pub fn display_export_ids(&self) -> impl fmt::Display + '_ {
449        use mz_ore::str::{bracketed, separated};
450        bracketed("[", "]", separated(", ", self.export_ids()))
451    }
452
453    /// Whether this dataflow installs transient collections.
454    pub fn is_transient(&self) -> bool {
455        self.export_ids().all(|id| id.is_transient())
456    }
457
458    /// Returns the description of the object to build with the specified
459    /// identifier.
460    ///
461    /// # Panics
462    ///
463    /// Panics if `id` is not present in `objects_to_build` exactly once.
464    pub fn build_desc(&self, id: GlobalId) -> &BuildDesc<P> {
465        let mut builds = self.objects_to_build.iter().filter(|build| build.id == id);
466        let build = builds
467            .next()
468            .unwrap_or_else(|| panic!("object to build id {id} unexpectedly missing"));
469        assert!(builds.next().is_none());
470        build
471    }
472
473    /// Returns the id of the dataflow's sink export.
474    ///
475    /// # Panics
476    ///
477    /// Panics if the dataflow has no sink exports or has more than one.
478    pub fn sink_id(&self) -> GlobalId {
479        let sink_exports = &self.sink_exports;
480        let sink_id = sink_exports.keys().into_element();
481        *sink_id
482    }
483}
484
485impl<P, S, O> DataflowDescription<P, S, O>
486where
487    P: CollectionPlan,
488{
489    /// Computes the set of identifiers upon which the specified collection
490    /// identifier depends.
491    ///
492    /// `collection_id` must specify a valid object in `objects_to_build`.
493    ///
494    /// This method includes identifiers for e.g. intermediate views, and should be filtered
495    /// if one only wants sources and indexes.
496    ///
497    /// This method is safe for mutually recursive view definitions.
498    pub fn depends_on(&self, collection_id: GlobalId) -> BTreeSet<GlobalId> {
499        let mut out = BTreeSet::new();
500        self.depends_on_into(collection_id, &mut out);
501        out
502    }
503
504    /// Like `depends_on`, but appends to an existing `BTreeSet`.
505    pub fn depends_on_into(&self, collection_id: GlobalId, out: &mut BTreeSet<GlobalId>) {
506        out.insert(collection_id);
507        if self.source_imports.contains_key(&collection_id) {
508            // The collection is provided by an imported source. Report the
509            // dependency on the source.
510            out.insert(collection_id);
511            return;
512        }
513
514        // NOTE(benesch): we're not smart enough here to know *which* index
515        // for the collection will be used, if one exists, so we have to report
516        // the dependency on all of them.
517        let mut found_index = false;
518        for (index_id, IndexImport { desc, .. }) in &self.index_imports {
519            if desc.on_id == collection_id {
520                // The collection is provided by an imported index. Report the
521                // dependency on the index.
522                out.insert(*index_id);
523                found_index = true;
524            }
525        }
526        if found_index {
527            return;
528        }
529
530        // The collection is not provided by a source or imported index.
531        // It must be a collection whose plan we have handy. Recurse.
532        let build = self.build_desc(collection_id);
533        for id in build.plan.depends_on() {
534            if !out.contains(&id) {
535                self.depends_on_into(id, out)
536            }
537        }
538    }
539
540    /// Computes the set of imports upon which the specified collection depends.
541    ///
542    /// This method behaves like `depends_on` but filters out internal dependencies that are not
543    /// included in the dataflow imports.
544    pub fn depends_on_imports(&self, collection_id: GlobalId) -> BTreeSet<GlobalId> {
545        let deps = self.depends_on(collection_id);
546        deps.into_iter().filter(|id| self.is_import(id)).collect()
547    }
548
549    /// Computes the set of imports the dataflow's exports read, meaning the imports reachable from
550    /// an index export's `on_id` or a sink export's `from`.
551    ///
552    /// A description that has been through the optimizer answers [`Self::import_ids`] here, because
553    /// the optimizer prunes the import list to what the exports read. The two come apart while a
554    /// description is still being assembled, and this is what the prune and the assertion guarding
555    /// it are both defined in terms of. The answer covers the dataflow as a whole, so an import only
556    /// one export reads is still reported, and a dataflow with no exports reports none.
557    ///
558    /// NOTE: On the index side this over-approximates. [`Self::depends_on`] cannot tell which index
559    /// on a collection a plan will use, so reaching a collection reports every index imported on it.
560    /// Pruning index imports needs the exact usage information the MIR pipeline collects, not this.
561    ///
562    /// Panics for an export naming a collection that is neither an import nor built exactly once
563    /// here, which is [`Self::depends_on`]'s precondition on its argument. Rendering resolves the
564    /// same ids, so a description that trips this does not survive being built either.
565    pub fn used_import_ids(&self) -> BTreeSet<GlobalId> {
566        let mut deps = BTreeSet::new();
567        for (index_desc, _typ) in self.index_exports.values() {
568            self.depends_on_into(index_desc.on_id, &mut deps);
569        }
570        for sink_desc in self.sink_exports.values() {
571            self.depends_on_into(sink_desc.from, &mut deps);
572        }
573        deps.retain(|id| self.is_import(id));
574        deps
575    }
576}
577
578impl<S> RenderDataflowDescription<S>
579where
580    S: Clone + PartialEq,
581{
582    /// Determine if a dataflow description is compatible with this dataflow description.
583    ///
584    /// Compatible dataflows have structurally equal exports, imports, and objects to build. The
585    /// `as_of` of the receiver has to be less equal the `other` `as_of`.
586    ///
587    /// Note that this method performs normalization as part of the structural equality checking,
588    /// which involves cloning both `self` and `other`. It is therefore relatively expensive and
589    /// should only be used on cold code paths.
590    ///
591    // TODO: The semantics of this function are only useful for command reconciliation at the moment.
592    pub fn compatible_with(&self, other: &Self) -> bool {
593        let old = self.as_comparable();
594        let new = other.as_comparable();
595
596        let equality = old.index_exports == new.index_exports
597            && old.sink_exports == new.sink_exports
598            && old.objects_to_build == new.objects_to_build
599            && old.index_imports == new.index_imports
600            && old.source_imports == new.source_imports
601            && old.time_dependence == new.time_dependence;
602
603        let partial = if let (Some(old_as_of), Some(new_as_of)) = (&old.as_of, &new.as_of) {
604            timely::PartialOrder::less_equal(old_as_of, new_as_of)
605        } else {
606            false
607        };
608
609        equality && partial
610    }
611
612    /// Returns a `DataflowDescription` that has the same structure as `self` and can be
613    /// structurally compared to other `DataflowDescription`s.
614    ///
615    /// The function normalizes several properties. It replaces transient `GlobalId`s
616    /// that are only used internally (i.e. not imported nor exported) with consecutive IDs
617    /// starting from `t1`. It replaces the source import's `upper` by a dummy value.
618    fn as_comparable(&self) -> Self {
619        let external_ids: BTreeSet<_> = self.import_ids().chain(self.export_ids()).collect();
620
621        let mut id_counter = 0;
622        let mut replacements = BTreeMap::new();
623
624        let mut maybe_replace = |id: GlobalId| {
625            if id.is_transient() && !external_ids.contains(&id) {
626                *replacements.entry(id).or_insert_with(|| {
627                    id_counter += 1;
628                    GlobalId::Transient(id_counter)
629                })
630            } else {
631                id
632            }
633        };
634
635        let mut source_imports = self.source_imports.clone();
636        for import in source_imports.values_mut() {
637            import.upper = Antichain::new();
638        }
639
640        let mut objects_to_build = self.objects_to_build.clone();
641        for object in &mut objects_to_build {
642            object.id = maybe_replace(object.id);
643            object.plan.replace_ids(&mut maybe_replace);
644        }
645
646        let mut index_exports = self.index_exports.clone();
647        for (desc, _typ) in index_exports.values_mut() {
648            desc.on_id = maybe_replace(desc.on_id);
649        }
650
651        let mut sink_exports = self.sink_exports.clone();
652        for desc in sink_exports.values_mut() {
653            desc.from = maybe_replace(desc.from);
654        }
655
656        DataflowDescription {
657            source_imports,
658            index_imports: self.index_imports.clone(),
659            objects_to_build,
660            index_exports,
661            sink_exports,
662            as_of: self.as_of.clone(),
663            until: self.until.clone(),
664            initial_storage_as_of: self.initial_storage_as_of.clone(),
665            refresh_schedule: self.refresh_schedule.clone(),
666            debug_name: self.debug_name.clone(),
667            time_dependence: self.time_dependence.clone(),
668        }
669    }
670}
671
672/// A dataflow of optimized MIR, as the optimizer builds and rewrites it.
673pub type MirDataflowDescription = DataflowDescription<OptimizedMirRelationExpr>;
674
675/// A dataflow lowered to LIR.
676///
677/// Its source operators are already planned. Rendering evaluates them as
678/// given, so lowering is the only place they are planned.
679pub type LirDataflowDescription<S = ()> = DataflowDescription<LirRelationExpr, S, MfpPlan>;
680
681/// A dataflow ready to render.
682pub type RenderDataflowDescription<S> = DataflowDescription<RenderPlan, S, MfpPlan>;
683
684/// An index storing processed updates so they can be queried
685/// or reused in other computations
686#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
687pub struct IndexDesc<E> {
688    /// Identity of the collection the index is on.
689    pub on_id: GlobalId,
690    /// Expressions to be arranged, in order of decreasing primacy.
691    pub key: Vec<E>,
692}
693
694impl IndexDesc<MirScalarExpr> {
695    /// Translate an index description from MIR to LIR.
696    pub fn as_lir(&self) -> IndexDesc<LirScalarExpr> {
697        let on_id = self.on_id.clone();
698        let key = lses_from_mses(&self.key);
699
700        IndexDesc { on_id, key }
701    }
702}
703
704/// Information about an imported index, and how it will be used by the dataflow.
705#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
706pub struct IndexImport {
707    /// Description of index.
708    pub desc: IndexDesc<MirScalarExpr>,
709    /// Schema and keys of the object the index is on.
710    pub typ: ReprRelationType,
711    /// Whether the index will supply monotonic data.
712    pub monotonic: bool,
713    /// Whether this import must include the snapshot data.
714    pub with_snapshot: bool,
715}
716
717/// Information about an imported source, and how it will be used by the dataflow.
718#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
719pub struct SourceImport<S: 'static = (), O = MapFilterProject> {
720    /// Description of the source instance to import.
721    pub desc: SourceInstanceDesc<S, O>,
722    /// Whether the source will supply monotonic data.
723    pub monotonic: bool,
724    /// Whether this import must include the snapshot data.
725    pub with_snapshot: bool,
726    /// The initial known upper frontier for the source.
727    pub upper: Antichain<Timestamp>,
728}
729
730/// An association of a global identifier to an expression.
731#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
732pub struct BuildDesc<P> {
733    /// TODO(database-issues#7533): Add documentation.
734    pub id: GlobalId,
735    /// TODO(database-issues#7533): Add documentation.
736    pub plan: P,
737}
738
739#[cfg(test)]
740mod tests {
741    use mz_expr::{AccessStrategy, Id, MirRelationExpr, UnmaterializableFunc, func};
742    use mz_repr::optimize::OptimizerFeatures;
743    use mz_repr::{RelationDesc, ReprRelationType, ReprScalarType, SqlRelationType};
744
745    use crate::sinks::{ComputeSinkConnection, ComputeSinkDesc, SubscribeSinkConnection};
746    use crate::sources::{SourceInstanceArguments, SourceInstanceDesc};
747
748    use super::*;
749
750    const READ: GlobalId = GlobalId::User(1);
751    const UNREAD: GlobalId = GlobalId::User(2);
752    const VIEW: GlobalId = GlobalId::Transient(1);
753    const SINK: GlobalId = GlobalId::Transient(2);
754    const INDEX: GlobalId = GlobalId::Transient(3);
755
756    fn typ() -> ReprRelationType {
757        ReprRelationType::new(vec![ReprScalarType::Int64.nullable(false)])
758    }
759
760    /// A dataflow importing `READ` and `UNREAD`, building `VIEW` from `plan`, and exporting a
761    /// subscribe sink over it.
762    fn dataflow(plan: MirRelationExpr) -> MirDataflowDescription {
763        let source_import = || SourceImport {
764            desc: SourceInstanceDesc {
765                arguments: SourceInstanceArguments { operators: None },
766                storage_metadata: (),
767                typ: SqlRelationType::from_repr(&typ()),
768            },
769            monotonic: false,
770            with_snapshot: true,
771            upper: Antichain::from_elem(Timestamp::MIN),
772        };
773
774        let mut df = MirDataflowDescription::new("test".to_string());
775        df.source_imports.insert(READ, source_import());
776        df.source_imports.insert(UNREAD, source_import());
777        df.objects_to_build.push(BuildDesc {
778            id: VIEW,
779            plan: OptimizedMirRelationExpr::declare_optimized(plan),
780        });
781        df.sink_exports.insert(
782            SINK,
783            ComputeSinkDesc {
784                from: VIEW,
785                from_desc: RelationDesc::new(SqlRelationType::from_repr(&typ()), ["c"]),
786                connection: ComputeSinkConnection::Subscribe(SubscribeSinkConnection {
787                    output: Vec::new(),
788                }),
789                with_snapshot: true,
790                up_to: Antichain::new(),
791                non_null_assertions: Vec::new(),
792                refresh_schedule: None,
793                from_arrangement: None,
794            },
795        );
796        df
797    }
798
799    #[mz_ore::test]
800    fn used_import_ids_reports_only_read_imports() {
801        let df = dataflow(MirRelationExpr::Get {
802            id: Id::Global(READ),
803            typ: typ(),
804            access_strategy: AccessStrategy::Persist,
805        });
806
807        assert_eq!(df.used_import_ids(), BTreeSet::from([READ]));
808    }
809
810    /// An export the optimizer folded to a constant reads nothing, even though the imports it was
811    /// folded from are still there. This is the shape that must not be reported as reading them.
812    #[mz_ore::test]
813    fn used_import_ids_is_empty_for_a_constant_export() {
814        let df = dataflow(MirRelationExpr::Constant {
815            rows: Ok(Vec::new()),
816            typ: typ(),
817        });
818
819        assert_eq!(df.used_import_ids(), BTreeSet::new());
820    }
821
822    /// Index exports are walked from the collection they are on, just like sink exports are from
823    /// the collection they are from.
824    #[mz_ore::test]
825    fn used_import_ids_covers_index_exports() {
826        let mut df = dataflow(MirRelationExpr::Constant {
827            rows: Ok(Vec::new()),
828            typ: typ(),
829        });
830        let other_view = GlobalId::Transient(4);
831        df.objects_to_build.push(BuildDesc {
832            id: other_view,
833            plan: OptimizedMirRelationExpr::declare_optimized(MirRelationExpr::Get {
834                id: Id::Global(UNREAD),
835                typ: typ(),
836                access_strategy: AccessStrategy::Persist,
837            }),
838        });
839        df.index_exports.insert(
840            INDEX,
841            (
842                IndexDesc {
843                    on_id: other_view,
844                    key: Vec::new(),
845                },
846                typ(),
847            ),
848        );
849
850        assert_eq!(df.used_import_ids(), BTreeSet::from([UNREAD]));
851    }
852
853    /// An imported index is reached through the collection it is on, and reported by the id of the
854    /// index rather than that of the collection.
855    #[mz_ore::test]
856    fn used_import_ids_reports_imported_indexes_by_index_id() {
857        let indexed_view = GlobalId::User(3);
858        let imported_index = GlobalId::User(4);
859
860        let mut df = dataflow(MirRelationExpr::Get {
861            id: Id::Global(indexed_view),
862            typ: typ(),
863            access_strategy: AccessStrategy::Index(Vec::new()),
864        });
865        df.index_imports.insert(
866            imported_index,
867            IndexImport {
868                desc: IndexDesc {
869                    on_id: indexed_view,
870                    key: Vec::new(),
871                },
872                typ: typ(),
873                monotonic: false,
874                with_snapshot: true,
875            },
876        );
877
878        assert_eq!(df.used_import_ids(), BTreeSet::from([imported_index]));
879    }
880
881    /// Builds a minimal dataflow description carrying only the `as_of` and
882    /// `until` frontiers that `is_single_time` inspects.
883    fn desc_with_bounds(
884        as_of: Antichain<Timestamp>,
885        until: Antichain<Timestamp>,
886    ) -> DataflowDescription<OptimizedMirRelationExpr, ()> {
887        let mut desc = DataflowDescription::new("test".to_string());
888        desc.set_as_of(as_of);
889        desc.until = until;
890        desc
891    }
892
893    #[mz_ore::test]
894    fn is_single_time_classification() {
895        // Ordinary single-time dataflow: `until = as_of + 1`.
896        assert!(
897            desc_with_bounds(
898                Antichain::from_elem(5.into()),
899                Antichain::from_elem(6.into())
900            )
901            .is_single_time()
902        );
903
904        // `as_of = MAX` with an empty `until` is single time: `MAX` is the last
905        // timestamp, so an unbounded dataflow starting there sees exactly one time.
906        assert!(
907            desc_with_bounds(Antichain::from_elem(Timestamp::MAX), Antichain::new())
908                .is_single_time()
909        );
910
911        // `as_of = MAX` with `until = {MAX}` is an empty range, produced by
912        // `SUBSCRIBE ... AS OF MAX UP TO MAX`. It must not be classified as single
913        // time, and it must not trip an invariant assertion (see CPU-169).
914        assert!(
915            !desc_with_bounds(
916                Antichain::from_elem(Timestamp::MAX),
917                Antichain::from_elem(Timestamp::MAX)
918            )
919            .is_single_time()
920        );
921    }
922
923    #[mz_ore::test]
924    fn lowering_plans_source_operators() {
925        // `#1 = #0::mz_timestamp`, filtered by `mz_now() < #1` and
926        // `mz_now() = #0::mz_timestamp`.
927        let mz_now = || MirScalarExpr::CallUnmaterializable(UnmaterializableFunc::MzNow);
928        let cast = || MirScalarExpr::column(0).call_unary(func::CastInt64ToMzTimestamp);
929        let mfp = MapFilterProject::new(1).map([cast()]).filter([
930            mz_now().call_binary(MirScalarExpr::column(1), func::Lt),
931            mz_now().call_binary(cast(), func::Eq),
932        ]);
933        let planned = MfpPlan::create_from(mfp.clone()).expect("plannable");
934
935        let mut df = dataflow(MirRelationExpr::Get {
936            id: Id::Global(READ),
937            typ: typ(),
938            access_strategy: AccessStrategy::Persist,
939        });
940        let unread = df.source_imports.get_mut(&UNREAD).expect("imported");
941        unread.desc.arguments.operators = Some(mfp);
942
943        let lowered = LirRelationExpr::finalize_dataflow(df, &OptimizerFeatures::default(), None)
944            .expect("lowers");
945        let operators = &lowered.source_imports[&UNREAD].desc.arguments.operators;
946        assert_eq!(operators.as_ref(), Some(&planned));
947    }
948
949    /// MIR predicate pushdown copies `mz_now() <= #0` into the source's operators and keeps it in
950    /// the `Filter` above the read. Hoisting the read's copy into the source must not leave the
951    /// source evaluating the same bound twice.
952    #[mz_ore::test]
953    fn lowering_merges_pushed_down_temporal_predicate() {
954        let mz_now = || MirScalarExpr::CallUnmaterializable(UnmaterializableFunc::MzNow);
955        let cast = || MirScalarExpr::column(0).call_unary(func::CastInt64ToMzTimestamp);
956        let le = || mz_now().call_binary(cast(), func::Lte);
957
958        let mut df = dataflow(
959            MirRelationExpr::Get {
960                id: Id::Global(READ),
961                typ: typ(),
962                access_strategy: AccessStrategy::Persist,
963            }
964            .filter([le()]),
965        );
966        let read = df.source_imports.get_mut(&READ).expect("imported");
967        read.desc.arguments.operators = Some(MapFilterProject::new(1).filter([le()]));
968
969        let lowered = LirRelationExpr::finalize_dataflow(df, &OptimizerFeatures::default(), None)
970            .expect("lowers");
971        let operators = lowered.source_imports[&READ]
972            .desc
973            .arguments
974            .operators
975            .as_ref()
976            .expect("planned operators");
977        let (_safe, lower, upper) = operators.as_parts();
978        assert!(lower.is_empty(), "{operators:?}");
979        assert_eq!(upper.len(), 1, "{operators:?}");
980    }
981}