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::{CollectionPlan, MirRelationExpr, MirScalarExpr, OptimizedMirRelationExpr};
16use mz_ore::collections::CollectionExt;
17use mz_ore::soft_assert_or_log;
18use mz_repr::refresh_schedule::RefreshSchedule;
19use mz_repr::{GlobalId, ReprRelationType, SqlRelationType, Timestamp};
20use mz_storage_types::time_dependence::TimeDependence;
21use serde::{Deserialize, Serialize};
22use timely::progress::Antichain;
23
24use crate::plan::LirRelationExpr;
25use crate::plan::render_plan::RenderPlan;
26use crate::plan::scalar::{LirScalarExpr, lses_from_mses};
27use crate::sinks::{ComputeSinkConnection, ComputeSinkDesc};
28use crate::sources::{SourceInstanceArguments, SourceInstanceDesc};
29
30/// A description of a dataflow to construct and results to surface.
31#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
32pub struct DataflowDescription<P, S: 'static = ()> {
33 /// Sources instantiations made available to the dataflow pair with monotonicity information.
34 pub source_imports: BTreeMap<GlobalId, SourceImport<S>>,
35 /// Indexes made available to the dataflow.
36 /// (id of index, import)
37 pub index_imports: BTreeMap<GlobalId, IndexImport>,
38 /// Views and indexes to be built and stored in the local context.
39 /// Objects must be built in the specific order, as there may be
40 /// dependencies of later objects on prior identifiers.
41 pub objects_to_build: Vec<BuildDesc<P>>,
42 /// Indexes to be made available to be shared with other dataflows
43 /// (id of new index, description of index, relationtype of base source/view/table)
44 pub index_exports: BTreeMap<GlobalId, (IndexDesc<MirScalarExpr>, ReprRelationType)>,
45 /// sinks to be created
46 /// (id of new sink, description of sink)
47 pub sink_exports: BTreeMap<GlobalId, ComputeSinkDesc<S>>,
48 /// An optional frontier to which inputs should be advanced.
49 ///
50 /// If this is set, it should override the default setting determined by
51 /// the upper bound of `since` frontiers contributing to the dataflow.
52 /// It is an error for this to be set to a frontier not beyond that default.
53 pub as_of: Option<Antichain<Timestamp>>,
54 /// Frontier beyond which the dataflow should not execute.
55 /// Specifically, updates at times greater or equal to this frontier are suppressed.
56 /// This is often set to `as_of + 1` to enable "batch" computations.
57 /// Note that frontier advancements might still happen to times that are after the `until`,
58 /// only data is suppressed. (This is consistent with how frontier advancements can also
59 /// happen before the `as_of`.)
60 pub until: Antichain<Timestamp>,
61 /// The initial as_of when the collection is first created. Filled only for materialized views.
62 /// Note that this doesn't change upon restarts.
63 pub initial_storage_as_of: Option<Antichain<Timestamp>>,
64 /// The schedule of REFRESH materialized views.
65 pub refresh_schedule: Option<RefreshSchedule>,
66 /// Human-readable name
67 pub debug_name: String,
68 /// Description of how the dataflow's progress relates to wall-clock time. None for unknown.
69 pub time_dependence: Option<TimeDependence>,
70}
71
72impl<P, S> DataflowDescription<P, S> {
73 /// Tests if the dataflow refers to a single timestamp, namely
74 /// that `as_of` has a single coordinate and that the `until`
75 /// value corresponds to the `as_of` value plus one, or `as_of`
76 /// is the maximum timestamp and is thus single.
77 pub fn is_single_time(&self) -> bool {
78 // TODO: this would be much easier to check if `until` was a strict lower bound,
79 // and we would be testing that `until == as_of`.
80
81 let until = &self.until;
82
83 // IF `as_of` is not set at all this can't be a single time dataflow.
84 let Some(as_of) = self.as_of.as_ref() else {
85 return false;
86 };
87 // Ensure that as_of <= until.
88 soft_assert_or_log!(
89 timely::PartialOrder::less_equal(as_of, until),
90 "expected empty `as_of ≤ until`, got `{as_of:?} ≰ {until:?}`",
91 );
92 // IF `as_of` is not a single timestamp this can't be a single time dataflow.
93 let Some(as_of) = as_of.as_option() else {
94 return false;
95 };
96 // Ensure that `as_of = MAX` implies `until.is_empty()`.
97 soft_assert_or_log!(
98 as_of != &mz_repr::Timestamp::MAX || until.is_empty(),
99 "expected `until = {{}}` due to `as_of = MAX`, got `until = {until:?}`",
100 );
101 // Note that the `(as_of = MAX, until = {})` case also returns `true`
102 // here (as expected) since we are going to compare two `None` values.
103 as_of.try_step_forward().as_ref() == until.as_option()
104 }
105}
106
107impl DataflowDescription<LirRelationExpr, ()> {
108 /// Check invariants expected to be true about `DataflowDescription`s.
109 pub fn check_invariants(&self) -> Result<(), String> {
110 let mut plans: Vec<_> = self.objects_to_build.iter().map(|o| &o.plan).collect();
111 let mut lir_ids = BTreeSet::new();
112
113 while let Some(plan) = plans.pop() {
114 let lir_id = plan.lir_id;
115 if !lir_ids.insert(lir_id) {
116 return Err(format!(
117 "duplicate `LirId` in `DataflowDescription`: {lir_id}"
118 ));
119 }
120 plans.extend(plan.node.children());
121 }
122
123 Ok(())
124 }
125}
126
127impl DataflowDescription<OptimizedMirRelationExpr, ()> {
128 /// Imports a previously exported index.
129 ///
130 /// This method makes available an index previously exported as `id`, identified
131 /// to the query by `description` (which names the view the index arranges, and
132 /// the keys by which it is arranged).
133 pub fn import_index(
134 &mut self,
135 id: GlobalId,
136 desc: IndexDesc<MirScalarExpr>,
137 typ: ReprRelationType,
138 monotonic: bool,
139 ) {
140 self.index_imports.insert(
141 id,
142 IndexImport {
143 desc,
144 typ,
145 monotonic,
146 with_snapshot: true,
147 },
148 );
149 }
150
151 /// Imports a source and makes it available as `id`.
152 pub fn import_source(&mut self, id: GlobalId, typ: SqlRelationType, monotonic: bool) {
153 // Import the source with no linear operators applied to it.
154 // They may be populated by whole-dataflow optimization.
155 // Similarly, we require the snapshot by default, though optimization may choose to skip it.
156 self.source_imports.insert(
157 id,
158 SourceImport {
159 desc: SourceInstanceDesc {
160 storage_metadata: (),
161 arguments: SourceInstanceArguments { operators: None },
162 typ,
163 },
164 monotonic,
165 with_snapshot: true,
166 upper: Antichain::new(),
167 },
168 );
169 }
170
171 /// Binds to `id` the relation expression `plan`.
172 pub fn insert_plan(&mut self, id: GlobalId, plan: OptimizedMirRelationExpr) {
173 self.objects_to_build.push(BuildDesc { id, plan });
174 }
175
176 /// Exports as `id` an index described by `description`.
177 ///
178 /// Future uses of `import_index` in other dataflow descriptions may use `id`,
179 /// as long as this dataflow has not been terminated in the meantime.
180 pub fn export_index(
181 &mut self,
182 id: GlobalId,
183 description: IndexDesc<MirScalarExpr>,
184 on_type: ReprRelationType,
185 ) {
186 // We first create a "view" named `id` that ensures that the
187 // data are correctly arranged and available for export.
188 self.insert_plan(
189 id,
190 OptimizedMirRelationExpr::declare_optimized(MirRelationExpr::ArrangeBy {
191 input: Box::new(MirRelationExpr::global_get(
192 description.on_id,
193 on_type.clone(),
194 )),
195 keys: vec![description.key.clone()],
196 }),
197 );
198 self.index_exports.insert(id, (description, on_type));
199 }
200
201 /// Exports as `id` a sink described by `description`.
202 pub fn export_sink(&mut self, id: GlobalId, description: ComputeSinkDesc<()>) {
203 self.sink_exports.insert(id, description);
204 }
205
206 /// Returns true iff `id` is already imported.
207 pub fn is_imported(&self, id: &GlobalId) -> bool {
208 self.objects_to_build.iter().any(|bd| &bd.id == id)
209 || self.index_imports.keys().any(|i| i == id)
210 || self.source_imports.keys().any(|i| i == id)
211 }
212
213 /// The number of columns associated with an identifier in the dataflow.
214 pub fn arity_of(&self, id: &GlobalId) -> usize {
215 for (source_id, source_import) in self.source_imports.iter() {
216 let source = &source_import.desc;
217 if source_id == id {
218 return source.typ.arity();
219 }
220 }
221 for IndexImport { desc, typ, .. } in self.index_imports.values() {
222 if &desc.on_id == id {
223 return typ.arity();
224 }
225 }
226 for desc in self.objects_to_build.iter() {
227 if &desc.id == id {
228 return desc.plan.arity();
229 }
230 }
231 panic!("GlobalId {} not found in DataflowDesc", id);
232 }
233
234 /// Calls r and s on any sub-members of those types in self. Halts at the first error return.
235 pub fn visit_children<R, S, E>(&mut self, r: R, s: S) -> Result<(), E>
236 where
237 R: Fn(&mut OptimizedMirRelationExpr) -> Result<(), E>,
238 S: Fn(&mut MirScalarExpr) -> Result<(), E>,
239 {
240 for BuildDesc { plan, .. } in &mut self.objects_to_build {
241 r(plan)?;
242 }
243 for source_import in self.source_imports.values_mut() {
244 let Some(mfp) = source_import.desc.arguments.operators.as_mut() else {
245 continue;
246 };
247 for expr in mfp.expressions.iter_mut() {
248 s(expr)?;
249 }
250 for (_, expr) in mfp.predicates.iter_mut() {
251 s(expr)?;
252 }
253 }
254 Ok(())
255 }
256}
257
258impl<P, S> DataflowDescription<P, S> {
259 /// Creates a new dataflow description with a human-readable name.
260 pub fn new(name: String) -> Self {
261 Self {
262 source_imports: Default::default(),
263 index_imports: Default::default(),
264 objects_to_build: Vec::new(),
265 index_exports: Default::default(),
266 sink_exports: Default::default(),
267 as_of: Default::default(),
268 until: Antichain::new(),
269 initial_storage_as_of: None,
270 refresh_schedule: None,
271 debug_name: name,
272 time_dependence: None,
273 }
274 }
275
276 /// Sets the `as_of` frontier to the supplied argument.
277 ///
278 /// This method allows the dataflow to indicate a frontier up through
279 /// which all times should be advanced. This can be done for at least
280 /// two reasons: 1. correctness and 2. performance.
281 ///
282 /// Correctness may require an `as_of` to ensure that historical detail
283 /// is consolidated at representative times that do not present specific
284 /// detail that is not specifically correct. For example, updates may be
285 /// compacted to times that are no longer the source times, but instead
286 /// some byproduct of when compaction was executed; we should not present
287 /// those specific times as meaningfully different from other equivalent
288 /// times.
289 ///
290 /// Performance may benefit from an aggressive `as_of` as it reduces the
291 /// number of distinct moments at which collections vary. Differential
292 /// dataflow will refresh its outputs at each time its inputs change and
293 /// to moderate that we can minimize the volume of distinct input times
294 /// as much as possible.
295 ///
296 /// Generally, one should consider setting `as_of` at least to the `since`
297 /// frontiers of contributing data sources and as aggressively as the
298 /// computation permits.
299 pub fn set_as_of(&mut self, as_of: Antichain<Timestamp>) {
300 self.as_of = Some(as_of);
301 }
302
303 /// Records the initial `as_of` of the storage collection associated with a materialized view.
304 pub fn set_initial_as_of(&mut self, initial_as_of: Antichain<Timestamp>) {
305 self.initial_storage_as_of = Some(initial_as_of);
306 }
307
308 /// Identifiers of imported objects (indexes and sources).
309 pub fn import_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
310 self.imported_index_ids().chain(self.imported_source_ids())
311 }
312
313 /// Identifiers of imported indexes.
314 pub fn imported_index_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
315 self.index_imports.keys().copied()
316 }
317
318 /// Identifiers of imported sources.
319 pub fn imported_source_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
320 self.source_imports.keys().copied()
321 }
322
323 /// Identifiers of exported objects (indexes and sinks).
324 pub fn export_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
325 self.exported_index_ids().chain(self.exported_sink_ids())
326 }
327
328 /// Identifiers of exported indexes.
329 pub fn exported_index_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
330 self.index_exports.keys().copied()
331 }
332
333 /// Identifiers of exported sinks.
334 pub fn exported_sink_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
335 self.sink_exports.keys().copied()
336 }
337
338 /// Identifiers of exported persist sinks.
339 pub fn persist_sink_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
340 self.sink_exports
341 .iter()
342 .filter_map(|(id, desc)| match desc.connection {
343 ComputeSinkConnection::MaterializedView(_) => Some(*id),
344 _ => None,
345 })
346 }
347
348 /// Identifiers of exported subscribe sinks.
349 pub fn subscribe_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
350 self.sink_exports
351 .iter()
352 .filter_map(|(id, desc)| match desc.connection {
353 ComputeSinkConnection::Subscribe(_) => Some(*id),
354 _ => None,
355 })
356 }
357
358 /// Identifiers of exported copy to sinks.
359 pub fn copy_to_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
360 self.sink_exports
361 .iter()
362 .filter_map(|(id, desc)| match desc.connection {
363 ComputeSinkConnection::CopyToS3Oneshot(_) => Some(*id),
364 _ => None,
365 })
366 }
367
368 /// Produce a `Display`able value containing the import IDs of this dataflow.
369 pub fn display_import_ids(&self) -> impl fmt::Display + '_ {
370 use mz_ore::str::{bracketed, separated};
371 bracketed("[", "]", separated(", ", self.import_ids()))
372 }
373
374 /// Produce a `Display`able value containing the export IDs of this dataflow.
375 pub fn display_export_ids(&self) -> impl fmt::Display + '_ {
376 use mz_ore::str::{bracketed, separated};
377 bracketed("[", "]", separated(", ", self.export_ids()))
378 }
379
380 /// Whether this dataflow installs transient collections.
381 pub fn is_transient(&self) -> bool {
382 self.export_ids().all(|id| id.is_transient())
383 }
384
385 /// Returns the description of the object to build with the specified
386 /// identifier.
387 ///
388 /// # Panics
389 ///
390 /// Panics if `id` is not present in `objects_to_build` exactly once.
391 pub fn build_desc(&self, id: GlobalId) -> &BuildDesc<P> {
392 let mut builds = self.objects_to_build.iter().filter(|build| build.id == id);
393 let build = builds
394 .next()
395 .unwrap_or_else(|| panic!("object to build id {id} unexpectedly missing"));
396 assert!(builds.next().is_none());
397 build
398 }
399
400 /// Returns the id of the dataflow's sink export.
401 ///
402 /// # Panics
403 ///
404 /// Panics if the dataflow has no sink exports or has more than one.
405 pub fn sink_id(&self) -> GlobalId {
406 let sink_exports = &self.sink_exports;
407 let sink_id = sink_exports.keys().into_element();
408 *sink_id
409 }
410}
411
412impl<P, S> DataflowDescription<P, S>
413where
414 P: CollectionPlan,
415{
416 /// Computes the set of identifiers upon which the specified collection
417 /// identifier depends.
418 ///
419 /// `collection_id` must specify a valid object in `objects_to_build`.
420 ///
421 /// This method includes identifiers for e.g. intermediate views, and should be filtered
422 /// if one only wants sources and indexes.
423 ///
424 /// This method is safe for mutually recursive view definitions.
425 pub fn depends_on(&self, collection_id: GlobalId) -> BTreeSet<GlobalId> {
426 let mut out = BTreeSet::new();
427 self.depends_on_into(collection_id, &mut out);
428 out
429 }
430
431 /// Like `depends_on`, but appends to an existing `BTreeSet`.
432 pub fn depends_on_into(&self, collection_id: GlobalId, out: &mut BTreeSet<GlobalId>) {
433 out.insert(collection_id);
434 if self.source_imports.contains_key(&collection_id) {
435 // The collection is provided by an imported source. Report the
436 // dependency on the source.
437 out.insert(collection_id);
438 return;
439 }
440
441 // NOTE(benesch): we're not smart enough here to know *which* index
442 // for the collection will be used, if one exists, so we have to report
443 // the dependency on all of them.
444 let mut found_index = false;
445 for (index_id, IndexImport { desc, .. }) in &self.index_imports {
446 if desc.on_id == collection_id {
447 // The collection is provided by an imported index. Report the
448 // dependency on the index.
449 out.insert(*index_id);
450 found_index = true;
451 }
452 }
453 if found_index {
454 return;
455 }
456
457 // The collection is not provided by a source or imported index.
458 // It must be a collection whose plan we have handy. Recurse.
459 let build = self.build_desc(collection_id);
460 for id in build.plan.depends_on() {
461 if !out.contains(&id) {
462 self.depends_on_into(id, out)
463 }
464 }
465 }
466
467 /// Computes the set of imports upon which the specified collection depends.
468 ///
469 /// This method behaves like `depends_on` but filters out internal dependencies that are not
470 /// included in the dataflow imports.
471 pub fn depends_on_imports(&self, collection_id: GlobalId) -> BTreeSet<GlobalId> {
472 let is_import = |id: &GlobalId| {
473 self.source_imports.contains_key(id) || self.index_imports.contains_key(id)
474 };
475
476 let deps = self.depends_on(collection_id);
477 deps.into_iter().filter(is_import).collect()
478 }
479}
480
481impl<S> DataflowDescription<RenderPlan, S>
482where
483 S: Clone + PartialEq,
484{
485 /// Determine if a dataflow description is compatible with this dataflow description.
486 ///
487 /// Compatible dataflows have structurally equal exports, imports, and objects to build. The
488 /// `as_of` of the receiver has to be less equal the `other` `as_of`.
489 ///
490 /// Note that this method performs normalization as part of the structural equality checking,
491 /// which involves cloning both `self` and `other`. It is therefore relatively expensive and
492 /// should only be used on cold code paths.
493 ///
494 // TODO: The semantics of this function are only useful for command reconciliation at the moment.
495 pub fn compatible_with(&self, other: &Self) -> bool {
496 let old = self.as_comparable();
497 let new = other.as_comparable();
498
499 let equality = old.index_exports == new.index_exports
500 && old.sink_exports == new.sink_exports
501 && old.objects_to_build == new.objects_to_build
502 && old.index_imports == new.index_imports
503 && old.source_imports == new.source_imports
504 && old.time_dependence == new.time_dependence;
505
506 let partial = if let (Some(old_as_of), Some(new_as_of)) = (&old.as_of, &new.as_of) {
507 timely::PartialOrder::less_equal(old_as_of, new_as_of)
508 } else {
509 false
510 };
511
512 equality && partial
513 }
514
515 /// Returns a `DataflowDescription` that has the same structure as `self` and can be
516 /// structurally compared to other `DataflowDescription`s.
517 ///
518 /// The function normalizes several properties. It replaces transient `GlobalId`s
519 /// that are only used internally (i.e. not imported nor exported) with consecutive IDs
520 /// starting from `t1`. It replaces the source import's `upper` by a dummy value.
521 fn as_comparable(&self) -> Self {
522 let external_ids: BTreeSet<_> = self.import_ids().chain(self.export_ids()).collect();
523
524 let mut id_counter = 0;
525 let mut replacements = BTreeMap::new();
526
527 let mut maybe_replace = |id: GlobalId| {
528 if id.is_transient() && !external_ids.contains(&id) {
529 *replacements.entry(id).or_insert_with(|| {
530 id_counter += 1;
531 GlobalId::Transient(id_counter)
532 })
533 } else {
534 id
535 }
536 };
537
538 let mut source_imports = self.source_imports.clone();
539 for import in source_imports.values_mut() {
540 import.upper = Antichain::new();
541 }
542
543 let mut objects_to_build = self.objects_to_build.clone();
544 for object in &mut objects_to_build {
545 object.id = maybe_replace(object.id);
546 object.plan.replace_ids(&mut maybe_replace);
547 }
548
549 let mut index_exports = self.index_exports.clone();
550 for (desc, _typ) in index_exports.values_mut() {
551 desc.on_id = maybe_replace(desc.on_id);
552 }
553
554 let mut sink_exports = self.sink_exports.clone();
555 for desc in sink_exports.values_mut() {
556 desc.from = maybe_replace(desc.from);
557 }
558
559 DataflowDescription {
560 source_imports,
561 index_imports: self.index_imports.clone(),
562 objects_to_build,
563 index_exports,
564 sink_exports,
565 as_of: self.as_of.clone(),
566 until: self.until.clone(),
567 initial_storage_as_of: self.initial_storage_as_of.clone(),
568 refresh_schedule: self.refresh_schedule.clone(),
569 debug_name: self.debug_name.clone(),
570 time_dependence: self.time_dependence.clone(),
571 }
572 }
573}
574
575/// A commonly used name for dataflows contain MIR expressions.
576pub type DataflowDesc = DataflowDescription<OptimizedMirRelationExpr, ()>;
577
578/// An index storing processed updates so they can be queried
579/// or reused in other computations
580#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
581pub struct IndexDesc<E> {
582 /// Identity of the collection the index is on.
583 pub on_id: GlobalId,
584 /// Expressions to be arranged, in order of decreasing primacy.
585 pub key: Vec<E>,
586}
587
588impl IndexDesc<MirScalarExpr> {
589 /// Translate an index description from MIR to LIR.
590 pub fn as_lir(&self) -> IndexDesc<LirScalarExpr> {
591 let on_id = self.on_id.clone();
592 let key = lses_from_mses(&self.key);
593
594 IndexDesc { on_id, key }
595 }
596}
597
598/// Information about an imported index, and how it will be used by the dataflow.
599#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
600pub struct IndexImport {
601 /// Description of index.
602 pub desc: IndexDesc<MirScalarExpr>,
603 /// Schema and keys of the object the index is on.
604 pub typ: ReprRelationType,
605 /// Whether the index will supply monotonic data.
606 pub monotonic: bool,
607 /// Whether this import must include the snapshot data.
608 pub with_snapshot: bool,
609}
610
611/// Information about an imported source, and how it will be used by the dataflow.
612#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
613pub struct SourceImport<S: 'static = ()> {
614 /// Description of the source instance to import.
615 pub desc: SourceInstanceDesc<S>,
616 /// Whether the source will supply monotonic data.
617 pub monotonic: bool,
618 /// Whether this import must include the snapshot data.
619 pub with_snapshot: bool,
620 /// The initial known upper frontier for the source.
621 pub upper: Antichain<Timestamp>,
622}
623
624/// An association of a global identifier to an expression.
625#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
626pub struct BuildDesc<P> {
627 /// TODO(database-issues#7533): Add documentation.
628 pub id: GlobalId,
629 /// TODO(database-issues#7533): Add documentation.
630 pub plan: P,
631}