Skip to main content

mz_adapter/coord/sequencer/inner/
create_materialized_view.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use 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            // This is currently asserted in the `sequence_explain_plan` code that
135            // calls this method.
136            unreachable!()
137        };
138        let plan::ExplaineeStatement::CreateMaterializedView { broken, plan } = stmt else {
139            // This is currently asserted in the `sequence_explain_plan` code that
140            // calls this method.
141            unreachable!()
142        };
143
144        // Create an OptimizerTrace instance to collect plans emitted when
145        // executing the optimizer pipeline.
146        let optimizer_trace = OptimizerTrace::new(stage.paths());
147
148        // Not used in the EXPLAIN path so it's OK to generate a dummy value.
149        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!() // Asserted in `sequence_explain_plan`.
180        };
181        let CatalogItem::MaterializedView(item) = self.catalog().get_entry(&id).item() else {
182            unreachable!() // Asserted in `plan_explain_plan`.
183        };
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!() // We are parsing the `create_sql` of a `MaterializedView` item.
194        };
195
196        // It is safe to assume that query optimization will always succeed, so
197        // for now we statically assume `broken = false`.
198        let broken = false;
199
200        // Create an OptimizerTrace instance to collect plans emitted when
201        // executing the optimizer pipeline.
202        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!() // Asserted in `sequence_explain_plan`.
233        };
234        let CatalogItem::MaterializedView(view) = self.catalog().get_entry(&id).item() else {
235            unreachable!() // Asserted in `plan_explain_plan`.
236        };
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        // An optional context set iff the state machine is initiated from
327        // sequencing an EXPLAIN for this statement.
328        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        // Validate any references in the materialized view's expression. We do
344        // this on the unoptimized plan to better reflect what the user typed.
345        // We want to reject queries that depend on log sources, for example,
346        // even if we can *technically* optimize that reference away.
347        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        // Materialized views are not allowed to depend on log sources, as replicas
352        // are not producing the same definite collection for these.
353        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        // Track the target cluster/replica and resolved dependencies so that
367        // concurrent drops (e.g. `ALTER CLUSTER ... SET (REPLICATION FACTOR
368        // ...)` racing with the off-thread optimizer) are caught between
369        // stages instead of panicking later when the persisted SQL is
370        // re-parsed during catalog application.
371        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        // Check whether we can read all inputs at all the REFRESH AT times.
380        if let Some(refresh_schedule) = refresh_schedule {
381            if !refresh_schedule.ats.is_empty() && matches!(explain_ctx, ExplainContext::None) {
382                // Purification has acquired the earliest possible read holds if there are any
383                // REFRESH options.
384                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                // Also check that no new id has appeared in `sufficient_collections` (e.g. a new
398                // index), otherwise we might be missing some read holds.
399                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        // Collect optimizer parameters.
447        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        // Build an optimizer for this MATERIALIZED VIEW.
465        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                        // HIR ⇒ MIR lowering and MIR ⇒ MIR optimization (local and global)
493                        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                        // MIR ⇒ LIR lowering and LIR ⇒ LIR optimization (global)
497                        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                        // Internal optimizer errors are handled differently
531                        // depending on the caller.
532                        Err(err) => {
533                            let ExplainContext::Plan(explain_ctx) = explain_ctx else {
534                                // In `sequence_~` contexts, immediately return the error.
535                                return Err(err);
536                            };
537
538                            if explain_ctx.broken {
539                                // In `EXPLAIN BROKEN` contexts, just log the error
540                                // and move to the next stage with default
541                                // parameters.
542                                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                                // In regular `EXPLAIN` contexts, immediately return the error.
554                                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        // Validate the replacement target, if one is given.
609        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            // Best-effort: the target can still seal after this check, so `APPLY REPLACEMENT`
619            // checks again.
620            if self.materialized_view_sealed(target_entry) {
621                return Err(AdapterError::ReplaceMaterializedViewSealed {
622                    name: target_entry.name().item.clone(),
623                });
624            }
625
626            // For now, we don't support schema evolution for materialized views.
627            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        // Timestamp selection
634        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            // In some cases, for example when REFRESH is used, the preparatory
639            // stages will already have acquired ReadHolds, we can re-use those.
640
641            txn_reads
642        } else {
643            // No one has acquired holds, make sure we can determine an as_of
644            // and render our dataflow below.
645            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        // Update the `create_sql` with the selected `as_of`. This is how we make sure the `as_of`
662        // is persisted to the catalog and can be relied on during bootstrapping.
663        // This has to be the `storage_as_of`, because bootstrapping uses this in
664        // `bootstrap_storage_collections`.
665        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        // Pre-allocate a vector of transient GlobalIds for each notice.
721        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        // Render optimizer notices before the catalog transaction. We wrap
727        // the system-session humanizer with an `ExprHumanizerExt` so that
728        // references to the to-be-created materialized view's own
729        // `global_id` in the persisted notice text resolve to its intended
730        // human-readable name.
731        //
732        // We keep `raw_df_meta` live so that on success we can emit its raw
733        // notices to the user session (rendered against the user's
734        // session-aware humanizer). We deliberately do NOT emit to the user
735        // here, so that if the catalog transaction below fails the user
736        // isn't shown confusing notices about an item that wasn't actually
737        // created.
738        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        // Populate the durable expression cache before the catalog
760        // transaction and await the write. This way any other envd (or a
761        // subsequent bootstrap here) will observe the cached plans +
762        // rendered notices as soon as the item becomes visible.
763        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                    // Save plan structures.
778                    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 this MV is intended to replace another one, we need to start it in
799                    // read-only mode, targeting the shard of the replacement target.
800                    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                    // Announce the creation of the materialized view source.
807                    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                // Only emit optimizer notices to the user now that the
844                // catalog transaction has succeeded. If the transaction had
845                // failed, emitting notices would confuse the user with
846                // information about an item that wasn't actually created.
847                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    /// Select the initial `dataflow_as_of`, `storage_as_of`, and `until` frontiers for a
869    /// materialized view.
870    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        // For non-REFRESH MVs both the `dataflow_as_of` and the `storage_as_of` should be simply
889        // `least_valid_read`.
890        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        // For MVs with non-trivial REFRESH schedules:
895        // 1. it's important to set the `storage_as_of` to the first refresh. This is because we'd
896        // like queries on the MV to block until the first refresh (rather than to show an empty
897        // MV).
898        // 2. We move the `dataflow_as_of` forward to the minimum of `greatest_available_read` and
899        // the first refresh time. There is no point in processing the times before
900        // `greatest_available_read`, because the first time for which results will be exposed is
901        // the first refresh time. Also note that simply moving the `dataflow_as_of` forward to the
902        // first refresh time would prevent warmup before the first refresh.
903        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                // The `as_of` should never be empty, because then the MV would be unreadable.
926                soft_panic_or_log!("creating a materialized view with an empty `as_of`");
927            }
928        }
929
930        // If we have a refresh schedule that has a last refresh, then set the `until` to the last refresh.
931        // (If the `try_step_forward` fails, then no need to set an `until`, because it's not possible to get any data
932        // beyond that last refresh time, because there are no times beyond that time.)
933        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!() // Asserted in `sequence_explain_pushdown`.
1014        };
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        // We don't have any way to "duplicate" the read hold of the actual collection, which we
1026        // obtain below... but the current implementation of read holds guarantees that the storage
1027        // holds we obtain here will not be any greater than the hold we actually want.
1028        //
1029        // We hold only the plan's storage imports, whose persist part stats the pushdown
1030        // explanation reads. We must not acquire holds on the plan's compute imports: an index
1031        // that the materialized view was planned against can be dropped while the view keeps
1032        // running, and a dropped index no longer has a compute collection to hold.
1033        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}