1use 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#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
32pub struct DataflowDescription<P, S: 'static = ()> {
33 pub source_imports: BTreeMap<GlobalId, SourceImport<S>>,
35 pub index_imports: BTreeMap<GlobalId, IndexImport>,
38 pub objects_to_build: Vec<BuildDesc<P>>,
42 pub index_exports: BTreeMap<GlobalId, (IndexDesc<MirScalarExpr>, ReprRelationType)>,
45 pub sink_exports: BTreeMap<GlobalId, ComputeSinkDesc<S>>,
48 pub as_of: Option<Antichain<Timestamp>>,
54 pub until: Antichain<Timestamp>,
61 pub initial_storage_as_of: Option<Antichain<Timestamp>>,
64 pub refresh_schedule: Option<RefreshSchedule>,
66 pub debug_name: String,
68 pub time_dependence: Option<TimeDependence>,
70}
71
72impl<P, S> DataflowDescription<P, S> {
73 pub fn is_single_time(&self) -> bool {
78 let until = &self.until;
82
83 let Some(as_of) = self.as_of.as_ref() else {
85 return false;
86 };
87 soft_assert_or_log!(
89 timely::PartialOrder::less_equal(as_of, until),
90 "expected empty `as_of ≤ until`, got `{as_of:?} ≰ {until:?}`",
91 );
92 let Some(as_of) = as_of.as_option() else {
94 return false;
95 };
96 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 as_of.try_step_forward().as_ref() == until.as_option()
104 }
105}
106
107impl DataflowDescription<LirRelationExpr, ()> {
108 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 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 pub fn import_source(&mut self, id: GlobalId, typ: SqlRelationType, monotonic: bool) {
153 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 pub fn insert_plan(&mut self, id: GlobalId, plan: OptimizedMirRelationExpr) {
173 self.objects_to_build.push(BuildDesc { id, plan });
174 }
175
176 pub fn export_index(
181 &mut self,
182 id: GlobalId,
183 description: IndexDesc<MirScalarExpr>,
184 on_type: ReprRelationType,
185 ) {
186 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 pub fn export_sink(&mut self, id: GlobalId, description: ComputeSinkDesc<()>) {
203 self.sink_exports.insert(id, description);
204 }
205
206 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 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 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 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 pub fn set_as_of(&mut self, as_of: Antichain<Timestamp>) {
300 self.as_of = Some(as_of);
301 }
302
303 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 pub fn import_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
310 self.imported_index_ids().chain(self.imported_source_ids())
311 }
312
313 pub fn imported_index_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
315 self.index_imports.keys().copied()
316 }
317
318 pub fn imported_source_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
320 self.source_imports.keys().copied()
321 }
322
323 pub fn is_import(&self, id: &GlobalId) -> bool {
325 self.index_imports.contains_key(id) || self.source_imports.contains_key(id)
326 }
327
328 pub fn export_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
330 self.exported_index_ids().chain(self.exported_sink_ids())
331 }
332
333 pub fn exported_index_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
335 self.index_exports.keys().copied()
336 }
337
338 pub fn exported_sink_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
340 self.sink_exports.keys().copied()
341 }
342
343 pub fn persist_sink_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
345 self.sink_exports
346 .iter()
347 .filter_map(|(id, desc)| match desc.connection {
348 ComputeSinkConnection::MaterializedView(_) => Some(*id),
349 _ => None,
350 })
351 }
352
353 pub fn subscribe_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
355 self.sink_exports
356 .iter()
357 .filter_map(|(id, desc)| match desc.connection {
358 ComputeSinkConnection::Subscribe(_) => Some(*id),
359 _ => None,
360 })
361 }
362
363 pub fn copy_to_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
365 self.sink_exports
366 .iter()
367 .filter_map(|(id, desc)| match desc.connection {
368 ComputeSinkConnection::CopyToS3Oneshot(_) => Some(*id),
369 _ => None,
370 })
371 }
372
373 pub fn display_import_ids(&self) -> impl fmt::Display + '_ {
375 use mz_ore::str::{bracketed, separated};
376 bracketed("[", "]", separated(", ", self.import_ids()))
377 }
378
379 pub fn display_export_ids(&self) -> impl fmt::Display + '_ {
381 use mz_ore::str::{bracketed, separated};
382 bracketed("[", "]", separated(", ", self.export_ids()))
383 }
384
385 pub fn is_transient(&self) -> bool {
387 self.export_ids().all(|id| id.is_transient())
388 }
389
390 pub fn build_desc(&self, id: GlobalId) -> &BuildDesc<P> {
397 let mut builds = self.objects_to_build.iter().filter(|build| build.id == id);
398 let build = builds
399 .next()
400 .unwrap_or_else(|| panic!("object to build id {id} unexpectedly missing"));
401 assert!(builds.next().is_none());
402 build
403 }
404
405 pub fn sink_id(&self) -> GlobalId {
411 let sink_exports = &self.sink_exports;
412 let sink_id = sink_exports.keys().into_element();
413 *sink_id
414 }
415}
416
417impl<P, S> DataflowDescription<P, S>
418where
419 P: CollectionPlan,
420{
421 pub fn depends_on(&self, collection_id: GlobalId) -> BTreeSet<GlobalId> {
431 let mut out = BTreeSet::new();
432 self.depends_on_into(collection_id, &mut out);
433 out
434 }
435
436 pub fn depends_on_into(&self, collection_id: GlobalId, out: &mut BTreeSet<GlobalId>) {
438 out.insert(collection_id);
439 if self.source_imports.contains_key(&collection_id) {
440 out.insert(collection_id);
443 return;
444 }
445
446 let mut found_index = false;
450 for (index_id, IndexImport { desc, .. }) in &self.index_imports {
451 if desc.on_id == collection_id {
452 out.insert(*index_id);
455 found_index = true;
456 }
457 }
458 if found_index {
459 return;
460 }
461
462 let build = self.build_desc(collection_id);
465 for id in build.plan.depends_on() {
466 if !out.contains(&id) {
467 self.depends_on_into(id, out)
468 }
469 }
470 }
471
472 pub fn depends_on_imports(&self, collection_id: GlobalId) -> BTreeSet<GlobalId> {
477 let deps = self.depends_on(collection_id);
478 deps.into_iter().filter(|id| self.is_import(id)).collect()
479 }
480
481 pub fn used_import_ids(&self) -> BTreeSet<GlobalId> {
498 let mut deps = BTreeSet::new();
499 for (index_desc, _typ) in self.index_exports.values() {
500 self.depends_on_into(index_desc.on_id, &mut deps);
501 }
502 for sink_desc in self.sink_exports.values() {
503 self.depends_on_into(sink_desc.from, &mut deps);
504 }
505 deps.retain(|id| self.is_import(id));
506 deps
507 }
508}
509
510impl<S> DataflowDescription<RenderPlan, S>
511where
512 S: Clone + PartialEq,
513{
514 pub fn compatible_with(&self, other: &Self) -> bool {
525 let old = self.as_comparable();
526 let new = other.as_comparable();
527
528 let equality = old.index_exports == new.index_exports
529 && old.sink_exports == new.sink_exports
530 && old.objects_to_build == new.objects_to_build
531 && old.index_imports == new.index_imports
532 && old.source_imports == new.source_imports
533 && old.time_dependence == new.time_dependence;
534
535 let partial = if let (Some(old_as_of), Some(new_as_of)) = (&old.as_of, &new.as_of) {
536 timely::PartialOrder::less_equal(old_as_of, new_as_of)
537 } else {
538 false
539 };
540
541 equality && partial
542 }
543
544 fn as_comparable(&self) -> Self {
551 let external_ids: BTreeSet<_> = self.import_ids().chain(self.export_ids()).collect();
552
553 let mut id_counter = 0;
554 let mut replacements = BTreeMap::new();
555
556 let mut maybe_replace = |id: GlobalId| {
557 if id.is_transient() && !external_ids.contains(&id) {
558 *replacements.entry(id).or_insert_with(|| {
559 id_counter += 1;
560 GlobalId::Transient(id_counter)
561 })
562 } else {
563 id
564 }
565 };
566
567 let mut source_imports = self.source_imports.clone();
568 for import in source_imports.values_mut() {
569 import.upper = Antichain::new();
570 }
571
572 let mut objects_to_build = self.objects_to_build.clone();
573 for object in &mut objects_to_build {
574 object.id = maybe_replace(object.id);
575 object.plan.replace_ids(&mut maybe_replace);
576 }
577
578 let mut index_exports = self.index_exports.clone();
579 for (desc, _typ) in index_exports.values_mut() {
580 desc.on_id = maybe_replace(desc.on_id);
581 }
582
583 let mut sink_exports = self.sink_exports.clone();
584 for desc in sink_exports.values_mut() {
585 desc.from = maybe_replace(desc.from);
586 }
587
588 DataflowDescription {
589 source_imports,
590 index_imports: self.index_imports.clone(),
591 objects_to_build,
592 index_exports,
593 sink_exports,
594 as_of: self.as_of.clone(),
595 until: self.until.clone(),
596 initial_storage_as_of: self.initial_storage_as_of.clone(),
597 refresh_schedule: self.refresh_schedule.clone(),
598 debug_name: self.debug_name.clone(),
599 time_dependence: self.time_dependence.clone(),
600 }
601 }
602}
603
604pub type DataflowDesc = DataflowDescription<OptimizedMirRelationExpr, ()>;
606
607#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
610pub struct IndexDesc<E> {
611 pub on_id: GlobalId,
613 pub key: Vec<E>,
615}
616
617impl IndexDesc<MirScalarExpr> {
618 pub fn as_lir(&self) -> IndexDesc<LirScalarExpr> {
620 let on_id = self.on_id.clone();
621 let key = lses_from_mses(&self.key);
622
623 IndexDesc { on_id, key }
624 }
625}
626
627#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
629pub struct IndexImport {
630 pub desc: IndexDesc<MirScalarExpr>,
632 pub typ: ReprRelationType,
634 pub monotonic: bool,
636 pub with_snapshot: bool,
638}
639
640#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
642pub struct SourceImport<S: 'static = ()> {
643 pub desc: SourceInstanceDesc<S>,
645 pub monotonic: bool,
647 pub with_snapshot: bool,
649 pub upper: Antichain<Timestamp>,
651}
652
653#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
655pub struct BuildDesc<P> {
656 pub id: GlobalId,
658 pub plan: P,
660}
661
662#[cfg(test)]
663mod tests {
664 use mz_expr::{AccessStrategy, Id, MirRelationExpr};
665 use mz_repr::{RelationDesc, ReprRelationType, ReprScalarType, SqlRelationType};
666
667 use crate::sinks::{ComputeSinkConnection, ComputeSinkDesc, SubscribeSinkConnection};
668 use crate::sources::{SourceInstanceArguments, SourceInstanceDesc};
669
670 use super::*;
671
672 const READ: GlobalId = GlobalId::User(1);
673 const UNREAD: GlobalId = GlobalId::User(2);
674 const VIEW: GlobalId = GlobalId::Transient(1);
675 const SINK: GlobalId = GlobalId::Transient(2);
676 const INDEX: GlobalId = GlobalId::Transient(3);
677
678 fn typ() -> ReprRelationType {
679 ReprRelationType::new(vec![ReprScalarType::Int64.nullable(false)])
680 }
681
682 fn dataflow(plan: MirRelationExpr) -> DataflowDesc {
685 let source_import = || SourceImport {
686 desc: SourceInstanceDesc {
687 arguments: SourceInstanceArguments { operators: None },
688 storage_metadata: (),
689 typ: SqlRelationType::from_repr(&typ()),
690 },
691 monotonic: false,
692 with_snapshot: true,
693 upper: Antichain::from_elem(Timestamp::MIN),
694 };
695
696 let mut df = DataflowDesc::new("test".to_string());
697 df.source_imports.insert(READ, source_import());
698 df.source_imports.insert(UNREAD, source_import());
699 df.objects_to_build.push(BuildDesc {
700 id: VIEW,
701 plan: OptimizedMirRelationExpr::declare_optimized(plan),
702 });
703 df.sink_exports.insert(
704 SINK,
705 ComputeSinkDesc {
706 from: VIEW,
707 from_desc: RelationDesc::new(SqlRelationType::from_repr(&typ()), ["c"]),
708 connection: ComputeSinkConnection::Subscribe(SubscribeSinkConnection {
709 output: Vec::new(),
710 }),
711 with_snapshot: true,
712 up_to: Antichain::new(),
713 non_null_assertions: Vec::new(),
714 refresh_schedule: None,
715 },
716 );
717 df
718 }
719
720 #[mz_ore::test]
721 fn used_import_ids_reports_only_read_imports() {
722 let df = dataflow(MirRelationExpr::Get {
723 id: Id::Global(READ),
724 typ: typ(),
725 access_strategy: AccessStrategy::Persist,
726 });
727
728 assert_eq!(df.used_import_ids(), BTreeSet::from([READ]));
729 }
730
731 #[mz_ore::test]
734 fn used_import_ids_is_empty_for_a_constant_export() {
735 let df = dataflow(MirRelationExpr::Constant {
736 rows: Ok(Vec::new()),
737 typ: typ(),
738 });
739
740 assert_eq!(df.used_import_ids(), BTreeSet::new());
741 }
742
743 #[mz_ore::test]
746 fn used_import_ids_covers_index_exports() {
747 let mut df = dataflow(MirRelationExpr::Constant {
748 rows: Ok(Vec::new()),
749 typ: typ(),
750 });
751 let other_view = GlobalId::Transient(4);
752 df.objects_to_build.push(BuildDesc {
753 id: other_view,
754 plan: OptimizedMirRelationExpr::declare_optimized(MirRelationExpr::Get {
755 id: Id::Global(UNREAD),
756 typ: typ(),
757 access_strategy: AccessStrategy::Persist,
758 }),
759 });
760 df.index_exports.insert(
761 INDEX,
762 (
763 IndexDesc {
764 on_id: other_view,
765 key: Vec::new(),
766 },
767 typ(),
768 ),
769 );
770
771 assert_eq!(df.used_import_ids(), BTreeSet::from([UNREAD]));
772 }
773
774 #[mz_ore::test]
777 fn used_import_ids_reports_imported_indexes_by_index_id() {
778 let indexed_view = GlobalId::User(3);
779 let imported_index = GlobalId::User(4);
780
781 let mut df = dataflow(MirRelationExpr::Get {
782 id: Id::Global(indexed_view),
783 typ: typ(),
784 access_strategy: AccessStrategy::Index(Vec::new()),
785 });
786 df.index_imports.insert(
787 imported_index,
788 IndexImport {
789 desc: IndexDesc {
790 on_id: indexed_view,
791 key: Vec::new(),
792 },
793 typ: typ(),
794 monotonic: false,
795 with_snapshot: true,
796 },
797 );
798
799 assert_eq!(df.used_import_ids(), BTreeSet::from([imported_index]));
800 }
801}