1use anyhow::anyhow;
11use differential_dataflow::lattice::Lattice;
12use maplit::btreemap;
13use maplit::btreeset;
14use mz_adapter_types::compaction::CompactionWindow;
15use mz_catalog::memory::objects::{CatalogItem, MaterializedView};
16use mz_expr::{CollectionPlan, ResultSpec};
17use mz_ore::collections::CollectionExt;
18use mz_ore::instrument;
19use mz_ore::soft_panic_or_log;
20use mz_repr::explain::{ExprHumanizerExt, TransientItem};
21use mz_repr::optimize::OptimizerFeatures;
22use mz_repr::optimize::OverrideFrom;
23use mz_repr::refresh_schedule::RefreshSchedule;
24use mz_repr::{CatalogItemId, Datum, RelationVersion, Row, VersionedRelationDesc};
25use mz_sql::ast::ExplainStage;
26use mz_sql::catalog::CatalogError;
27use mz_sql::names::ResolvedIds;
28use mz_sql::plan;
29use mz_sql::session::metadata::SessionMetadata;
30use mz_sql_parser::ast;
31use mz_sql_parser::ast::display::AstDisplay;
32use mz_storage_client::controller::CollectionDescription;
33use std::collections::BTreeMap;
34use timely::progress::Antichain;
35use tracing::Span;
36
37use crate::ReadHolds;
38use crate::catalog::CatalogState;
39use crate::command::ExecuteResponse;
40use crate::coord::sequencer::inner::return_if_err;
41use crate::coord::{
42 Coordinator, CreateMaterializedViewExplain, CreateMaterializedViewFinish,
43 CreateMaterializedViewOptimize, CreateMaterializedViewStage, ExplainContext,
44 ExplainPlanContext, Message, PlanValidity, StageResult, Staged,
45};
46use crate::error::AdapterError;
47use crate::explain::explain_dataflow;
48use crate::explain::explain_plan;
49use crate::explain::optimizer_trace::OptimizerTrace;
50use crate::optimize::dataflows::dataflow_import_id_bundle;
51use crate::optimize::{self, Optimize};
52use crate::session::Session;
53use crate::util::ResultExt;
54use crate::{AdapterNotice, CollectionIdBundle, ExecuteContext, TimestampProvider, catalog};
55
56impl Staged for CreateMaterializedViewStage {
57 type Ctx = ExecuteContext;
58
59 fn validity(&mut self) -> &mut PlanValidity {
60 match self {
61 Self::Optimize(stage) => &mut stage.validity,
62 Self::Finish(stage) => &mut stage.validity,
63 Self::Explain(stage) => &mut stage.validity,
64 }
65 }
66
67 async fn stage(
68 self,
69 coord: &mut Coordinator,
70 ctx: &mut ExecuteContext,
71 ) -> Result<StageResult<Box<Self>>, AdapterError> {
72 match self {
73 CreateMaterializedViewStage::Optimize(stage) => {
74 coord
75 .create_materialized_view_optimize(ctx.session().conn_id().clone(), stage)
76 .await
77 }
78 CreateMaterializedViewStage::Finish(stage) => {
79 coord.create_materialized_view_finish(ctx, stage).await
80 }
81 CreateMaterializedViewStage::Explain(stage) => {
82 coord
83 .create_materialized_view_explain(ctx.session(), stage)
84 .await
85 }
86 }
87 }
88
89 fn message(self, ctx: ExecuteContext, span: Span) -> Message {
90 Message::CreateMaterializedViewStageReady {
91 ctx,
92 span,
93 stage: self,
94 }
95 }
96
97 fn cancel_enabled(&self) -> bool {
98 true
99 }
100}
101
102impl Coordinator {
103 #[instrument]
104 pub(crate) async fn sequence_create_materialized_view(
105 &mut self,
106 ctx: ExecuteContext,
107 plan: plan::CreateMaterializedViewPlan,
108 resolved_ids: ResolvedIds,
109 ) {
110 let stage = return_if_err!(
111 self.create_materialized_view_validate(
112 ctx.session(),
113 plan,
114 resolved_ids,
115 ExplainContext::None
116 ),
117 ctx
118 );
119 self.sequence_staged(ctx, Span::current(), stage).await;
120 }
121
122 #[instrument]
123 pub(crate) async fn explain_create_materialized_view(
124 &mut self,
125 ctx: ExecuteContext,
126 plan::ExplainPlanPlan {
127 stage,
128 format,
129 config,
130 explainee,
131 }: plan::ExplainPlanPlan,
132 ) {
133 let plan::Explainee::Statement(stmt) = explainee else {
134 unreachable!()
137 };
138 let plan::ExplaineeStatement::CreateMaterializedView { broken, plan } = stmt else {
139 unreachable!()
142 };
143
144 let optimizer_trace = OptimizerTrace::new(stage.paths());
147
148 let resolved_ids = ResolvedIds::empty();
150
151 let explain_ctx = ExplainContext::Plan(ExplainPlanContext {
152 broken,
153 config,
154 format,
155 stage,
156 replan: None,
157 desc: None,
158 optimizer_trace,
159 });
160 let stage = return_if_err!(
161 self.create_materialized_view_validate(ctx.session(), plan, resolved_ids, explain_ctx),
162 ctx
163 );
164 self.sequence_staged(ctx, Span::current(), stage).await;
165 }
166
167 #[instrument]
168 pub(crate) async fn explain_replan_materialized_view(
169 &mut self,
170 ctx: ExecuteContext,
171 plan::ExplainPlanPlan {
172 stage,
173 format,
174 config,
175 explainee,
176 }: plan::ExplainPlanPlan,
177 ) {
178 let plan::Explainee::ReplanMaterializedView(id) = explainee else {
179 unreachable!() };
181 let CatalogItem::MaterializedView(item) = self.catalog().get_entry(&id).item() else {
182 unreachable!() };
184 let gid = item.global_id_writes();
185
186 let create_sql = item.create_sql.clone();
187 let plan_result = self
188 .catalog_mut()
189 .deserialize_plan_with_enable_for_item_parsing(&create_sql, true);
190 let (plan, resolved_ids) = return_if_err!(plan_result, ctx);
191
192 let plan::Plan::CreateMaterializedView(plan) = plan else {
193 unreachable!() };
195
196 let broken = false;
199
200 let optimizer_trace = OptimizerTrace::new(stage.paths());
203
204 let explain_ctx = ExplainContext::Plan(ExplainPlanContext {
205 broken,
206 config,
207 format,
208 stage,
209 replan: Some(gid),
210 desc: None,
211 optimizer_trace,
212 });
213 let stage = return_if_err!(
214 self.create_materialized_view_validate(ctx.session(), plan, resolved_ids, explain_ctx,),
215 ctx
216 );
217 self.sequence_staged(ctx, Span::current(), stage).await;
218 }
219
220 #[instrument]
221 pub(super) fn explain_materialized_view(
222 &self,
223 ctx: &ExecuteContext,
224 plan::ExplainPlanPlan {
225 stage,
226 format,
227 config,
228 explainee,
229 }: plan::ExplainPlanPlan,
230 ) -> Result<ExecuteResponse, AdapterError> {
231 let plan::Explainee::MaterializedView(id) = explainee else {
232 unreachable!() };
234 let CatalogItem::MaterializedView(view) = self.catalog().get_entry(&id).item() else {
235 unreachable!() };
237 let gid = view.global_id_writes();
238
239 let Some(dataflow_metainfo) = self.catalog().try_get_dataflow_metainfo(&gid) else {
240 if !id.is_system() {
241 tracing::error!(
242 "cannot find dataflow metainformation for materialized view {id} in catalog"
243 );
244 }
245 coord_bail!(
246 "cannot find dataflow metainformation for materialized view {id} in catalog"
247 );
248 };
249
250 let target_cluster = self.catalog().get_cluster(view.cluster_id);
251
252 let features = OptimizerFeatures::from(self.catalog().system_config())
253 .override_from(&target_cluster.config.features())
254 .override_from(&self.cluster_scoped_optimizer_overrides(view.cluster_id))
255 .override_from(&config.features);
256
257 let cardinality_stats = BTreeMap::new();
258
259 let explain = match stage {
260 ExplainStage::RawPlan => explain_plan(
261 view.raw_expr.as_ref().clone(),
262 format,
263 &config,
264 &features,
265 &self.catalog().for_session(ctx.session()),
266 cardinality_stats,
267 Some(target_cluster.name.as_str()),
268 )?,
269 ExplainStage::LocalPlan => explain_plan(
270 view.locally_optimized_expr.as_inner().clone(),
271 format,
272 &config,
273 &features,
274 &self.catalog().for_session(ctx.session()),
275 cardinality_stats,
276 Some(target_cluster.name.as_str()),
277 )?,
278 ExplainStage::GlobalPlan => {
279 let Some(plan) = self.catalog().try_get_optimized_plan(&gid).cloned() else {
280 tracing::error!("cannot find {stage} for materialized view {id} in catalog");
281 coord_bail!("cannot find {stage} for materialized view in catalog");
282 };
283 explain_dataflow(
284 plan,
285 format,
286 &config,
287 &features,
288 &self.catalog().for_session(ctx.session()),
289 cardinality_stats,
290 Some(target_cluster.name.as_str()),
291 dataflow_metainfo,
292 )?
293 }
294 ExplainStage::PhysicalPlan => {
295 let Some(plan) = self.catalog().try_get_physical_plan(&gid).cloned() else {
296 tracing::error!("cannot find {stage} for materialized view {id} in catalog",);
297 coord_bail!("cannot find {stage} for materialized view in catalog");
298 };
299 explain_dataflow(
300 plan,
301 format,
302 &config,
303 &features,
304 &self.catalog().for_session(ctx.session()),
305 cardinality_stats,
306 Some(target_cluster.name.as_str()),
307 dataflow_metainfo,
308 )?
309 }
310 _ => {
311 coord_bail!("cannot EXPLAIN {} FOR MATERIALIZED VIEW", stage);
312 }
313 };
314
315 let row = Row::pack_slice(&[Datum::from(explain.as_str())]);
316
317 Ok(Self::send_immediate_rows(row))
318 }
319
320 #[instrument]
321 fn create_materialized_view_validate(
322 &self,
323 session: &Session,
324 plan: plan::CreateMaterializedViewPlan,
325 resolved_ids: ResolvedIds,
326 explain_ctx: ExplainContext,
329 ) -> Result<CreateMaterializedViewStage, AdapterError> {
330 let plan::CreateMaterializedViewPlan {
331 materialized_view:
332 plan::MaterializedView {
333 expr,
334 cluster_id,
335 target_replica,
336 refresh_schedule,
337 ..
338 },
339 ambiguous_columns,
340 ..
341 } = &plan;
342
343 let expr_depends_on = expr.depends_on();
348 self.catalog()
349 .validate_timeline_context(expr_depends_on.iter().copied())?;
350 self.validate_system_column_references(*ambiguous_columns, &expr_depends_on)?;
351 let log_names = expr_depends_on
354 .iter()
355 .map(|gid| self.catalog.resolve_item_id(gid))
356 .flat_map(|item_id| self.catalog().introspection_dependencies(item_id))
357 .map(|item_id| self.catalog().get_entry(&item_id).name().item.clone())
358 .collect::<Vec<_>>();
359 if !log_names.is_empty() {
360 return Err(AdapterError::InvalidLogDependency {
361 object_type: "materialized view".into(),
362 log_names,
363 });
364 }
365
366 let validity = PlanValidity::new(
372 self.catalog(),
373 resolved_ids.items().copied().collect(),
374 Some(*cluster_id),
375 *target_replica,
376 session.role_metadata().clone(),
377 );
378
379 if let Some(refresh_schedule) = refresh_schedule {
381 if !refresh_schedule.ats.is_empty() && matches!(explain_ctx, ExplainContext::None) {
382 let read_holds = self
385 .txn_read_holds
386 .get(session.conn_id())
387 .expect("purification acquired read holds if there are REFRESH ATs");
388 let least_valid_read = read_holds.least_valid_read();
389 for refresh_at_ts in &refresh_schedule.ats {
390 if !least_valid_read.less_equal(refresh_at_ts) {
391 return Err(AdapterError::InputNotReadableAtRefreshAtTime(
392 *refresh_at_ts,
393 least_valid_read,
394 ));
395 }
396 }
397 let ids = self
400 .index_oracle(*cluster_id)
401 .sufficient_collections(resolved_ids.collections().copied());
402 if !ids.difference(&read_holds.id_bundle()).is_empty() {
403 return Err(AdapterError::ChangedPlan(
404 "the set of possible inputs changed during the creation of the \
405 materialized view"
406 .to_string(),
407 ));
408 }
409 }
410 }
411
412 Ok(CreateMaterializedViewStage::Optimize(
413 CreateMaterializedViewOptimize {
414 validity,
415 plan,
416 resolved_ids,
417 explain_ctx,
418 },
419 ))
420 }
421
422 #[instrument]
423 async fn create_materialized_view_optimize(
424 &mut self,
425 conn_id: mz_adapter_types::connection::ConnectionId,
426 CreateMaterializedViewOptimize {
427 validity,
428 plan,
429 resolved_ids,
430 explain_ctx,
431 }: CreateMaterializedViewOptimize,
432 ) -> Result<StageResult<Box<CreateMaterializedViewStage>>, AdapterError> {
433 let plan::CreateMaterializedViewPlan {
434 name,
435 materialized_view:
436 plan::MaterializedView {
437 column_names,
438 cluster_id,
439 non_null_assertions,
440 refresh_schedule,
441 ..
442 },
443 ..
444 } = &plan;
445
446 let compute_instance = self
448 .instance_snapshot(*cluster_id)
449 .expect("compute instance does not exist");
450 let (item_id, global_id) = if let ExplainContext::None = explain_ctx {
451 self.allocate_user_id().await?
452 } else {
453 self.allocate_transient_id()
454 };
455
456 let (_, view_id) = self.allocate_transient_id();
457 let debug_name = self.catalog().resolve_full_name(name, None).to_string();
458 let optimizer_config = optimize::OptimizerConfig::from(self.catalog().system_config())
459 .override_from(&self.catalog.get_cluster(*cluster_id).config.features())
460 .override_from(&self.cluster_scoped_optimizer_overrides(*cluster_id))
461 .override_from(&explain_ctx);
462 let optimizer_features = optimizer_config.features.clone();
463
464 let mut optimizer = optimize::materialized_view::Optimizer::new(
466 self.owned_catalog().as_optimizer_catalog(),
467 compute_instance,
468 global_id,
469 view_id,
470 column_names.clone(),
471 non_null_assertions.clone(),
472 refresh_schedule.clone(),
473 debug_name,
474 optimizer_config,
475 self.optimizer_metrics(),
476 );
477
478 let span = Span::current();
479 Ok(StageResult::Handle(mz_ore::task::spawn_blocking(
480 || "optimize create materialized view",
481 move || {
482 span.in_scope(|| {
483 let mut pipeline = || -> Result<(
484 optimize::materialized_view::LocalMirPlan,
485 optimize::materialized_view::GlobalMirPlan,
486 optimize::materialized_view::GlobalLirPlan,
487 ), AdapterError> {
488 let _dispatch_guard = explain_ctx.dispatch_guard();
489
490 let raw_expr = plan.materialized_view.expr.clone();
491
492 let local_mir_plan = optimizer.catch_unwind_optimize(raw_expr)?;
494 let global_mir_plan =
495 optimizer.catch_unwind_optimize(local_mir_plan.clone())?;
496 let global_lir_plan =
498 optimizer.catch_unwind_optimize(global_mir_plan.clone())?;
499
500 Ok((local_mir_plan, global_mir_plan, global_lir_plan))
501 };
502
503 let stage = match pipeline() {
504 Ok((local_mir_plan, global_mir_plan, global_lir_plan)) => {
505 if let ExplainContext::Plan(explain_ctx) = explain_ctx {
506 let (_, df_meta) = global_lir_plan.unapply();
507 CreateMaterializedViewStage::Explain(
508 CreateMaterializedViewExplain {
509 validity,
510 global_id,
511 plan,
512 df_meta,
513 explain_ctx,
514 },
515 )
516 } else {
517 CreateMaterializedViewStage::Finish(CreateMaterializedViewFinish {
518 item_id,
519 global_id,
520 validity,
521 plan,
522 resolved_ids,
523 local_mir_plan,
524 global_mir_plan,
525 global_lir_plan,
526 optimizer_features,
527 })
528 }
529 }
530 Err(err) => {
533 let ExplainContext::Plan(explain_ctx) = explain_ctx else {
534 return Err(err);
536 };
537
538 if explain_ctx.broken {
539 tracing::error!("error while handling EXPLAIN statement: {}", err);
543 CreateMaterializedViewStage::Explain(
544 CreateMaterializedViewExplain {
545 global_id,
546 validity,
547 plan,
548 df_meta: Default::default(),
549 explain_ctx,
550 },
551 )
552 } else {
553 return Err(err);
555 }
556 }
557 };
558
559 tracing::debug!(
560 target: "mz_adapter::catalog_preparation",
561 %conn_id, kind = "materialized_view", "catalog preparation complete"
562 );
563 fail::fail_point!("catalog_materialized_view_ready");
564 Ok(Box::new(stage))
565 })
566 },
567 )))
568 }
569
570 #[instrument]
571 async fn create_materialized_view_finish(
572 &mut self,
573 ctx: &mut ExecuteContext,
574 stage: CreateMaterializedViewFinish,
575 ) -> Result<StageResult<Box<CreateMaterializedViewStage>>, AdapterError> {
576 let CreateMaterializedViewFinish {
577 item_id,
578 global_id,
579 plan:
580 plan::CreateMaterializedViewPlan {
581 name,
582 materialized_view:
583 plan::MaterializedView {
584 mut create_sql,
585 expr: raw_expr,
586 column_names,
587 dependencies,
588 replacement_target,
589 cluster_id,
590 target_replica,
591 non_null_assertions,
592 compaction_window,
593 refresh_schedule,
594 ..
595 },
596 drop_ids,
597 if_not_exists,
598 ..
599 },
600 resolved_ids,
601 local_mir_plan,
602 global_mir_plan,
603 global_lir_plan,
604 optimizer_features,
605 ..
606 } = stage;
607
608 if let Some(target_id) = replacement_target {
610 let target_entry = self.catalog().get_entry(&target_id);
611 let Some(target) = target_entry.materialized_view() else {
612 return Err(AdapterError::internal(
613 "create materialized view",
614 "replacement target not a materialized view",
615 ));
616 };
617
618 if self.materialized_view_sealed(target_entry) {
621 return Err(AdapterError::ReplaceMaterializedViewSealed {
622 name: target_entry.name().item.clone(),
623 });
624 }
625
626 let schema_diff = target.desc.latest().diff(global_lir_plan.desc());
628 if !schema_diff.is_empty() {
629 return Err(AdapterError::ReplacementSchemaMismatch(schema_diff));
630 }
631 }
632
633 let id_bundle = dataflow_import_id_bundle(global_lir_plan.df_desc(), cluster_id);
635
636 let read_holds_owned;
637 let read_holds = if let Some(txn_reads) = self.txn_read_holds.get(ctx.session().conn_id()) {
638 txn_reads
642 } else {
643 read_holds_owned = self.acquire_read_holds(&id_bundle);
646 &read_holds_owned
647 };
648
649 let (dataflow_as_of, storage_as_of, until) =
650 self.select_timestamps(id_bundle, refresh_schedule.as_ref(), read_holds)?;
651
652 tracing::info!(
653 dataflow_as_of = ?dataflow_as_of,
654 storage_as_of = ?storage_as_of,
655 until = ?until,
656 "materialized view timestamp selection",
657 );
658
659 let initial_as_of = storage_as_of.clone();
660
661 if let Some(storage_as_of_ts) = storage_as_of.as_option() {
666 let stmt = mz_sql::parse::parse(&create_sql)
667 .map_err(|_| {
668 AdapterError::internal(
669 "create materialized view",
670 "original SQL should roundtrip",
671 )
672 })?
673 .into_element()
674 .ast;
675 let ast::Statement::CreateMaterializedView(mut stmt) = stmt else {
676 panic!("unexpected statement type");
677 };
678 stmt.as_of = Some(storage_as_of_ts.into());
679 create_sql = stmt.to_ast_string_stable();
680 }
681
682 let desc = VersionedRelationDesc::new(global_lir_plan.desc().clone());
683 let collections = [(RelationVersion::root(), global_id)].into_iter().collect();
684
685 let local_mir_for_cache = local_mir_plan.expr();
686
687 let ops = vec![
688 catalog::Op::DropObjects(
689 drop_ids
690 .into_iter()
691 .map(catalog::DropObjectInfo::Item)
692 .collect(),
693 ),
694 catalog::Op::CreateItem {
695 id: item_id,
696 name: name.clone(),
697 item: CatalogItem::MaterializedView(MaterializedView {
698 create_sql,
699 raw_expr: raw_expr.into(),
700 locally_optimized_expr: local_mir_plan.expr().into(),
701 desc,
702 collections,
703 resolved_ids,
704 dependencies,
705 replacement_target,
706 cluster_id,
707 target_replica,
708 non_null_assertions,
709 custom_logical_compaction_window: compaction_window,
710 refresh_schedule: refresh_schedule.clone(),
711 initial_as_of: Some(initial_as_of.clone()),
712 optimized_plan: None,
713 physical_plan: None,
714 dataflow_metainfo: None,
715 }),
716 owner_id: *ctx.session().current_role_id(),
717 },
718 ];
719
720 let notice_ids = std::iter::repeat_with(|| self.allocate_transient_id())
722 .map(|(_item_id, global_id)| global_id)
723 .take(global_lir_plan.df_meta().optimizer_notices.len())
724 .collect::<Vec<_>>();
725
726 let output_desc = global_lir_plan.desc().clone();
739 let (mut df_desc, raw_df_meta) = global_lir_plan.unapply();
740 let df_meta = {
741 let system_catalog = self.catalog().for_system_session();
742 let full_name = self.catalog().resolve_full_name(&name, None);
743 let transient_items = btreemap! {
744 global_id => TransientItem::new(
745 Some(full_name.into_parts()),
746 Some(column_names.iter().map(|c| c.to_string()).collect()),
747 )
748 };
749 let humanizer = ExprHumanizerExt::new(transient_items, &system_catalog);
750 CatalogState::render_notices_core(
751 &humanizer,
752 (self.catalog().config().now)(),
753 &raw_df_meta,
754 notice_ids,
755 Some(global_id),
756 )
757 };
758
759 self.catalog()
764 .cache_expressions(
765 global_id,
766 Some(local_mir_for_cache),
767 global_mir_plan.df_desc().clone(),
768 df_desc.clone(),
769 df_meta.clone(),
770 optimizer_features,
771 )
772 .await;
773
774 let transact_result = self
775 .catalog_transact_with_side_effects(Some(ctx), ops, move |coord, _ctx| {
776 Box::pin(async move {
777 coord
779 .catalog_mut()
780 .set_optimized_plan(global_id, global_mir_plan.df_desc().clone());
781 coord
782 .catalog_mut()
783 .set_physical_plan(global_id, df_desc.clone());
784
785 let notice_builtin_updates_fut =
786 coord.persist_dataflow_metainfo(df_meta, global_id);
787
788 df_desc.set_as_of(dataflow_as_of.clone());
789 df_desc.set_initial_as_of(initial_as_of);
790 df_desc.until = until;
791
792 let storage_metadata = coord.catalog.state().storage_metadata();
793
794 let mut collection_desc =
795 CollectionDescription::for_other(output_desc, Some(storage_as_of));
796 let mut allow_writes = true;
797
798 if let Some(target_id) = replacement_target {
801 let target_gid = coord.catalog.get_entry(&target_id).latest_global_id();
802 collection_desc.primary = Some(target_gid);
803 allow_writes = false;
804 }
805
806 coord
808 .controller
809 .storage
810 .create_collections(
811 storage_metadata,
812 None,
813 vec![(global_id, collection_desc)],
814 )
815 .await
816 .unwrap_or_terminate("cannot fail to append");
817
818 coord
819 .initialize_storage_read_policies(
820 btreeset![item_id],
821 compaction_window.unwrap_or(CompactionWindow::Default),
822 )
823 .await;
824
825 coord
826 .ship_dataflow_and_notice_builtin_table_updates(
827 df_desc,
828 cluster_id,
829 notice_builtin_updates_fut,
830 target_replica,
831 )
832 .await;
833
834 if allow_writes {
835 coord.allow_writes(cluster_id, global_id);
836 }
837 })
838 })
839 .await;
840
841 match transact_result {
842 Ok(_) => {
843 self.emit_raw_optimizer_notices_to_user(ctx, &raw_df_meta.optimizer_notices);
848 Ok(ExecuteResponse::CreatedMaterializedView)
849 }
850 Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
851 kind:
852 mz_catalog::memory::error::ErrorKind::Sql(
853 CatalogError::ItemAlreadyExists(_, _),
854 ),
855 })) if if_not_exists => {
856 ctx.session()
857 .add_notice(AdapterNotice::ObjectAlreadyExists {
858 name: name.item,
859 ty: "materialized view",
860 });
861 Ok(ExecuteResponse::CreatedMaterializedView)
862 }
863 Err(err) => Err(err),
864 }
865 .map(StageResult::Response)
866 }
867
868 fn select_timestamps(
871 &self,
872 id_bundle: CollectionIdBundle,
873 refresh_schedule: Option<&RefreshSchedule>,
874 read_holds: &ReadHolds,
875 ) -> Result<
876 (
877 Antichain<mz_repr::Timestamp>,
878 Antichain<mz_repr::Timestamp>,
879 Antichain<mz_repr::Timestamp>,
880 ),
881 AdapterError,
882 > {
883 assert!(
884 id_bundle.difference(&read_holds.id_bundle()).is_empty(),
885 "we must have read holds for all involved collections"
886 );
887
888 let least_valid_read = read_holds.least_valid_read();
891 let mut dataflow_as_of = least_valid_read.clone();
892 let mut storage_as_of = least_valid_read.clone();
893
894 if let Some(refresh_schedule) = &refresh_schedule {
904 if let Some(least_valid_read_ts) = least_valid_read.as_option() {
905 if let Some(first_refresh_ts) =
906 refresh_schedule.round_up_timestamp(*least_valid_read_ts)
907 {
908 storage_as_of = Antichain::from_elem(first_refresh_ts);
909 dataflow_as_of.join_assign(
910 &self
911 .greatest_available_read(&id_bundle)
912 .meet(&storage_as_of),
913 );
914 } else {
915 let last_refresh = refresh_schedule.last_refresh().expect(
916 "if round_up_timestamp returned None, then there should be a last refresh",
917 );
918
919 return Err(AdapterError::MaterializedViewWouldNeverRefresh(
920 last_refresh,
921 *least_valid_read_ts,
922 ));
923 }
924 } else {
925 soft_panic_or_log!("creating a materialized view with an empty `as_of`");
927 }
928 }
929
930 let until_ts = refresh_schedule
934 .and_then(|s| s.last_refresh())
935 .and_then(|r| r.try_step_forward());
936 let until = Antichain::from_iter(until_ts);
937
938 Ok((dataflow_as_of, storage_as_of, until))
939 }
940
941 #[instrument]
942 async fn create_materialized_view_explain(
943 &self,
944 session: &Session,
945 CreateMaterializedViewExplain {
946 global_id,
947 plan:
948 plan::CreateMaterializedViewPlan {
949 name,
950 materialized_view:
951 plan::MaterializedView {
952 column_names,
953 cluster_id,
954 ..
955 },
956 ..
957 },
958 df_meta,
959 explain_ctx:
960 ExplainPlanContext {
961 config,
962 format,
963 stage,
964 optimizer_trace,
965 ..
966 },
967 ..
968 }: CreateMaterializedViewExplain,
969 ) -> Result<StageResult<Box<CreateMaterializedViewStage>>, AdapterError> {
970 let session_catalog = self.catalog().for_session(session);
971 let expr_humanizer = {
972 let full_name = self.catalog().resolve_full_name(&name, None);
973 let transient_items = btreemap! {
974 global_id => TransientItem::new(
975 Some(full_name.into_parts()),
976 Some(column_names.iter().map(|c| c.to_string()).collect()),
977 )
978 };
979 ExprHumanizerExt::new(transient_items, &session_catalog)
980 };
981
982 let target_cluster = self.catalog().get_cluster(cluster_id);
983
984 let features = OptimizerFeatures::from(self.catalog().system_config())
985 .override_from(&target_cluster.config.features())
986 .override_from(&self.cluster_scoped_optimizer_overrides(cluster_id))
987 .override_from(&config.features);
988
989 let rows = optimizer_trace
990 .into_rows(
991 format,
992 &config,
993 &features,
994 &expr_humanizer,
995 None,
996 Some(target_cluster),
997 df_meta,
998 stage,
999 plan::ExplaineeStatementKind::CreateMaterializedView,
1000 None,
1001 )
1002 .await?;
1003
1004 Ok(StageResult::Response(Self::send_immediate_rows(rows)))
1005 }
1006
1007 pub(crate) async fn explain_pushdown_materialized_view(
1008 &self,
1009 ctx: ExecuteContext,
1010 item_id: CatalogItemId,
1011 ) {
1012 let CatalogItem::MaterializedView(mview) = self.catalog().get_entry(&item_id).item() else {
1013 unreachable!() };
1015 let gid = mview.global_id_writes();
1016 let mview = mview.clone();
1017
1018 let Some(plan) = self.catalog().try_get_physical_plan(&gid).cloned() else {
1019 let msg = format!("cannot find plan for materialized view {item_id} in catalog");
1020 tracing::error!("{msg}");
1021 ctx.retire(Err(anyhow!("{msg}").into()));
1022 return;
1023 };
1024
1025 let id_bundle = CollectionIdBundle {
1034 storage_ids: plan.source_imports.keys().copied().collect(),
1035 compute_ids: BTreeMap::new(),
1036 };
1037 let read_holds = Some(self.acquire_read_holds(&id_bundle));
1038
1039 let frontiers = self
1040 .controller
1041 .compute
1042 .collection_frontiers(gid, Some(mview.cluster_id))
1043 .expect("materialized view exists");
1044
1045 let as_of = frontiers.read_frontier.to_owned();
1046
1047 let until = mview
1048 .refresh_schedule
1049 .as_ref()
1050 .and_then(|s| s.last_refresh())
1051 .unwrap_or(mz_repr::Timestamp::MAX);
1052
1053 let mz_now = match as_of.as_option() {
1054 Some(&as_of) => {
1055 ResultSpec::value_between(Datum::MzTimestamp(as_of), Datum::MzTimestamp(until))
1056 }
1057 None => ResultSpec::value_all(),
1058 };
1059
1060 self.execute_explain_pushdown_with_read_holds(
1061 ctx,
1062 as_of,
1063 mz_now,
1064 read_holds,
1065 plan.source_imports
1066 .into_iter()
1067 .filter_map(|(id, import)| import.desc.arguments.operators.map(|plan| (id, plan))),
1068 )
1069 .await
1070 }
1071}