1use 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#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
35pub struct DataflowDescription<P, S: 'static = (), O = MapFilterProject> {
36 pub source_imports: BTreeMap<GlobalId, SourceImport<S, O>>,
38 pub index_imports: BTreeMap<GlobalId, IndexImport>,
41 pub objects_to_build: Vec<BuildDesc<P>>,
45 pub index_exports: BTreeMap<GlobalId, (IndexDesc<MirScalarExpr>, ReprRelationType)>,
48 pub sink_exports: BTreeMap<GlobalId, ComputeSinkDesc<S>>,
51 pub as_of: Option<Antichain<Timestamp>>,
57 pub until: Antichain<Timestamp>,
64 pub initial_storage_as_of: Option<Antichain<Timestamp>>,
67 pub refresh_schedule: Option<RefreshSchedule>,
69 pub debug_name: String,
71 pub time_dependence: Option<TimeDependence>,
73}
74
75impl<P, S, O> DataflowDescription<P, S, O> {
76 pub fn is_single_time(&self) -> bool {
81 let until = &self.until;
85
86 let Some(as_of) = self.as_of.as_ref() else {
88 return false;
89 };
90 soft_assert_or_log!(
92 timely::PartialOrder::less_equal(as_of, until),
93 "expected `as_of ≤ until`, got `{as_of:?} ≰ {until:?}`",
94 );
95 let Some(as_of) = as_of.as_option() else {
97 return false;
98 };
99 as_of.try_step_forward().as_ref() == until.as_option()
108 }
109}
110
111impl LirDataflowDescription {
112 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 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 pub fn import_source(&mut self, id: GlobalId, typ: SqlRelationType, monotonic: bool) {
157 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 pub fn insert_plan(&mut self, id: GlobalId, plan: OptimizedMirRelationExpr) {
177 self.objects_to_build.push(BuildDesc { id, plan });
178 }
179
180 pub fn export_index(
185 &mut self,
186 id: GlobalId,
187 description: IndexDesc<MirScalarExpr>,
188 on_type: ReprRelationType,
189 ) {
190 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 pub fn export_sink(&mut self, id: GlobalId, description: ComputeSinkDesc<()>) {
207 self.sink_exports.insert(id, description);
208 }
209
210 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 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 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 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 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 pub fn set_as_of(&mut self, as_of: Antichain<Timestamp>) {
368 self.as_of = Some(as_of);
369 }
370
371 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 pub fn import_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
378 self.imported_index_ids().chain(self.imported_source_ids())
379 }
380
381 pub fn imported_index_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
383 self.index_imports.keys().copied()
384 }
385
386 pub fn imported_source_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
388 self.source_imports.keys().copied()
389 }
390
391 pub fn is_import(&self, id: &GlobalId) -> bool {
393 self.index_imports.contains_key(id) || self.source_imports.contains_key(id)
394 }
395
396 pub fn export_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
398 self.exported_index_ids().chain(self.exported_sink_ids())
399 }
400
401 pub fn exported_index_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
403 self.index_exports.keys().copied()
404 }
405
406 pub fn exported_sink_ids(&self) -> impl Iterator<Item = GlobalId> + Clone + '_ {
408 self.sink_exports.keys().copied()
409 }
410
411 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 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 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 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 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 pub fn is_transient(&self) -> bool {
455 self.export_ids().all(|id| id.is_transient())
456 }
457
458 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 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 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 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 out.insert(collection_id);
511 return;
512 }
513
514 let mut found_index = false;
518 for (index_id, IndexImport { desc, .. }) in &self.index_imports {
519 if desc.on_id == collection_id {
520 out.insert(*index_id);
523 found_index = true;
524 }
525 }
526 if found_index {
527 return;
528 }
529
530 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 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 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 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 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
672pub type MirDataflowDescription = DataflowDescription<OptimizedMirRelationExpr>;
674
675pub type LirDataflowDescription<S = ()> =
680 DataflowDescription<LirRelationExpr, S, MfpPlan<LirScalarExpr>>;
681
682pub type RenderDataflowDescription<S> = DataflowDescription<RenderPlan, S, MfpPlan<LirScalarExpr>>;
684
685#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
688pub struct IndexDesc<E> {
689 pub on_id: GlobalId,
691 pub key: Vec<E>,
693}
694
695impl IndexDesc<MirScalarExpr> {
696 pub fn as_lir(&self) -> IndexDesc<LirScalarExpr> {
698 let on_id = self.on_id.clone();
699 let key = lses_from_mses(&self.key);
700
701 IndexDesc { on_id, key }
702 }
703}
704
705#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
707pub struct IndexImport {
708 pub desc: IndexDesc<MirScalarExpr>,
710 pub typ: ReprRelationType,
712 pub monotonic: bool,
714 pub with_snapshot: bool,
716}
717
718#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
720pub struct SourceImport<S: 'static = (), O = MapFilterProject> {
721 pub desc: SourceInstanceDesc<S, O>,
723 pub monotonic: bool,
725 pub with_snapshot: bool,
727 pub upper: Antichain<Timestamp>,
729}
730
731#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
733pub struct BuildDesc<P> {
734 pub id: GlobalId,
736 pub plan: P,
738}
739
740#[cfg(test)]
741mod tests {
742 use mz_expr::{AccessStrategy, Id, MirRelationExpr, UnmaterializableFunc, func};
743 use mz_repr::optimize::OptimizerFeatures;
744 use mz_repr::{RelationDesc, ReprRelationType, ReprScalarType, SqlRelationType};
745
746 use crate::plan::scalar::mfp_plan_mir_to_lir;
747 use crate::sinks::{ComputeSinkConnection, ComputeSinkDesc, SubscribeSinkConnection};
748 use crate::sources::{SourceInstanceArguments, SourceInstanceDesc};
749
750 use super::*;
751
752 const READ: GlobalId = GlobalId::User(1);
753 const UNREAD: GlobalId = GlobalId::User(2);
754 const VIEW: GlobalId = GlobalId::Transient(1);
755 const SINK: GlobalId = GlobalId::Transient(2);
756 const INDEX: GlobalId = GlobalId::Transient(3);
757
758 fn typ() -> ReprRelationType {
759 ReprRelationType::new(vec![ReprScalarType::Int64.nullable(false)])
760 }
761
762 fn dataflow(plan: MirRelationExpr) -> MirDataflowDescription {
765 let source_import = || SourceImport {
766 desc: SourceInstanceDesc {
767 arguments: SourceInstanceArguments { operators: None },
768 storage_metadata: (),
769 typ: SqlRelationType::from_repr(&typ()),
770 },
771 monotonic: false,
772 with_snapshot: true,
773 upper: Antichain::from_elem(Timestamp::MIN),
774 };
775
776 let mut df = MirDataflowDescription::new("test".to_string());
777 df.source_imports.insert(READ, source_import());
778 df.source_imports.insert(UNREAD, source_import());
779 df.objects_to_build.push(BuildDesc {
780 id: VIEW,
781 plan: OptimizedMirRelationExpr::declare_optimized(plan),
782 });
783 df.sink_exports.insert(
784 SINK,
785 ComputeSinkDesc {
786 from: VIEW,
787 from_desc: RelationDesc::new(SqlRelationType::from_repr(&typ()), ["c"]),
788 connection: ComputeSinkConnection::Subscribe(SubscribeSinkConnection {
789 output: Vec::new(),
790 }),
791 with_snapshot: true,
792 up_to: Antichain::new(),
793 non_null_assertions: Vec::new(),
794 refresh_schedule: None,
795 from_arrangement: None,
796 },
797 );
798 df
799 }
800
801 #[mz_ore::test]
802 fn used_import_ids_reports_only_read_imports() {
803 let df = dataflow(MirRelationExpr::Get {
804 id: Id::Global(READ),
805 typ: typ(),
806 access_strategy: AccessStrategy::Persist,
807 });
808
809 assert_eq!(df.used_import_ids(), BTreeSet::from([READ]));
810 }
811
812 #[mz_ore::test]
815 fn used_import_ids_is_empty_for_a_constant_export() {
816 let df = dataflow(MirRelationExpr::Constant {
817 rows: Ok(Vec::new()),
818 typ: typ(),
819 });
820
821 assert_eq!(df.used_import_ids(), BTreeSet::new());
822 }
823
824 #[mz_ore::test]
827 fn used_import_ids_covers_index_exports() {
828 let mut df = dataflow(MirRelationExpr::Constant {
829 rows: Ok(Vec::new()),
830 typ: typ(),
831 });
832 let other_view = GlobalId::Transient(4);
833 df.objects_to_build.push(BuildDesc {
834 id: other_view,
835 plan: OptimizedMirRelationExpr::declare_optimized(MirRelationExpr::Get {
836 id: Id::Global(UNREAD),
837 typ: typ(),
838 access_strategy: AccessStrategy::Persist,
839 }),
840 });
841 df.index_exports.insert(
842 INDEX,
843 (
844 IndexDesc {
845 on_id: other_view,
846 key: Vec::new(),
847 },
848 typ(),
849 ),
850 );
851
852 assert_eq!(df.used_import_ids(), BTreeSet::from([UNREAD]));
853 }
854
855 #[mz_ore::test]
858 fn used_import_ids_reports_imported_indexes_by_index_id() {
859 let indexed_view = GlobalId::User(3);
860 let imported_index = GlobalId::User(4);
861
862 let mut df = dataflow(MirRelationExpr::Get {
863 id: Id::Global(indexed_view),
864 typ: typ(),
865 access_strategy: AccessStrategy::Index(Vec::new()),
866 });
867 df.index_imports.insert(
868 imported_index,
869 IndexImport {
870 desc: IndexDesc {
871 on_id: indexed_view,
872 key: Vec::new(),
873 },
874 typ: typ(),
875 monotonic: false,
876 with_snapshot: true,
877 },
878 );
879
880 assert_eq!(df.used_import_ids(), BTreeSet::from([imported_index]));
881 }
882
883 fn desc_with_bounds(
886 as_of: Antichain<Timestamp>,
887 until: Antichain<Timestamp>,
888 ) -> DataflowDescription<OptimizedMirRelationExpr, ()> {
889 let mut desc = DataflowDescription::new("test".to_string());
890 desc.set_as_of(as_of);
891 desc.until = until;
892 desc
893 }
894
895 #[mz_ore::test]
896 fn is_single_time_classification() {
897 assert!(
899 desc_with_bounds(
900 Antichain::from_elem(5.into()),
901 Antichain::from_elem(6.into())
902 )
903 .is_single_time()
904 );
905
906 assert!(
909 desc_with_bounds(Antichain::from_elem(Timestamp::MAX), Antichain::new())
910 .is_single_time()
911 );
912
913 assert!(
917 !desc_with_bounds(
918 Antichain::from_elem(Timestamp::MAX),
919 Antichain::from_elem(Timestamp::MAX)
920 )
921 .is_single_time()
922 );
923 }
924
925 #[mz_ore::test]
926 fn lowering_plans_source_operators() {
927 let mz_now = || MirScalarExpr::CallUnmaterializable(UnmaterializableFunc::MzNow);
930 let cast = || MirScalarExpr::column(0).call_unary(func::CastInt64ToMzTimestamp);
931 let mfp = MapFilterProject::new(1).map([cast()]).filter([
932 mz_now().call_binary(MirScalarExpr::column(1), func::Lt),
933 mz_now().call_binary(cast(), func::Eq),
934 ]);
935 let planned = MfpPlan::create_from(mfp.clone()).expect("plannable");
936
937 let mut df = dataflow(MirRelationExpr::Get {
938 id: Id::Global(READ),
939 typ: typ(),
940 access_strategy: AccessStrategy::Persist,
941 });
942 let unread = df.source_imports.get_mut(&UNREAD).expect("imported");
943 unread.desc.arguments.operators = Some(mfp);
944
945 let lowered = LirRelationExpr::finalize_dataflow(df, &OptimizerFeatures::default(), None)
946 .expect("lowers");
947 let operators = &lowered.source_imports[&UNREAD].desc.arguments.operators;
948 assert_eq!(operators.as_ref(), Some(&mfp_plan_mir_to_lir(planned)));
949 }
950
951 #[mz_ore::test]
955 fn lowering_merges_pushed_down_temporal_predicate() {
956 let mz_now = || MirScalarExpr::CallUnmaterializable(UnmaterializableFunc::MzNow);
957 let cast = || MirScalarExpr::column(0).call_unary(func::CastInt64ToMzTimestamp);
958 let le = || mz_now().call_binary(cast(), func::Lte);
959
960 let mut df = dataflow(
961 MirRelationExpr::Get {
962 id: Id::Global(READ),
963 typ: typ(),
964 access_strategy: AccessStrategy::Persist,
965 }
966 .filter([le()]),
967 );
968 let read = df.source_imports.get_mut(&READ).expect("imported");
969 read.desc.arguments.operators = Some(MapFilterProject::new(1).filter([le()]));
970
971 let lowered = LirRelationExpr::finalize_dataflow(df, &OptimizerFeatures::default(), None)
972 .expect("lowers");
973 let operators = lowered.source_imports[&READ]
974 .desc
975 .arguments
976 .operators
977 .as_ref()
978 .expect("planned operators");
979 let (_safe, lower, upper) = operators.as_parts();
980 assert!(lower.is_empty(), "{operators:?}");
981 assert_eq!(upper.len(), 1, "{operators:?}");
982 }
983}