1use std::cell::RefCell;
80use std::collections::{BTreeMap, BTreeSet};
81use std::fmt;
82use std::rc::Rc;
83
84use mz_compute_types::dataflows::LirDataflowDescription;
85use mz_ore::collections::CollectionExt;
86use mz_ore::soft_panic_or_log;
87use mz_repr::{GlobalId, Timestamp};
88use mz_storage_client::storage_collections::StorageCollections;
89use mz_storage_types::read_holds::ReadHold;
90use mz_storage_types::read_policy::ReadPolicy;
91use timely::PartialOrder;
92use timely::progress::Antichain;
93use tracing::{info, warn};
94
95pub fn run(
101 dataflows: &mut [LirDataflowDescription],
102 read_policies: &BTreeMap<GlobalId, ReadPolicy>,
103 storage_collections: &dyn StorageCollections,
104 current_time: Timestamp,
105 read_only_mode: bool,
106) -> BTreeMap<GlobalId, ReadHold> {
107 let mut storage_read_holds = BTreeMap::new();
110 for dataflow in &*dataflows {
111 for id in dataflow.source_imports.keys() {
112 if !storage_read_holds.contains_key(id) {
113 let read_hold = storage_collections
114 .acquire_read_holds(vec![*id])
115 .expect("storage collection exists")
116 .into_element();
117 storage_read_holds.insert(*id, read_hold);
118 }
119 }
120 }
121
122 let mut ctx = Context::new(dataflows, storage_collections, read_policies, current_time);
123
124 ctx.prune_sealed_persist_sinks();
128
129 if read_only_mode {
134 ctx.prune_dropped_collections();
135 }
136
137 ctx.apply_upstream_storage_constraints(&storage_read_holds);
139 ctx.apply_downstream_storage_constraints();
140
141 ctx.apply_warmup_constraints();
157
158 ctx.apply_index_read_policy_constraints();
160
161 ctx.apply_index_current_time_constraints();
164
165 for dataflow in dataflows {
167 let first_export = dataflow.export_ids().next();
170 let as_of = first_export.map_or_else(Antichain::new, |id| ctx.best_as_of(id));
171 dataflow.as_of = Some(as_of);
172 }
173
174 storage_read_holds
175}
176
177#[derive(Debug)]
179struct AsOfBounds {
180 lower: Antichain<Timestamp>,
181 upper: Antichain<Timestamp>,
182 sealed: bool,
184}
185
186impl AsOfBounds {
187 fn single(frontier: Antichain<Timestamp>) -> Self {
189 Self {
190 lower: frontier.clone(),
191 upper: frontier,
192 sealed: false,
193 }
194 }
195
196 fn get(&self, type_: BoundType) -> &Antichain<Timestamp> {
198 match type_ {
199 BoundType::Lower => &self.lower,
200 BoundType::Upper => &self.upper,
201 }
202 }
203}
204
205impl Default for AsOfBounds {
206 fn default() -> Self {
207 Self {
208 lower: Antichain::from_elem(Timestamp::MIN),
209 upper: Antichain::new(),
210 sealed: false,
211 }
212 }
213}
214
215impl fmt::Display for AsOfBounds {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 write!(
218 f,
219 "[{:?} .. {:?}]",
220 self.lower.elements(),
221 self.upper.elements()
222 )
223 }
224}
225
226#[derive(Clone, Copy, Debug, PartialEq, Eq)]
228enum BoundType {
229 Lower,
230 Upper,
231}
232
233impl fmt::Display for BoundType {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 match self {
236 Self::Lower => f.write_str("lower"),
237 Self::Upper => f.write_str("upper"),
238 }
239 }
240}
241
242#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244enum ConstraintType {
245 Hard,
248 Soft,
251}
252
253#[derive(Debug)]
255struct Constraint<'a> {
256 type_: ConstraintType,
257 bound_type: BoundType,
259 frontier: &'a Antichain<Timestamp>,
261 reason: &'a str,
265}
266
267impl Constraint<'_> {
268 fn apply(&self, bounds: &mut AsOfBounds) -> Result<bool, bool> {
278 if bounds.sealed {
279 return Ok(false);
280 }
281
282 match self.bound_type {
283 BoundType::Lower => {
284 if PartialOrder::less_than(&bounds.upper, self.frontier) {
285 bounds.sealed = true;
286 if PartialOrder::less_than(&bounds.lower, &bounds.upper) {
287 bounds.lower.clone_from(&bounds.upper);
288 Err(true)
289 } else {
290 Err(false)
291 }
292 } else if PartialOrder::less_equal(self.frontier, &bounds.lower) {
293 Ok(false)
294 } else {
295 bounds.lower.clone_from(self.frontier);
296 Ok(true)
297 }
298 }
299 BoundType::Upper => {
300 if PartialOrder::less_than(self.frontier, &bounds.lower) {
301 bounds.sealed = true;
302 if PartialOrder::less_than(&bounds.lower, &bounds.upper) {
303 bounds.upper.clone_from(&bounds.lower);
304 Err(true)
305 } else {
306 Err(false)
307 }
308 } else if PartialOrder::less_equal(&bounds.upper, self.frontier) {
309 Ok(false)
310 } else {
311 bounds.upper.clone_from(self.frontier);
312 Ok(true)
313 }
314 }
315 }
316 }
317}
318
319struct Collection<'a> {
321 storage_inputs: Vec<GlobalId>,
322 compute_inputs: Vec<GlobalId>,
323 read_policy: Option<&'a ReadPolicy>,
324 bounds: Rc<RefCell<AsOfBounds>>,
328 is_index: bool,
330}
331
332struct Context<'a> {
334 collections: BTreeMap<GlobalId, Collection<'a>>,
335 storage_collections: &'a dyn StorageCollections,
336 current_time: Timestamp,
337}
338
339impl<'a> Context<'a> {
340 fn new(
342 dataflows: &[LirDataflowDescription],
343 storage_collections: &'a dyn StorageCollections,
344 read_policies: &'a BTreeMap<GlobalId, ReadPolicy>,
345 current_time: Timestamp,
346 ) -> Self {
347 let mut collections = BTreeMap::new();
350 for dataflow in dataflows {
351 let storage_inputs: Vec<_> = dataflow.source_imports.keys().copied().collect();
352 let compute_inputs: Vec<_> = dataflow.index_imports.keys().copied().collect();
353
354 let bounds = match dataflow.as_of.clone() {
355 Some(frontier) => AsOfBounds::single(frontier),
356 None => AsOfBounds::default(),
357 };
358 let bounds = Rc::new(RefCell::new(bounds));
359
360 for id in dataflow.export_ids() {
361 let collection = Collection {
362 storage_inputs: storage_inputs.clone(),
363 compute_inputs: compute_inputs.clone(),
364 read_policy: read_policies.get(&id),
365 bounds: Rc::clone(&bounds),
366 is_index: dataflow.index_exports.contains_key(&id),
367 };
368 collections.insert(id, collection);
369 }
370 }
371
372 Self {
373 collections,
374 storage_collections,
375 current_time,
376 }
377 }
378
379 fn expect_collection(&self, id: GlobalId) -> &Collection<'_> {
385 self.collections
386 .get(&id)
387 .unwrap_or_else(|| panic!("collection missing: {id}"))
388 }
389
390 fn apply_constraint(&self, id: GlobalId, constraint: Constraint) -> bool {
394 let collection = self.expect_collection(id);
395 let mut bounds = collection.bounds.borrow_mut();
396 match constraint.apply(&mut bounds) {
397 Ok(changed) => {
398 if changed {
399 info!(%id, %bounds, reason = %constraint.reason, "applied as-of constraint");
400 }
401 changed
402 }
403 Err(changed) => {
404 match constraint.type_ {
405 ConstraintType::Hard => {
406 soft_panic_or_log!(
407 "failed to apply hard as-of constraint \
408 (id={id}, bounds={bounds}, constraint={constraint:?})"
409 );
410 }
411 ConstraintType::Soft => {
412 info!(%id, %bounds, ?constraint, "failed to apply soft as-of constraint");
413 }
414 }
415 changed
416 }
417 }
418 }
419
420 fn apply_upstream_storage_constraints(
428 &self,
429 storage_read_holds: &BTreeMap<GlobalId, ReadHold>,
430 ) {
431 for (id, collection) in &self.collections {
433 for input_id in &collection.storage_inputs {
434 let read_hold = &storage_read_holds[input_id];
435 let constraint = Constraint {
436 type_: ConstraintType::Hard,
437 bound_type: BoundType::Lower,
438 frontier: read_hold.since(),
439 reason: &format!("storage input {input_id} read frontier"),
440 };
441 self.apply_constraint(*id, constraint);
442 }
443 }
444
445 self.propagate_bounds_downstream(BoundType::Lower);
447 }
448
449 fn apply_downstream_storage_constraints(&self) {
475 for id in self.collections.keys() {
477 let Ok(frontiers) = self.storage_collections.collection_frontiers(*id) else {
478 continue;
479 };
480
481 let collection_empty =
482 PartialOrder::less_equal(&frontiers.write_frontier, &frontiers.read_capabilities);
483 let upper = if collection_empty {
484 frontiers.read_capabilities
485 } else {
486 step_back_frontier(&frontiers.write_frontier)
487 };
488
489 let constraint = Constraint {
490 type_: ConstraintType::Hard,
491 bound_type: BoundType::Upper,
492 frontier: &upper,
493 reason: &format!("storage export {id} write frontier"),
494 };
495 self.apply_constraint(*id, constraint);
496 }
497
498 self.propagate_bounds_upstream(BoundType::Upper);
500 }
501
502 fn apply_warmup_constraints(&self) {
511 let mut write_frontiers = BTreeMap::new();
513 for (id, collection) in &self.collections {
514 let storage_frontiers = self
515 .storage_collections
516 .collections_frontiers(collection.storage_inputs.clone())
517 .expect("storage collections exist");
518
519 let mut write_frontier = Antichain::new();
520 for frontiers in storage_frontiers {
521 write_frontier.extend(frontiers.write_frontier);
522 }
523
524 write_frontiers.insert(*id, write_frontier);
525 }
526
527 fixpoint(|changed| {
529 for (id, collection) in &self.collections {
530 let mut write_frontier = write_frontiers.remove(id).expect("inserted above");
531 for input_id in &collection.compute_inputs {
532 let frontier = &write_frontiers[input_id];
533 *changed |= write_frontier.extend(frontier.iter().cloned());
534 }
535 write_frontiers.insert(*id, write_frontier);
536 }
537 });
538
539 for (id, write_frontier) in write_frontiers {
541 let upper = step_back_frontier(&write_frontier);
542 let constraint = Constraint {
543 type_: ConstraintType::Soft,
544 bound_type: BoundType::Upper,
545 frontier: &upper,
546 reason: &format!(
547 "warmup frontier derived from storage write frontier {:?}",
548 write_frontier.elements()
549 ),
550 };
551 self.apply_constraint(id, constraint);
552 }
553
554 self.propagate_bounds_upstream(BoundType::Upper);
556 }
557
558 fn apply_index_read_policy_constraints(&self) {
568 let mut write_frontiers = BTreeMap::new();
575 for (id, collection) in &self.collections {
576 let storage_frontiers = self
577 .storage_collections
578 .collections_frontiers(collection.storage_inputs.clone())
579 .expect("storage collections exist");
580
581 let mut write_frontier = Antichain::new();
582 for frontiers in storage_frontiers {
583 write_frontier.extend(frontiers.write_frontier);
584 }
585
586 write_frontiers.insert(*id, write_frontier);
587 }
588
589 fixpoint(|changed| {
591 for (id, collection) in &self.collections {
592 let mut write_frontier = write_frontiers.remove(id).expect("inserted above");
593 for input_id in &collection.compute_inputs {
594 let frontier = &write_frontiers[input_id];
595 *changed |= write_frontier.extend(frontier.iter().cloned());
596 }
597 write_frontiers.insert(*id, write_frontier);
598 }
599 });
600
601 for (id, collection) in &self.collections {
603 if let (true, Some(read_policy)) = (collection.is_index, &collection.read_policy) {
604 let mut write_frontier = write_frontiers.remove(id).expect("inserted above");
605 if write_frontier.is_empty() {
606 write_frontier = Antichain::from_elem(self.current_time.clone());
607 }
608 let upper = read_policy.frontier(write_frontier.borrow());
609 let constraint = Constraint {
610 type_: ConstraintType::Soft,
611 bound_type: BoundType::Upper,
612 frontier: &upper,
613 reason: &format!(
614 "read policy applied to write frontier {:?}",
615 write_frontier.elements()
616 ),
617 };
618 self.apply_constraint(*id, constraint);
619 }
620 }
621
622 self.propagate_bounds_upstream(BoundType::Upper);
624 }
625
626 fn apply_index_current_time_constraints(&self) {
633 let upper = Antichain::from_elem(self.current_time.clone());
635 for (id, collection) in &self.collections {
636 if collection.is_index {
637 let constraint = Constraint {
638 type_: ConstraintType::Soft,
639 bound_type: BoundType::Upper,
640 frontier: &upper,
641 reason: "index current time",
642 };
643 self.apply_constraint(*id, constraint);
644 }
645 }
646
647 self.propagate_bounds_upstream(BoundType::Upper);
649 }
650
651 fn propagate_bounds_downstream(&self, bound_type: BoundType) {
653 let constraint_type = match bound_type {
656 BoundType::Lower => ConstraintType::Hard,
657 BoundType::Upper => ConstraintType::Soft,
658 };
659
660 fixpoint(|changed| {
663 self.propagate_bounds_downstream_inner(bound_type, constraint_type, changed);
664
665 if bound_type == BoundType::Upper {
668 self.propagate_bounds_upstream_inner(
669 BoundType::Upper,
670 ConstraintType::Hard,
671 changed,
672 );
673 }
674 });
675 }
676
677 fn propagate_bounds_downstream_inner(
678 &self,
679 bound_type: BoundType,
680 constraint_type: ConstraintType,
681 changed: &mut bool,
682 ) {
683 for (id, collection) in &self.collections {
684 for input_id in &collection.compute_inputs {
685 let input_collection = self.expect_collection(*input_id);
686 let bounds = input_collection.bounds.borrow();
687 let constraint = Constraint {
688 type_: constraint_type,
689 bound_type,
690 frontier: bounds.get(bound_type),
691 reason: &format!("upstream {input_id} {bound_type} as-of bound"),
692 };
693 *changed |= self.apply_constraint(*id, constraint);
694 }
695 }
696 }
697
698 fn propagate_bounds_upstream(&self, bound_type: BoundType) {
700 let constraint_type = match bound_type {
703 BoundType::Lower => ConstraintType::Soft,
704 BoundType::Upper => ConstraintType::Hard,
705 };
706
707 fixpoint(|changed| {
710 self.propagate_bounds_upstream_inner(bound_type, constraint_type, changed);
711
712 if bound_type == BoundType::Lower {
715 self.propagate_bounds_downstream_inner(
716 BoundType::Lower,
717 ConstraintType::Hard,
718 changed,
719 );
720 }
721 });
722 }
723
724 fn propagate_bounds_upstream_inner(
725 &self,
726 bound_type: BoundType,
727 constraint_type: ConstraintType,
728 changed: &mut bool,
729 ) {
730 for (id, collection) in self.collections.iter().rev() {
731 let bounds = collection.bounds.borrow();
732 for input_id in &collection.compute_inputs {
733 let constraint = Constraint {
734 type_: constraint_type,
735 bound_type,
736 frontier: bounds.get(bound_type),
737 reason: &format!("downstream {id} {bound_type} as-of bound"),
738 };
739 *changed |= self.apply_constraint(*input_id, constraint);
740 }
741 }
742 }
743
744 fn best_as_of(&self, id: GlobalId) -> Antichain<Timestamp> {
751 if let Some(collection) = self.collections.get(&id) {
752 let bounds = collection.bounds.borrow();
753 bounds.upper.clone()
754 } else {
755 Antichain::new()
756 }
757 }
758
759 fn prune_sealed_persist_sinks(&mut self) {
767 self.collections.retain(|id, _| {
768 self.storage_collections
769 .collection_frontiers(*id)
770 .map_or(true, |f| !f.write_frontier.is_empty())
771 });
772 }
773
774 fn prune_dropped_collections(&mut self) {
781 let mut pruned = BTreeSet::new();
783 self.collections.retain(|id, c| {
784 let input_dropped = c.storage_inputs.iter().any(|id| {
785 let frontiers = self
786 .storage_collections
787 .collection_frontiers(*id)
788 .expect("storage collection exists");
789 frontiers.read_capabilities.is_empty()
790 });
791
792 if input_dropped {
793 pruned.insert(*id);
794 false
795 } else {
796 true
797 }
798 });
799
800 warn!(?pruned, "pruned dependants of dropped storage collections");
801
802 while !pruned.is_empty() {
804 let pruned_inputs = std::mem::take(&mut pruned);
805
806 self.collections.retain(|id, c| {
807 if c.compute_inputs.iter().any(|id| pruned_inputs.contains(id)) {
808 pruned.insert(*id);
809 false
810 } else {
811 true
812 }
813 });
814
815 warn!(?pruned, "pruned collections with pruned inputs");
816 }
817 }
818}
819
820fn fixpoint(mut step: impl FnMut(&mut bool)) {
822 loop {
823 let mut changed = false;
824 step(&mut changed);
825 if !changed {
826 break;
827 }
828 }
829}
830
831fn step_back_frontier(frontier: &Antichain<Timestamp>) -> Antichain<Timestamp> {
836 frontier
837 .iter()
838 .map(|t| t.step_back().unwrap_or(Timestamp::MIN))
839 .collect()
840}
841
842#[cfg(test)]
843mod tests {
844 use std::collections::BTreeSet;
845
846 use async_trait::async_trait;
847 use futures::future::BoxFuture;
848 use futures::stream::BoxStream;
849 use mz_compute_types::dataflows::{DataflowDescription, IndexDesc, IndexImport};
850 use mz_compute_types::sinks::ComputeSinkConnection;
851 use mz_compute_types::sinks::ComputeSinkDesc;
852 use mz_compute_types::sinks::MaterializedViewSinkConnection;
853 use mz_compute_types::sources::SourceInstanceArguments;
854 use mz_compute_types::sources::SourceInstanceDesc;
855 use mz_persist_client::stats::{SnapshotPartsStats, SnapshotStats};
856 use mz_persist_types::ShardId;
857 use mz_repr::{RelationDesc, RelationVersion, Row, SqlRelationType};
858 use mz_repr::{ReprRelationType, Timestamp};
859 use mz_storage_client::client::TimestamplessUpdateBuilder;
860 use mz_storage_client::controller::{CollectionDescription, StorageMetadata, StorageTxn};
861 use mz_storage_client::storage_collections::{CollectionFrontiers, SnapshotCursor};
862 use mz_storage_types::StorageDiff;
863 use mz_storage_types::controller::{CollectionMetadata, StorageError};
864 use mz_storage_types::errors::CollectionMissing;
865 use mz_storage_types::parameters::StorageParameters;
866 use mz_storage_types::sources::SourceData;
867 use mz_storage_types::time_dependence::{TimeDependence, TimeDependenceError};
868
869 use super::*;
870
871 const SEALED: u64 = 0x5ea1ed;
872
873 fn ts_to_frontier(ts: u64) -> Antichain<Timestamp> {
874 if ts == SEALED {
875 Antichain::new()
876 } else {
877 Antichain::from_elem(ts.into())
878 }
879 }
880
881 #[derive(Debug)]
882 struct StorageFrontiers(BTreeMap<GlobalId, (Antichain<Timestamp>, Antichain<Timestamp>)>);
883
884 #[async_trait]
885 impl StorageCollections for StorageFrontiers {
886 async fn initialize_state(
887 &self,
888 _txn: &mut (dyn StorageTxn + Send),
889 _init_ids: BTreeSet<GlobalId>,
890 ) -> Result<(), StorageError> {
891 unimplemented!()
892 }
893
894 fn update_parameters(&self, _config_params: StorageParameters) {
895 unimplemented!()
896 }
897
898 fn collection_metadata(
899 &self,
900 _id: GlobalId,
901 ) -> Result<CollectionMetadata, CollectionMissing> {
902 unimplemented!()
903 }
904
905 fn active_collection_metadatas(&self) -> Vec<(GlobalId, CollectionMetadata)> {
906 unimplemented!()
907 }
908
909 fn collections_frontiers(
910 &self,
911 ids: Vec<GlobalId>,
912 ) -> Result<Vec<CollectionFrontiers>, CollectionMissing> {
913 let mut frontiers = Vec::with_capacity(ids.len());
914 for id in ids {
915 let (read, write) = self.0.get(&id).ok_or(CollectionMissing(id))?;
916 frontiers.push(CollectionFrontiers {
917 id,
918 write_frontier: write.clone(),
919 implied_capability: read.clone(),
920 read_capabilities: read.clone(),
921 })
922 }
923 Ok(frontiers)
924 }
925
926 fn active_collection_frontiers(&self) -> Vec<CollectionFrontiers> {
927 unimplemented!()
928 }
929
930 fn check_exists(&self, _id: GlobalId) -> Result<(), StorageError> {
931 unimplemented!()
932 }
933
934 async fn snapshot_stats(
935 &self,
936 _id: GlobalId,
937 _as_of: Antichain<Timestamp>,
938 ) -> Result<SnapshotStats, StorageError> {
939 unimplemented!()
940 }
941
942 async fn snapshot_parts_stats(
943 &self,
944 _id: GlobalId,
945 _as_of: Antichain<Timestamp>,
946 ) -> BoxFuture<'static, Result<SnapshotPartsStats, StorageError>> {
947 unimplemented!()
948 }
949
950 fn snapshot(
951 &self,
952 _id: GlobalId,
953 _as_of: Timestamp,
954 ) -> BoxFuture<'static, Result<Vec<(Row, StorageDiff)>, StorageError>> {
955 unimplemented!()
956 }
957
958 async fn snapshot_latest(&self, _id: GlobalId) -> Result<Vec<Row>, StorageError> {
959 unimplemented!()
960 }
961
962 fn snapshot_cursor(
963 &self,
964 _id: GlobalId,
965 _as_of: Timestamp,
966 ) -> BoxFuture<'static, Result<SnapshotCursor, StorageError>> {
967 unimplemented!()
968 }
969
970 fn snapshot_and_stream(
971 &self,
972 _id: GlobalId,
973 _as_of: Timestamp,
974 ) -> BoxFuture<
975 'static,
976 Result<BoxStream<'static, (SourceData, Timestamp, StorageDiff)>, StorageError>,
977 > {
978 unimplemented!()
979 }
980
981 fn create_update_builder(
982 &self,
983 _id: GlobalId,
984 ) -> BoxFuture<
985 'static,
986 Result<TimestamplessUpdateBuilder<SourceData, (), StorageDiff>, StorageError>,
987 > {
988 unimplemented!()
989 }
990
991 async fn prepare_state(
992 &self,
993 _txn: &mut (dyn StorageTxn + Send),
994 _ids_to_add: BTreeSet<GlobalId>,
995 _ids_to_drop: BTreeSet<GlobalId>,
996 _ids_to_register: BTreeMap<GlobalId, ShardId>,
997 ) -> Result<(), StorageError> {
998 unimplemented!()
999 }
1000
1001 async fn create_collections_for_bootstrap(
1002 &self,
1003 _storage_metadata: &StorageMetadata,
1004 _register_ts: Option<Timestamp>,
1005 _collections: Vec<(GlobalId, CollectionDescription)>,
1006 _migrated_storage_collections: &BTreeSet<GlobalId>,
1007 ) -> Result<(), StorageError> {
1008 unimplemented!()
1009 }
1010
1011 async fn alter_table_desc(
1012 &self,
1013 _existing_collection: GlobalId,
1014 _new_collection: GlobalId,
1015 _new_desc: RelationDesc,
1016 _expected_version: RelationVersion,
1017 ) -> Result<(), StorageError> {
1018 unimplemented!()
1019 }
1020
1021 fn drop_collections_unvalidated(
1022 &self,
1023 _storage_metadata: &StorageMetadata,
1024 _identifiers: Vec<GlobalId>,
1025 ) {
1026 unimplemented!()
1027 }
1028
1029 fn set_read_policies(&self, _policies: Vec<(GlobalId, ReadPolicy)>) {
1030 unimplemented!()
1031 }
1032
1033 fn acquire_read_holds(
1034 &self,
1035 desired_holds: Vec<GlobalId>,
1036 ) -> Result<Vec<ReadHold>, CollectionMissing> {
1037 let mut holds = Vec::with_capacity(desired_holds.len());
1038 for id in desired_holds {
1039 let (read, _write) = self.0.get(&id).ok_or(CollectionMissing(id))?;
1040 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
1041 holds.push(ReadHold::with_channel(id, read.clone(), tx));
1042 }
1043 Ok(holds)
1044 }
1045
1046 fn determine_time_dependence(
1047 &self,
1048 _id: GlobalId,
1049 ) -> Result<Option<TimeDependence>, TimeDependenceError> {
1050 unimplemented!()
1051 }
1052
1053 fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
1054 unimplemented!()
1055 }
1056 }
1057
1058 fn dataflow(
1059 export_id: &str,
1060 input_ids: &[&str],
1061 storage_ids: &BTreeSet<&str>,
1062 ) -> LirDataflowDescription {
1063 let source_imports = input_ids
1064 .iter()
1065 .filter(|s| storage_ids.contains(*s))
1066 .map(|s| {
1067 let id = s.parse().unwrap();
1068 let desc = SourceInstanceDesc {
1069 arguments: SourceInstanceArguments {
1070 operators: Default::default(),
1071 },
1072 storage_metadata: Default::default(),
1073 typ: SqlRelationType::empty(),
1074 };
1075 (
1076 id,
1077 mz_compute_types::dataflows::SourceImport {
1078 desc,
1079 monotonic: Default::default(),
1080 with_snapshot: true,
1081 upper: Default::default(),
1082 },
1083 )
1084 })
1085 .collect();
1086 let index_imports = input_ids
1087 .iter()
1088 .filter(|s| !storage_ids.contains(*s))
1089 .map(|s| {
1090 let id = s.parse().unwrap();
1091 let import = IndexImport {
1092 desc: IndexDesc {
1093 on_id: GlobalId::Transient(0),
1094 key: Default::default(),
1095 },
1096 typ: ReprRelationType::empty(),
1097 monotonic: Default::default(),
1098 with_snapshot: true,
1099 };
1100 (id, import)
1101 })
1102 .collect();
1103 let index_exports = std::iter::once(export_id)
1104 .filter(|s| !storage_ids.contains(*s))
1105 .map(|sid| {
1106 let id = sid.parse().unwrap();
1107 let desc = IndexDesc {
1108 on_id: GlobalId::Transient(0),
1109 key: Default::default(),
1110 };
1111 let typ = ReprRelationType::empty();
1112 (id, (desc, typ))
1113 })
1114 .collect();
1115 let sink_exports = std::iter::once(export_id)
1116 .filter(|s| storage_ids.contains(*s))
1117 .map(|sid| {
1118 let id = sid.parse().unwrap();
1119 let desc = ComputeSinkDesc {
1120 from: GlobalId::Transient(0),
1121 from_desc: RelationDesc::empty(),
1122 connection: ComputeSinkConnection::MaterializedView(
1123 MaterializedViewSinkConnection {
1124 value_desc: RelationDesc::empty(),
1125 storage_metadata: Default::default(),
1126 },
1127 ),
1128 with_snapshot: Default::default(),
1129 up_to: Default::default(),
1130 non_null_assertions: Default::default(),
1131 refresh_schedule: Default::default(),
1132 from_arrangement: None,
1133 };
1134 (id, desc)
1135 })
1136 .collect();
1137
1138 DataflowDescription {
1139 source_imports,
1140 index_imports,
1141 objects_to_build: Default::default(),
1142 index_exports,
1143 sink_exports,
1144 as_of: None,
1145 until: Default::default(),
1146 initial_storage_as_of: Default::default(),
1147 refresh_schedule: Default::default(),
1148 debug_name: Default::default(),
1149 time_dependence: None,
1150 }
1151 }
1152
1153 macro_rules! testcase {
1154 ($name:ident, {
1155 storage: { $( $storage_id:literal: ($read:expr, $write:expr), )* },
1156 dataflows: [ $( $export_id:literal <- $inputs:expr => $as_of:expr, )* ],
1157 current_time: $current_time:literal,
1158 $( read_policies: { $( $policy_id:literal: $policy:expr, )* }, )?
1159 $( read_only: $read_only:expr, )?
1160 }) => {
1161 #[mz_ore::test]
1162 fn $name() {
1163 let storage_ids = [$( $storage_id, )*].into();
1164
1165 let storage_frontiers = StorageFrontiers(BTreeMap::from([
1166 $(
1167 (
1168 $storage_id.parse().unwrap(),
1169 (ts_to_frontier($read), ts_to_frontier($write)),
1170 ),
1171 )*
1172 ]));
1173
1174 let mut dataflows = [
1175 $(
1176 dataflow($export_id, &$inputs, &storage_ids),
1177 )*
1178 ];
1179
1180 let read_policies = BTreeMap::from([
1181 $($( ($policy_id.parse().unwrap(), $policy), )*)?
1182 ]);
1183
1184 #[allow(unused_variables)]
1185 let read_only = false;
1186 $( let read_only = $read_only; )?
1187
1188 super::run(
1189 &mut dataflows,
1190 &read_policies,
1191 &storage_frontiers,
1192 $current_time.into(),
1193 read_only,
1194 );
1195
1196 let actual_as_ofs: Vec<_> = dataflows
1197 .into_iter()
1198 .map(|d| d.as_of.unwrap())
1199 .collect();
1200 let expected_as_ofs = [ $( ts_to_frontier($as_of), )* ];
1201
1202 assert_eq!(actual_as_ofs, expected_as_ofs);
1203 }
1204 };
1205 }
1206
1207 testcase!(upstream_storage_constraints, {
1208 storage: {
1209 "s1": (10, 20),
1210 "s2": (20, 30),
1211 },
1212 dataflows: [
1213 "u1" <- ["s1"] => 10,
1214 "u2" <- ["s2"] => 20,
1215 "u3" <- ["s1", "s2"] => 20,
1216 "u4" <- ["u1", "u2"] => 20,
1217 ],
1218 current_time: 0,
1219 });
1220
1221 testcase!(downstream_storage_constraints, {
1222 storage: {
1223 "s1": (10, 20),
1224 "u3": (10, 15),
1225 "u4": (10, 13),
1226 },
1227 dataflows: [
1228 "u1" <- ["s1"] => 19,
1229 "u2" <- ["s1"] => 12,
1230 "u3" <- ["u2"] => 14,
1231 "u4" <- ["u2"] => 12,
1232 ],
1233 current_time: 100,
1234 });
1235
1236 testcase!(warmup_constraints, {
1237 storage: {
1238 "s1": (10, 20),
1239 "s2": (10, 30),
1240 "s3": (10, 40),
1241 "s4": (10, 50),
1242 },
1243 dataflows: [
1244 "u1" <- ["s1"] => 19,
1245 "u2" <- ["s2"] => 19,
1246 "u3" <- ["s3"] => 39,
1247 "u4" <- ["s4"] => 39,
1248 "u5" <- ["u1", "u2"] => 19,
1249 "u6" <- ["u3", "u4"] => 39,
1250 ],
1251 current_time: 100,
1252 });
1253
1254 testcase!(index_read_policy_constraints, {
1255 storage: {
1256 "s1": (10, 20),
1257 "u6": (10, 18),
1258 },
1259 dataflows: [
1260 "u1" <- ["s1"] => 15,
1261 "u2" <- ["s1"] => 10,
1262 "u3" <- ["s1"] => 13,
1263 "u4" <- ["s1"] => 10,
1264 "u5" <- [] => 95,
1265 "u6" <- ["s1"] => 17,
1266 ],
1267 current_time: 100,
1268 read_policies: {
1269 "u1": ReadPolicy::lag_writes_by(5.into(), 1.into()),
1270 "u2": ReadPolicy::lag_writes_by(15.into(), 1.into()),
1271 "u3": ReadPolicy::ValidFrom(Antichain::from_elem(13.into())),
1272 "u4": ReadPolicy::ValidFrom(Antichain::from_elem(5.into())),
1273 "u5": ReadPolicy::lag_writes_by(5.into(), 1.into()),
1274 "u6": ReadPolicy::ValidFrom(Antichain::from_elem(13.into())),
1275 },
1276 });
1277
1278 testcase!(index_current_time_constraints, {
1279 storage: {
1280 "s1": (10, 20),
1281 "s2": (20, 30),
1282 "u4": (10, 12),
1283 "u5": (10, 18),
1284 },
1285 dataflows: [
1286 "u1" <- ["s1"] => 15,
1287 "u2" <- ["s2"] => 20,
1288 "u3" <- ["s1"] => 11,
1289 "u4" <- ["u3"] => 11,
1290 "u5" <- ["s1"] => 17,
1291 "u6" <- [] => 15,
1292 ],
1293 current_time: 15,
1294 });
1295
1296 testcase!(sealed_storage_sink, {
1297 storage: {
1298 "s1": (10, 20),
1299 "u1": (10, SEALED),
1300 },
1301 dataflows: [
1302 "u1" <- ["s1"] => SEALED,
1303 ],
1304 current_time: 100,
1305 });
1306
1307 testcase!(read_only_dropped_storage_inputs, {
1308 storage: {
1309 "s1": (10, 20),
1310 "s2": (SEALED, SEALED),
1311 "u4": (10, 20),
1312 },
1313 dataflows: [
1314 "u1" <- ["s1"] => 15,
1315 "u2" <- ["s2"] => SEALED,
1316 "u3" <- ["s1", "s2"] => SEALED,
1317 "u4" <- ["u2"] => SEALED,
1318 ],
1319 current_time: 15,
1320 read_only: true,
1321 });
1322
1323 testcase!(github_9273, {
1325 storage: {
1326 "s1": (10, 20),
1327 "u3": (14, 15),
1328 },
1329 dataflows: [
1330 "u1" <- ["s1"] => 14,
1331 "u2" <- ["u1"] => 19,
1332 "u3" <- ["u1"] => 14,
1333 ],
1334 current_time: 100,
1335 read_policies: {
1336 "u1": ReadPolicy::lag_writes_by(1.into(), 1.into()),
1337 "u2": ReadPolicy::lag_writes_by(1.into(), 1.into()),
1338 },
1339 });
1340}