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 = ()> = DataflowDescription<LirRelationExpr, S, MfpPlan>;
680
681pub type RenderDataflowDescription<S> = DataflowDescription<RenderPlan, S, MfpPlan>;
683
684#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
687pub struct IndexDesc<E> {
688 pub on_id: GlobalId,
690 pub key: Vec<E>,
692}
693
694impl IndexDesc<MirScalarExpr> {
695 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#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
706pub struct IndexImport {
707 pub desc: IndexDesc<MirScalarExpr>,
709 pub typ: ReprRelationType,
711 pub monotonic: bool,
713 pub with_snapshot: bool,
715}
716
717#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
719pub struct SourceImport<S: 'static = (), O = MapFilterProject> {
720 pub desc: SourceInstanceDesc<S, O>,
722 pub monotonic: bool,
724 pub with_snapshot: bool,
726 pub upper: Antichain<Timestamp>,
728}
729
730#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
732pub struct BuildDesc<P> {
733 pub id: GlobalId,
735 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 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 #[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 #[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 #[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 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 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 assert!(
907 desc_with_bounds(Antichain::from_elem(Timestamp::MAX), Antichain::new())
908 .is_single_time()
909 );
910
911 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 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 #[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}