Skip to main content

mz_adapter/coord/sequencer/
inner.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 std::borrow::Cow;
11use std::collections::{BTreeMap, BTreeSet, VecDeque};
12use std::iter;
13use std::num::{NonZeroI64, NonZeroUsize};
14use std::sync::Arc;
15use std::time::{Duration, Instant};
16
17use anyhow::anyhow;
18use futures::future::{BoxFuture, FutureExt};
19use futures::{Future, StreamExt, future};
20use itertools::Itertools;
21use maplit::btreemap;
22use mz_adapter_types::compaction::CompactionWindow;
23use mz_adapter_types::connection::ConnectionId;
24use mz_adapter_types::dyncfgs::{
25    ENABLE_EXPRESSION_CACHE, ENABLE_PASSWORD_AUTH, FRONTEND_READ_THEN_WRITE,
26    READ_THEN_WRITE_MAX_DEPENDENCIES,
27};
28use mz_catalog::memory::error::ErrorKind;
29use mz_catalog::memory::objects::{
30    CatalogItem, Connection, DataSourceDesc, Sink, Source, Table, TableDataSource, Type,
31};
32use mz_compute_types::ComputeInstanceId;
33use mz_compute_types::dataflows::DataflowDescription;
34use mz_compute_types::plan::LirRelationExpr;
35use mz_expr::{
36    CollectionPlan, Eval, MapFilterProject, OptimizedMirRelationExpr, ResultSpec, RowSetFinishing,
37};
38use mz_ore::cast::CastFrom;
39use mz_ore::collections::{CollectionExt, HashSet};
40use mz_ore::future::OreFutureExt;
41use mz_ore::task::{self, JoinHandle, spawn};
42use mz_ore::tracing::OpenTelemetryContext;
43use mz_ore::{assert_none, instrument};
44use mz_repr::adt::jsonb::Jsonb;
45use mz_repr::adt::mz_acl_item::{MzAclItem, PrivilegeMap};
46use mz_repr::explain::json::json_string;
47use mz_repr::explain::{ExprHumanizer, ExprHumanizerExt, TransientItem};
48use mz_repr::role_id::RoleId;
49use mz_repr::{
50    CatalogItemId, Datum, Diff, GlobalId, RelationDesc, RelationVersion, RelationVersionSelector,
51    Row, RowArena, RowIterator, Timestamp,
52};
53use mz_secrets::SecretsReader;
54use mz_sql::ast::{
55    AlterSourceAddSubsourceOption, CreateSinkOption, CreateSinkOptionName, CreateSourceOptionName,
56    CreateSubsourceOption, CreateSubsourceOptionName, SqlServerConfigOption,
57    SqlServerConfigOptionName,
58};
59use mz_sql::ast::{CreateSubsourceStatement, MySqlConfigOptionName, UnresolvedItemName};
60use mz_sql::catalog::{
61    CatalogCluster, CatalogClusterReplica, CatalogDatabase, CatalogError,
62    CatalogItem as SqlCatalogItem, CatalogRole, CatalogSchema, CatalogTypeDetails,
63    ErrorMessageObjectDescription, ObjectType, RoleAttributesRaw, RoleVars, SessionCatalog,
64};
65use mz_sql::names::{
66    Aug, ObjectId, QualifiedItemName, ResolvedDatabaseSpecifier, ResolvedIds, ResolvedItemName,
67    SchemaSpecifier, SystemObjectId,
68};
69use mz_sql::plan::{
70    AlterMaterializedViewApplyReplacementPlan, ConnectionDetails, NetworkPolicyRule, PlanError,
71    StatementContext,
72};
73use mz_sql::pure::{PurifiedSourceExport, generate_subsource_statements};
74use mz_storage_types::sinks::StorageSinkDesc;
75use mz_timestamp_oracle::TimestampOracle;
76// Import `plan` module, but only import select elements to avoid merge conflicts on use statements.
77use mz_sql::plan::{
78    AlterConnectionAction, AlterConnectionPlan, CreateSourcePlanBundle, ExplainSinkSchemaPlan,
79    Explainee, ExplaineeStatement, MutationKind, Params, Plan, PlannedAlterRoleOption,
80    PlannedRoleVariable, QueryWhen, SideEffectingFunc, UpdatePrivilege, VariableValue,
81};
82use mz_sql::session::metadata::SessionMetadata;
83use mz_sql::session::user::UserKind;
84use mz_sql::session::vars::{
85    self, IsolationLevel, MAX_CONCURRENT_OCC_WRITES, NETWORK_POLICY, OwnedVarInput, SCHEMA_ALIAS,
86    TRANSACTION_ISOLATION_VAR_NAME, Var, VarError, VarInput,
87};
88use mz_sql::{plan, rbac};
89use mz_sql_parser::ast::display::AstDisplay;
90use mz_sql_parser::ast::{
91    ConnectionOption, ConnectionOptionName, CreateSourceConnection, DeferredItemName,
92    MySqlConfigOption, PgConfigOption, PgConfigOptionName, Statement, TransactionMode,
93    WithOptionValue,
94};
95use mz_ssh_util::keys::SshKeyPairSet;
96use mz_storage_client::controller::ExportDescription;
97use mz_storage_types::AlterCompatible;
98use mz_storage_types::connections::AwsPrivatelinkConnection;
99use mz_storage_types::connections::inline::IntoInlineConnection;
100use mz_storage_types::controller::StorageError;
101use mz_storage_types::sources::SourceConnection;
102use mz_transform::dataflow::DataflowMetainfo;
103use mz_transform::notice::{OptimizerNotice, RawOptimizerNotice};
104use smallvec::SmallVec;
105use timely::progress::Antichain;
106use tokio::sync::{oneshot, watch};
107use tracing::{Instrument, Span, info, warn};
108
109use crate::catalog::{
110    self, Catalog, CatalogState, ConnCatalog, DropObjectInfo, UpdatePrivilegeVariant,
111};
112use crate::command::{ExecuteResponse, Response};
113use crate::coord::appends::{
114    BuiltinTableAppendNotify, DeferredOp, DeferredPlan, PendingWriteTxn, UserWriteResponder,
115};
116use crate::coord::read_then_write::{DependencyPolicy, validate_read_then_write_dependencies};
117use crate::coord::sequencer::emit_optimizer_notices;
118use crate::coord::{
119    AlterConnectionValidationReady, AlterMaterializedViewReadyContext, AlterSinkReadyContext,
120    Coordinator, CreateConnectionValidationReady, DeferredPlanStatement, ExecuteContext,
121    ExplainContext, Message, NetworkPolicyError, PendingRead, PendingReadTxn, PendingTxn,
122    PendingTxnResponse, PlanValidity, StageResult, Staged, StagedContext, TargetCluster,
123    WatchSetResponse, validate_ip_with_policy_rules,
124};
125use crate::error::AdapterError;
126use crate::notice::{AdapterNotice, DroppedInUseIndex};
127use crate::optimize::dataflows::{EvalTime, ExprPrep, ExprPrepOneShot};
128use crate::optimize::{self, Optimize};
129use crate::session::{
130    EndTransactionAction, RequireLinearization, Session, TransactionOps, TransactionStatus,
131    WriteLocks, WriteOp,
132};
133use crate::util::{ClientTransmitter, ResultExt, viewable_variables};
134use crate::{CollectionIdBundle, PeekResponseUnary, ReadHolds};
135
136/// A future that resolves to a real-time recency timestamp.
137type RtrTimestampFuture = BoxFuture<'static, Result<Timestamp, StorageError>>;
138
139mod cluster;
140mod copy_from;
141mod create_index;
142mod create_materialized_view;
143mod create_metric_sink;
144mod create_view;
145mod explain_timestamp;
146mod peek;
147mod secret;
148mod subscribe;
149
150/// Attempts to evaluate an expression. If an error is returned then the error is sent
151/// to the client and the function is exited.
152macro_rules! return_if_err {
153    ($expr:expr, $ctx:expr) => {
154        match $expr {
155            Ok(v) => v,
156            Err(e) => return $ctx.retire(Err(e.into())),
157        }
158    };
159}
160
161pub(super) use return_if_err;
162
163fn spawn_linearized_read_ts<S>(
164    oracle: Option<Arc<dyn TimestampOracle<Timestamp> + Send + Sync>>,
165    name: &'static str,
166    build_stage: impl FnOnce(Option<Timestamp>) -> S + Send + 'static,
167) -> StageResult<Box<S>>
168where
169    S: Send + 'static,
170{
171    match oracle {
172        Some(oracle) => {
173            let span = Span::current();
174            StageResult::Handle(mz_ore::task::spawn(
175                move || name,
176                async move {
177                    let oracle_read_ts = oracle.read_ts().await;
178                    Ok(Box::new(build_stage(Some(oracle_read_ts))))
179                }
180                .instrument(span),
181            ))
182        }
183        None => StageResult::Immediate(Box::new(build_stage(None))),
184    }
185}
186
187/// Rejects connection options whose values cannot work, independent of any
188/// external system.
189fn check_connection_details(details: &ConnectionDetails) -> Result<(), PlanError> {
190    match details {
191        ConnectionDetails::AwsPrivatelink(privatelink) => {
192            AwsPrivatelinkConnection::check_service_name(&privatelink.service_name)
193                .map_err(PlanError::InvalidPrivatelinkServiceName)
194        }
195        _ => Ok(()),
196    }
197}
198
199struct DropOps {
200    ops: Vec<catalog::Op>,
201    dropped_active_db: bool,
202    dropped_active_cluster: bool,
203    dropped_in_use_indexes: Vec<DroppedInUseIndex>,
204}
205
206// A bundle of values returned from create_source_inner
207struct CreateSourceInner {
208    ops: Vec<catalog::Op>,
209    sources: Vec<(CatalogItemId, QualifiedItemName, Source)>,
210    if_not_exists_ids: BTreeMap<CatalogItemId, QualifiedItemName>,
211}
212
213impl Coordinator {
214    /// Sequences a [Staged] plan.
215    ///
216    /// This is designed for plans that execute both on and off the coordinator
217    /// thread. Stages can either produce another stage to execute or a final
218    /// response. Maintains the connection-scoped cancel watch in
219    /// `connection_cancel_watches` while a stage is cancelable.
220    pub(crate) async fn sequence_staged<S>(
221        &mut self,
222        mut ctx: S::Ctx,
223        parent_span: Span,
224        mut stage: S,
225    ) where
226        S: Staged + 'static,
227        S::Ctx: Send + 'static,
228    {
229        return_if_err!(stage.validity().check(self.catalog()), ctx);
230        loop {
231            let mut cancel_enabled = stage.cancel_enabled();
232            if let Some(session) = ctx.session() {
233                if cancel_enabled {
234                    // Channel to await cancellation. Insert a new channel, but check if the previous one
235                    // was already canceled.
236                    if let Some((_prev_tx, prev_rx)) = self
237                        .connection_cancel_watches
238                        .insert(session.conn_id().clone(), watch::channel(false))
239                    {
240                        let was_canceled = *prev_rx.borrow();
241                        if was_canceled {
242                            ctx.retire(Err(AdapterError::Canceled));
243                            return;
244                        }
245                    }
246                } else {
247                    // If no cancel allowed, remove it so handle_spawn doesn't observe any previous value
248                    // when cancel_enabled may have been true on an earlier stage.
249                    self.connection_cancel_watches.remove(session.conn_id());
250                }
251            } else {
252                cancel_enabled = false
253            };
254            let next = stage
255                .stage(self, &mut ctx)
256                .instrument(parent_span.clone())
257                .await;
258            let res = return_if_err!(next, ctx);
259            stage = match res {
260                StageResult::Handle(handle) => {
261                    let internal_cmd_tx = self.internal_cmd_tx.clone();
262                    self.handle_spawn(ctx, handle, cancel_enabled, move |ctx, next| {
263                        let _ = internal_cmd_tx.send(next.message(ctx, parent_span));
264                    });
265                    return;
266                }
267                StageResult::HandleRetire(handle) => {
268                    self.handle_spawn(ctx, handle, cancel_enabled, move |ctx, resp| {
269                        ctx.retire(Ok(resp));
270                    });
271                    return;
272                }
273                StageResult::Response(resp) => {
274                    ctx.retire(Ok(resp));
275                    return;
276                }
277                StageResult::Immediate(stage) => *stage,
278            }
279        }
280    }
281
282    /// Waits for either the spawned stage work to complete or cancellation to
283    /// be signaled through the connection-scoped cancel watch.
284    fn handle_spawn<C, T, F>(
285        &self,
286        ctx: C,
287        handle: JoinHandle<Result<T, AdapterError>>,
288        cancel_enabled: bool,
289        f: F,
290    ) where
291        C: StagedContext + Send + 'static,
292        T: Send + 'static,
293        F: FnOnce(C, T) + Send + 'static,
294    {
295        let rx: BoxFuture<()> = if let Some((_tx, rx)) = ctx
296            .session()
297            .and_then(|session| self.connection_cancel_watches.get(session.conn_id()))
298        {
299            let mut rx = rx.clone();
300            Box::pin(async move {
301                // Wait for true or dropped sender.
302                let _ = rx.wait_for(|v| *v).await;
303                ()
304            })
305        } else {
306            Box::pin(future::pending())
307        };
308        spawn(|| "sequence_staged", async move {
309            tokio::select! {
310                res = handle => {
311                    let next = return_if_err!(res, ctx);
312                    f(ctx, next);
313                }
314                _ = rx, if cancel_enabled => {
315                    ctx.retire(Err(AdapterError::Canceled));
316                }
317            }
318        });
319    }
320
321    async fn create_source_inner(
322        &self,
323        session: &Session,
324        plans: Vec<plan::CreateSourcePlanBundle>,
325    ) -> Result<CreateSourceInner, AdapterError> {
326        let mut ops = vec![];
327        let mut sources = vec![];
328
329        let if_not_exists_ids = plans
330            .iter()
331            .filter_map(
332                |plan::CreateSourcePlanBundle {
333                     item_id,
334                     global_id: _,
335                     plan,
336                     resolved_ids: _,
337                     available_source_references: _,
338                 }| {
339                    if plan.if_not_exists {
340                        Some((*item_id, plan.name.clone()))
341                    } else {
342                        None
343                    }
344                },
345            )
346            .collect::<BTreeMap<_, _>>();
347
348        for plan::CreateSourcePlanBundle {
349            item_id,
350            global_id,
351            mut plan,
352            resolved_ids,
353            available_source_references,
354        } in plans
355        {
356            let name = plan.name.clone();
357
358            // Attempt to reduce the `CHECK` expression, we timeout if this takes too long.
359            if let mz_sql::plan::DataSourceDesc::Webhook {
360                validate_using: Some(validate),
361                ..
362            } = &mut plan.source.data_source
363            {
364                if let Err(reason) = validate.reduce_expression().await {
365                    self.metrics
366                        .webhook_validation_reduce_failures
367                        .with_label_values(&[reason])
368                        .inc();
369                    return Err(AdapterError::Internal(format!(
370                        "failed to reduce check expression, {reason}"
371                    )));
372                }
373            }
374
375            // If this source contained a set of available source references, update the
376            // source references catalog table.
377            let mut reference_ops = vec![];
378            if let Some(references) = &available_source_references {
379                reference_ops.push(catalog::Op::UpdateSourceReferences {
380                    source_id: item_id,
381                    references: references.clone().into(),
382                });
383            }
384
385            let source = Source::new(plan, global_id, resolved_ids, None, false);
386            ops.push(catalog::Op::CreateItem {
387                id: item_id,
388                name: name.clone(),
389                item: CatalogItem::Source(source.clone()),
390                owner_id: *session.current_role_id(),
391            });
392            sources.push((item_id, name, source));
393            // These operations must be executed after the source is added to the catalog.
394            ops.extend(reference_ops);
395        }
396
397        Ok(CreateSourceInner {
398            ops,
399            sources,
400            if_not_exists_ids,
401        })
402    }
403
404    /// Subsources are planned differently from other statements because they
405    /// are typically synthesized from other statements, e.g. `CREATE SOURCE`.
406    /// Because of this, we have usually "missed" the opportunity to plan them
407    /// through the normal statement execution life cycle (the exception being
408    /// during bootstrapping).
409    ///
410    /// The caller needs to provide a `CatalogItemId` and `GlobalId` for the sub-source.
411    pub(crate) fn plan_subsource(
412        &self,
413        session: &Session,
414        params: &mz_sql::plan::Params,
415        subsource_stmt: CreateSubsourceStatement<mz_sql::names::Aug>,
416        item_id: CatalogItemId,
417        global_id: GlobalId,
418    ) -> Result<CreateSourcePlanBundle, AdapterError> {
419        let catalog = self.catalog().for_session(session);
420        let resolved_ids = mz_sql::names::visit_dependencies(&catalog, &subsource_stmt);
421
422        let (plan, _sql_impl_ids) = self.plan_statement(
423            session,
424            Statement::CreateSubsource(subsource_stmt),
425            params,
426            &resolved_ids,
427        )?;
428        let plan = match plan {
429            Plan::CreateSource(plan) => plan,
430            _ => unreachable!(),
431        };
432        Ok(CreateSourcePlanBundle {
433            item_id,
434            global_id,
435            plan,
436            resolved_ids,
437            available_source_references: None,
438        })
439    }
440
441    /// Prepares an `ALTER SOURCE...ADD SUBSOURCE`.
442    pub(crate) async fn plan_purified_alter_source_add_subsource(
443        &mut self,
444        session: &Session,
445        params: Params,
446        source_name: ResolvedItemName,
447        options: Vec<AlterSourceAddSubsourceOption<Aug>>,
448        subsources: BTreeMap<UnresolvedItemName, PurifiedSourceExport>,
449    ) -> Result<(Plan, ResolvedIds), AdapterError> {
450        let mut subsource_plans = Vec::with_capacity(subsources.len());
451
452        // Generate subsource statements
453        let conn_catalog = self.catalog().for_system_session();
454        let pcx = plan::PlanContext::zero();
455        let scx = StatementContext::new(Some(&pcx), &conn_catalog);
456
457        let entry = self.catalog().get_entry(source_name.item_id());
458        let source = entry.source().ok_or_else(|| {
459            AdapterError::internal(
460                "plan alter source",
461                format!("expected Source found {entry:?}"),
462            )
463        })?;
464
465        let item_id = entry.id();
466        let ingestion_id = source.global_id();
467        let subsource_stmts = generate_subsource_statements(&scx, source_name, subsources)?;
468
469        let ids = self
470            .allocate_user_ids(u64::cast_from(subsource_stmts.len()))
471            .await?;
472        for (subsource_stmt, (item_id, global_id)) in subsource_stmts.into_iter().zip_eq(ids) {
473            let s = self.plan_subsource(session, &params, subsource_stmt, item_id, global_id)?;
474            subsource_plans.push(s);
475        }
476
477        let action = mz_sql::plan::AlterSourceAction::AddSubsourceExports {
478            subsources: subsource_plans,
479            options,
480        };
481
482        Ok((
483            Plan::AlterSource(mz_sql::plan::AlterSourcePlan {
484                item_id,
485                ingestion_id,
486                action,
487            }),
488            ResolvedIds::empty(),
489        ))
490    }
491
492    /// Prepares an `ALTER SOURCE...REFRESH REFERENCES`.
493    pub(crate) fn plan_purified_alter_source_refresh_references(
494        &self,
495        _session: &Session,
496        _params: Params,
497        source_name: ResolvedItemName,
498        available_source_references: plan::SourceReferences,
499    ) -> Result<(Plan, ResolvedIds), AdapterError> {
500        let entry = self.catalog().get_entry(source_name.item_id());
501        let source = entry.source().ok_or_else(|| {
502            AdapterError::internal(
503                "plan alter source",
504                format!("expected Source found {entry:?}"),
505            )
506        })?;
507        let action = mz_sql::plan::AlterSourceAction::RefreshReferences {
508            references: available_source_references,
509        };
510
511        Ok((
512            Plan::AlterSource(mz_sql::plan::AlterSourcePlan {
513                item_id: entry.id(),
514                ingestion_id: source.global_id(),
515                action,
516            }),
517            ResolvedIds::empty(),
518        ))
519    }
520
521    /// Prepares a `CREATE SOURCE` statement to create its progress subsource,
522    /// the primary source, and any ingestion export subsources (e.g. PG
523    /// tables).
524    pub(crate) async fn plan_purified_create_source(
525        &mut self,
526        ctx: &ExecuteContext,
527        params: Params,
528        progress_stmt: Option<CreateSubsourceStatement<Aug>>,
529        mut source_stmt: mz_sql::ast::CreateSourceStatement<Aug>,
530        subsources: BTreeMap<UnresolvedItemName, PurifiedSourceExport>,
531        available_source_references: plan::SourceReferences,
532    ) -> Result<(Plan, ResolvedIds), AdapterError> {
533        let mut create_source_plans = Vec::with_capacity(subsources.len() + 2);
534
535        // 1. First plan the progress subsource, if any.
536        if let Some(progress_stmt) = progress_stmt {
537            // The primary source depends on this subsource because the primary
538            // source needs to know its shard ID, and the easiest way of
539            // guaranteeing that the shard ID is discoverable is to create this
540            // collection first.
541            assert_none!(progress_stmt.of_source);
542            let (item_id, global_id) = self.allocate_user_id().await?;
543            let progress_plan =
544                self.plan_subsource(ctx.session(), &params, progress_stmt, item_id, global_id)?;
545            let progress_full_name = self
546                .catalog()
547                .resolve_full_name(&progress_plan.plan.name, None);
548            let progress_subsource = ResolvedItemName::Item {
549                id: progress_plan.item_id,
550                qualifiers: progress_plan.plan.name.qualifiers.clone(),
551                full_name: progress_full_name,
552                print_id: true,
553                version: RelationVersionSelector::Latest,
554            };
555
556            create_source_plans.push(progress_plan);
557
558            source_stmt.progress_subsource = Some(DeferredItemName::Named(progress_subsource));
559        }
560
561        let catalog = self.catalog().for_session(ctx.session());
562        let resolved_ids = mz_sql::names::visit_dependencies(&catalog, &source_stmt);
563
564        let propagated_with_options: Vec<_> = source_stmt
565            .with_options
566            .iter()
567            .filter_map(|opt| match opt.name {
568                CreateSourceOptionName::TimestampInterval => None,
569                CreateSourceOptionName::RetainHistory => Some(CreateSubsourceOption {
570                    name: CreateSubsourceOptionName::RetainHistory,
571                    value: opt.value.clone(),
572                }),
573            })
574            .collect();
575
576        // 2. Then plan the main source.
577        let source_plan = match self.plan_statement(
578            ctx.session(),
579            Statement::CreateSource(source_stmt),
580            &params,
581            &resolved_ids,
582        )? {
583            (Plan::CreateSource(plan), _sql_impl_ids) => plan,
584            (p, _) => unreachable!("s must be CreateSourcePlan but got {:?}", p),
585        };
586
587        let (item_id, global_id) = self.allocate_user_id().await?;
588
589        let source_full_name = self.catalog().resolve_full_name(&source_plan.name, None);
590        let of_source = ResolvedItemName::Item {
591            id: item_id,
592            qualifiers: source_plan.name.qualifiers.clone(),
593            full_name: source_full_name,
594            print_id: true,
595            version: RelationVersionSelector::Latest,
596        };
597
598        // Generate subsource statements
599        let conn_catalog = self.catalog().for_system_session();
600        let pcx = plan::PlanContext::zero();
601        let scx = StatementContext::new(Some(&pcx), &conn_catalog);
602
603        let mut subsource_stmts = generate_subsource_statements(&scx, of_source, subsources)?;
604
605        for subsource_stmt in subsource_stmts.iter_mut() {
606            subsource_stmt
607                .with_options
608                .extend(propagated_with_options.iter().cloned())
609        }
610
611        create_source_plans.push(CreateSourcePlanBundle {
612            item_id,
613            global_id,
614            plan: source_plan,
615            resolved_ids: resolved_ids.clone(),
616            available_source_references: Some(available_source_references),
617        });
618
619        // 3. Finally, plan all the subsources
620        let ids = self
621            .allocate_user_ids(u64::cast_from(subsource_stmts.len()))
622            .await?;
623        for (stmt, (item_id, global_id)) in subsource_stmts.into_iter().zip_eq(ids) {
624            let plan = self.plan_subsource(ctx.session(), &params, stmt, item_id, global_id)?;
625            create_source_plans.push(plan);
626        }
627
628        Ok((
629            Plan::CreateSources(create_source_plans),
630            ResolvedIds::empty(),
631        ))
632    }
633
634    #[instrument]
635    pub(super) async fn sequence_create_source(
636        &mut self,
637        ctx: &mut ExecuteContext,
638        plans: Vec<plan::CreateSourcePlanBundle>,
639    ) -> Result<ExecuteResponse, AdapterError> {
640        let CreateSourceInner {
641            ops,
642            sources,
643            if_not_exists_ids,
644        } = self.create_source_inner(ctx.session(), plans).await?;
645
646        let transact_result = self
647            .catalog_transact_with_ddl_transaction(ctx, ops, |_, _| Box::pin(async {}))
648            .await;
649
650        // Check if any sources are webhook sources and report them as created.
651        for (item_id, _name, source) in &sources {
652            if matches!(source.data_source, DataSourceDesc::Webhook { .. }) {
653                if let Some(url) = self.catalog().state().try_get_webhook_url(item_id) {
654                    ctx.session()
655                        .add_notice(AdapterNotice::WebhookSourceCreated { url });
656                }
657            }
658        }
659
660        match transact_result {
661            Ok(()) => {
662                // Warn about any new source that runs on only one replica but
663                // landed on a cluster that has more than one. The source's
664                // name is passed along explicitly: in a DDL transaction the
665                // creation is only staged at this point, so the source is not
666                // yet visible in the catalog.
667                for (_item_id, name, source) in &sources {
668                    let cluster_id = match &source.data_source {
669                        DataSourceDesc::Ingestion { desc, cluster_id }
670                        | DataSourceDesc::OldSyntaxIngestion {
671                            desc, cluster_id, ..
672                        } if desc.connection.prefers_single_replica() => cluster_id,
673                        _ => continue,
674                    };
675                    let cluster = self.catalog().get_cluster(*cluster_id);
676                    self.notify_single_replica_sources(ctx.session(), cluster, Some(name));
677                }
678                Ok(ExecuteResponse::CreatedSource)
679            }
680            Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
681                kind: ErrorKind::Sql(CatalogError::ItemAlreadyExists(id, _)),
682            })) if if_not_exists_ids.contains_key(&id) => {
683                ctx.session()
684                    .add_notice(AdapterNotice::ObjectAlreadyExists {
685                        name: if_not_exists_ids[&id].item.clone(),
686                        ty: "source",
687                    });
688                Ok(ExecuteResponse::CreatedSource)
689            }
690            Err(err) => Err(err),
691        }
692    }
693
694    /// Applies `details.secret_content_guards()` by reading each guarded
695    /// secret's current contents. Must be called whenever a connection is
696    /// created or its options altered, before the change takes effect.
697    async fn check_connection_secret_content_guards(
698        &self,
699        details: &ConnectionDetails,
700    ) -> Result<(), AdapterError> {
701        for (secret_id, guard) in details.secret_content_guards() {
702            let contents = self.caching_secrets_reader.read_string(secret_id).await?;
703            guard(&contents)?;
704        }
705        Ok(())
706    }
707
708    /// Applies the secret-content guards of every connection that uses
709    /// `secret_id` against proposed new `contents` for that secret. Must be
710    /// called whenever a secret's contents change, before the new value is
711    /// persisted.
712    pub(super) fn check_secret_content_guards_of_dependents(
713        &self,
714        secret_id: CatalogItemId,
715        contents: &str,
716    ) -> Result<(), AdapterError> {
717        for dependent_id in self.catalog().get_entry(&secret_id).used_by() {
718            if let CatalogItem::Connection(conn) = self.catalog().get_entry(dependent_id).item() {
719                for (guarded_id, guard) in conn.details.secret_content_guards() {
720                    if guarded_id == secret_id {
721                        guard(contents)?;
722                    }
723                }
724            }
725        }
726        Ok(())
727    }
728
729    #[instrument]
730    pub(super) async fn sequence_create_connection(
731        &mut self,
732        mut ctx: ExecuteContext,
733        plan: plan::CreateConnectionPlan,
734        resolved_ids: ResolvedIds,
735    ) {
736        if let Err(err) = check_connection_details(&plan.connection.details) {
737            return ctx.retire(Err(AdapterError::PlanError(err)));
738        }
739
740        let (connection_id, connection_gid) = match self.allocate_user_id().await {
741            Ok(item_id) => item_id,
742            Err(err) => return ctx.retire(Err(err)),
743        };
744
745        match &plan.connection.details {
746            ConnectionDetails::Ssh { key_1, key_2, .. } => {
747                let key_1 = match key_1.as_key_pair() {
748                    Some(key_1) => key_1.clone(),
749                    None => {
750                        return ctx.retire(Err(AdapterError::Unstructured(anyhow!(
751                            "the PUBLIC KEY 1 option cannot be explicitly specified"
752                        ))));
753                    }
754                };
755
756                let key_2 = match key_2.as_key_pair() {
757                    Some(key_2) => key_2.clone(),
758                    None => {
759                        return ctx.retire(Err(AdapterError::Unstructured(anyhow!(
760                            "the PUBLIC KEY 2 option cannot be explicitly specified"
761                        ))));
762                    }
763                };
764
765                let key_set = SshKeyPairSet::from_parts(key_1, key_2);
766                let secret = key_set.to_bytes();
767                if let Err(err) = self.secrets_controller.ensure(connection_id, &secret).await {
768                    return ctx.retire(Err(err.into()));
769                }
770                self.caching_secrets_reader.invalidate(connection_id);
771            }
772            _ => (),
773        };
774
775        // Inspect guarded secrets as early as we can, before the connection is
776        // installed in the catalog.
777        if let Err(err) = self
778            .check_connection_secret_content_guards(&plan.connection.details)
779            .await
780        {
781            return ctx.retire(Err(err));
782        }
783
784        if plan.validate {
785            let internal_cmd_tx = self.internal_cmd_tx.clone();
786            let catalog = self.owned_catalog();
787            let conn_id = ctx.session().conn_id().clone();
788            let otel_ctx = OpenTelemetryContext::obtain();
789            let role_metadata = ctx.session().role_metadata().clone();
790
791            let connection = plan
792                .connection
793                .details
794                .to_connection()
795                .into_inline_connection(self.catalog().state());
796
797            let current_storage_parameters = self.controller.storage.config().clone();
798            task::spawn(|| format!("validate_connection:{conn_id}"), async move {
799                let result = match std::panic::AssertUnwindSafe(
800                    connection.validate(connection_id, &current_storage_parameters),
801                )
802                .ore_catch_unwind()
803                .await
804                {
805                    Ok(Ok(())) => Ok(plan),
806                    Ok(Err(err)) => Err(err.into()),
807                    Err(_panic) => {
808                        tracing::error!("connection validation panicked");
809                        Err(AdapterError::Internal(
810                            "connection validation panicked".into(),
811                        ))
812                    }
813                };
814
815                // It is not an error for validation to complete after `internal_cmd_rx` is dropped.
816                let result = internal_cmd_tx.send(Message::CreateConnectionValidationReady(
817                    CreateConnectionValidationReady {
818                        ctx,
819                        result,
820                        connection_id,
821                        connection_gid,
822                        plan_validity: PlanValidity::new(
823                            &catalog,
824                            resolved_ids.items().copied().collect(),
825                            None,
826                            None,
827                            role_metadata,
828                        ),
829                        otel_ctx,
830                        resolved_ids: resolved_ids.clone(),
831                    },
832                ));
833                if let Err(e) = result {
834                    tracing::warn!("internal_cmd_rx dropped before we could send: {:?}", e);
835                }
836            });
837        } else {
838            let result = self
839                .sequence_create_connection_stage_finish(
840                    &mut ctx,
841                    connection_id,
842                    connection_gid,
843                    plan,
844                    resolved_ids,
845                )
846                .await;
847            ctx.retire(result);
848        }
849    }
850
851    #[instrument]
852    pub(crate) async fn sequence_create_connection_stage_finish(
853        &mut self,
854        ctx: &mut ExecuteContext,
855        connection_id: CatalogItemId,
856        connection_gid: GlobalId,
857        plan: plan::CreateConnectionPlan,
858        resolved_ids: ResolvedIds,
859    ) -> Result<ExecuteResponse, AdapterError> {
860        let ops = vec![catalog::Op::CreateItem {
861            id: connection_id,
862            name: plan.name.clone(),
863            item: CatalogItem::Connection(Connection {
864                create_sql: plan.connection.create_sql,
865                global_id: connection_gid,
866                details: plan.connection.details.clone(),
867                resolved_ids,
868            }),
869            owner_id: *ctx.session().current_role_id(),
870        }];
871
872        // VPC endpoint creation for AWS PrivateLink connections is now handled
873        // in apply_catalog_implications.
874        let conn_id = ctx.session().conn_id().clone();
875        let transact_result = self
876            .catalog_transact_with_context(Some(&conn_id), Some(ctx), ops)
877            .await;
878
879        match transact_result {
880            Ok(_) => Ok(ExecuteResponse::CreatedConnection),
881            Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
882                kind: ErrorKind::Sql(CatalogError::ItemAlreadyExists(_, _)),
883            })) if plan.if_not_exists => {
884                // Clean up SSH key material if it was persisted, since the
885                // catalog item was not created.
886                if matches!(plan.connection.details, ConnectionDetails::Ssh { .. }) {
887                    if let Err(e) = self.secrets_controller.delete(connection_id).await {
888                        tracing::warn!(
889                            "Dropping SSH secret for existing connection has encountered an error: {}",
890                            e
891                        );
892                    } else {
893                        self.caching_secrets_reader.invalidate(connection_id);
894                    }
895                }
896                ctx.session()
897                    .add_notice(AdapterNotice::ObjectAlreadyExists {
898                        name: plan.name.item,
899                        ty: "connection",
900                    });
901                Ok(ExecuteResponse::CreatedConnection)
902            }
903            Err(err) => {
904                // Clean up SSH key material if it was persisted, since the
905                // catalog item was not created.
906                if matches!(plan.connection.details, ConnectionDetails::Ssh { .. }) {
907                    if let Err(e) = self.secrets_controller.delete(connection_id).await {
908                        tracing::warn!(
909                            "Dropping SSH secret for failed connection has encountered an error: {}",
910                            e
911                        );
912                    } else {
913                        self.caching_secrets_reader.invalidate(connection_id);
914                    }
915                }
916                Err(err)
917            }
918        }
919    }
920
921    #[instrument]
922    pub(super) async fn sequence_create_database(
923        &mut self,
924        session: &Session,
925        plan: plan::CreateDatabasePlan,
926    ) -> Result<ExecuteResponse, AdapterError> {
927        let ops = vec![catalog::Op::CreateDatabase {
928            name: plan.name.clone(),
929            owner_id: *session.current_role_id(),
930        }];
931        match self.catalog_transact(Some(session), ops).await {
932            Ok(_) => Ok(ExecuteResponse::CreatedDatabase),
933            Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
934                kind: ErrorKind::Sql(CatalogError::DatabaseAlreadyExists(_)),
935            })) if plan.if_not_exists => {
936                session.add_notice(AdapterNotice::DatabaseAlreadyExists { name: plan.name });
937                Ok(ExecuteResponse::CreatedDatabase)
938            }
939            Err(err) => Err(err),
940        }
941    }
942
943    #[instrument]
944    pub(super) async fn sequence_create_schema(
945        &mut self,
946        session: &Session,
947        plan: plan::CreateSchemaPlan,
948    ) -> Result<ExecuteResponse, AdapterError> {
949        let op = catalog::Op::CreateSchema {
950            database_id: plan.database_spec,
951            schema_name: plan.schema_name.clone(),
952            owner_id: *session.current_role_id(),
953        };
954        match self.catalog_transact(Some(session), vec![op]).await {
955            Ok(_) => Ok(ExecuteResponse::CreatedSchema),
956            Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
957                kind: ErrorKind::Sql(CatalogError::SchemaAlreadyExists(_)),
958            })) if plan.if_not_exists => {
959                session.add_notice(AdapterNotice::SchemaAlreadyExists {
960                    name: plan.schema_name,
961                });
962                Ok(ExecuteResponse::CreatedSchema)
963            }
964            Err(err) => Err(err),
965        }
966    }
967
968    /// Validates the role attributes for a `CREATE ROLE` statement.
969    fn validate_role_attributes(&self, attributes: &RoleAttributesRaw) -> Result<(), AdapterError> {
970        if !ENABLE_PASSWORD_AUTH.get(self.catalog().system_config().dyncfgs()) {
971            if attributes.superuser.is_some() || attributes.password.is_some() {
972                return Err(AdapterError::UnavailableFeature {
973                    feature: "SUPERUSER and PASSWORD attributes".to_string(),
974                    docs: Some("https://materialize.com/docs/sql/create-role/#details".to_string()),
975                });
976            }
977        }
978        Ok(())
979    }
980
981    #[instrument]
982    pub(super) async fn sequence_create_role(
983        &mut self,
984        conn_id: Option<&ConnectionId>,
985        plan::CreateRolePlan { name, attributes }: plan::CreateRolePlan,
986    ) -> Result<ExecuteResponse, AdapterError> {
987        self.validate_role_attributes(&attributes.clone())?;
988        let op = catalog::Op::CreateRole { name, attributes };
989        self.catalog_transact_with_context(conn_id, None, vec![op])
990            .await
991            .map(|_| ExecuteResponse::CreatedRole)
992    }
993
994    #[instrument]
995    pub(super) async fn sequence_create_network_policy(
996        &mut self,
997        session: &Session,
998        plan::CreateNetworkPolicyPlan { name, rules }: plan::CreateNetworkPolicyPlan,
999    ) -> Result<ExecuteResponse, AdapterError> {
1000        let op = catalog::Op::CreateNetworkPolicy {
1001            rules,
1002            name,
1003            owner_id: *session.current_role_id(),
1004        };
1005        self.catalog_transact_with_context(Some(session.conn_id()), None, vec![op])
1006            .await
1007            .map(|_| ExecuteResponse::CreatedNetworkPolicy)
1008    }
1009
1010    #[instrument]
1011    pub(super) async fn sequence_alter_network_policy(
1012        &mut self,
1013        session: &Session,
1014        plan::AlterNetworkPolicyPlan { id, name, rules }: plan::AlterNetworkPolicyPlan,
1015    ) -> Result<ExecuteResponse, AdapterError> {
1016        // TODO(network_policy): Consider role based network policies here.
1017        let current_network_policy_name =
1018            self.catalog().system_config().default_network_policy_name();
1019        // Check if the way we're alerting the policy is still valid for the current connection.
1020        if current_network_policy_name == name {
1021            self.validate_alter_network_policy(session, &rules)?;
1022        }
1023
1024        let op = catalog::Op::AlterNetworkPolicy {
1025            id,
1026            rules,
1027            name,
1028            owner_id: *session.current_role_id(),
1029        };
1030        self.catalog_transact_with_context(Some(session.conn_id()), None, vec![op])
1031            .await
1032            .map(|_| ExecuteResponse::AlteredObject(ObjectType::NetworkPolicy))
1033    }
1034
1035    #[instrument]
1036    pub(super) async fn sequence_create_table(
1037        &mut self,
1038        ctx: &mut ExecuteContext,
1039        plan: plan::CreateTablePlan,
1040        resolved_ids: ResolvedIds,
1041    ) -> Result<ExecuteResponse, AdapterError> {
1042        let plan::CreateTablePlan {
1043            name,
1044            table,
1045            if_not_exists,
1046        } = plan;
1047
1048        let conn_id = if table.temporary {
1049            Some(ctx.session().conn_id())
1050        } else {
1051            None
1052        };
1053        let (table_id, global_id) = self.allocate_user_id().await?;
1054        let collections = [(RelationVersion::root(), global_id)].into_iter().collect();
1055
1056        let data_source = match table.data_source {
1057            plan::TableDataSource::TableWrites { defaults } => {
1058                TableDataSource::TableWrites { defaults }
1059            }
1060            plan::TableDataSource::DataSource {
1061                desc: data_source_plan,
1062                timeline,
1063            } => match data_source_plan {
1064                plan::DataSourceDesc::IngestionExport {
1065                    ingestion_id,
1066                    external_reference,
1067                    details,
1068                    data_config,
1069                } => TableDataSource::DataSource {
1070                    desc: DataSourceDesc::IngestionExport {
1071                        ingestion_id,
1072                        external_reference,
1073                        details,
1074                        data_config,
1075                    },
1076                    timeline,
1077                },
1078                plan::DataSourceDesc::Webhook {
1079                    validate_using,
1080                    body_format,
1081                    headers,
1082                    cluster_id,
1083                } => TableDataSource::DataSource {
1084                    desc: DataSourceDesc::Webhook {
1085                        validate_using,
1086                        body_format,
1087                        headers,
1088                        cluster_id: cluster_id.expect("Webhook Tables must have cluster_id set"),
1089                    },
1090                    timeline,
1091                },
1092                o => {
1093                    unreachable!("CREATE TABLE data source got {:?}", o)
1094                }
1095            },
1096        };
1097
1098        let is_webhook = if let TableDataSource::DataSource {
1099            desc: DataSourceDesc::Webhook { .. },
1100            timeline: _,
1101        } = &data_source
1102        {
1103            true
1104        } else {
1105            false
1106        };
1107
1108        let table = Table {
1109            create_sql: Some(table.create_sql),
1110            desc: table.desc,
1111            collections,
1112            conn_id: conn_id.cloned(),
1113            resolved_ids,
1114            custom_logical_compaction_window: table.compaction_window,
1115            is_retained_metrics_object: false,
1116            data_source,
1117        };
1118        let ops = vec![catalog::Op::CreateItem {
1119            id: table_id,
1120            name: name.clone(),
1121            item: CatalogItem::Table(table.clone()),
1122            owner_id: *ctx.session().current_role_id(),
1123        }];
1124
1125        let catalog_result = self
1126            .catalog_transact_with_ddl_transaction(ctx, ops, |_, _| Box::pin(async {}))
1127            .await;
1128
1129        if is_webhook {
1130            // try_get_webhook_url will make up a URL for things that are not
1131            // webhooks, so we guard against that here.
1132            if let Some(url) = self.catalog().state().try_get_webhook_url(&table_id) {
1133                ctx.session()
1134                    .add_notice(AdapterNotice::WebhookSourceCreated { url })
1135            }
1136        }
1137
1138        match catalog_result {
1139            Ok(()) => Ok(ExecuteResponse::CreatedTable),
1140            Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
1141                kind: ErrorKind::Sql(CatalogError::ItemAlreadyExists(_, _)),
1142            })) if if_not_exists => {
1143                ctx.session_mut()
1144                    .add_notice(AdapterNotice::ObjectAlreadyExists {
1145                        name: name.item,
1146                        ty: "table",
1147                    });
1148                Ok(ExecuteResponse::CreatedTable)
1149            }
1150            Err(err) => Err(err),
1151        }
1152    }
1153
1154    #[instrument]
1155    pub(super) async fn sequence_create_sink(
1156        &mut self,
1157        ctx: ExecuteContext,
1158        plan: plan::CreateSinkPlan,
1159        resolved_ids: ResolvedIds,
1160    ) {
1161        let plan::CreateSinkPlan {
1162            name,
1163            sink,
1164            with_snapshot,
1165            if_not_exists,
1166            in_cluster,
1167        } = plan;
1168
1169        // First try to allocate an ID and an OID. If either fails, we're done.
1170        let (item_id, global_id) = return_if_err!(self.allocate_user_id().await, ctx);
1171
1172        let catalog_sink = Sink {
1173            create_sql: sink.create_sql,
1174            global_id,
1175            from: sink.from,
1176            connection: sink.connection,
1177            envelope: sink.envelope,
1178            version: sink.version,
1179            with_snapshot,
1180            resolved_ids,
1181            cluster_id: in_cluster,
1182            commit_interval: sink.commit_interval,
1183        };
1184
1185        let ops = vec![catalog::Op::CreateItem {
1186            id: item_id,
1187            name: name.clone(),
1188            item: CatalogItem::Sink(catalog_sink.clone()),
1189            owner_id: *ctx.session().current_role_id(),
1190        }];
1191
1192        let result = self.catalog_transact(Some(ctx.session()), ops).await;
1193
1194        match result {
1195            Ok(()) => {}
1196            Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
1197                kind: ErrorKind::Sql(CatalogError::ItemAlreadyExists(_, _)),
1198            })) if if_not_exists => {
1199                ctx.session()
1200                    .add_notice(AdapterNotice::ObjectAlreadyExists {
1201                        name: name.item,
1202                        ty: "sink",
1203                    });
1204                ctx.retire(Ok(ExecuteResponse::CreatedSink));
1205                return;
1206            }
1207            Err(e) => {
1208                ctx.retire(Err(e));
1209                return;
1210            }
1211        };
1212
1213        self.create_storage_export(global_id, &catalog_sink)
1214            .await
1215            .unwrap_or_terminate("cannot fail to create exports");
1216
1217        self.initialize_storage_read_policies([item_id].into(), CompactionWindow::Default)
1218            .await;
1219
1220        ctx.retire(Ok(ExecuteResponse::CreatedSink))
1221    }
1222
1223    /// Validates that a view definition does not contain any expressions that may lead to
1224    /// ambiguous column references to system tables. For example `NATURAL JOIN` or `SELECT *`.
1225    ///
1226    /// We prevent these expressions so that we can add columns to system tables without
1227    /// changing the definition of the view.
1228    ///
1229    /// Here is a bit of a hand wavy proof as to why we only need to check the
1230    /// immediate view definition for system objects and ambiguous column
1231    /// references, and not the entire dependency tree:
1232    ///
1233    ///   - A view with no object references cannot have any ambiguous column
1234    ///   references to a system object, because it has no system objects.
1235    ///   - A view with a direct reference to a system object and a * or
1236    ///   NATURAL JOIN will be rejected due to ambiguous column references.
1237    ///   - A view with system objects but no * or NATURAL JOINs cannot have
1238    ///   any ambiguous column references to a system object, because all column
1239    ///   references are explicitly named.
1240    ///   - A view with * or NATURAL JOINs, that doesn't directly reference a
1241    ///   system object cannot have any ambiguous column references to a system
1242    ///   object, because there are no system objects in the top level view and
1243    ///   all sub-views are guaranteed to have no ambiguous column references to
1244    ///   system objects.
1245    pub(super) fn validate_system_column_references(
1246        &self,
1247        uses_ambiguous_columns: bool,
1248        depends_on: &BTreeSet<GlobalId>,
1249    ) -> Result<(), AdapterError> {
1250        if uses_ambiguous_columns
1251            && depends_on
1252                .iter()
1253                .any(|id| id.is_system() && self.catalog().get_entry_by_global_id(id).is_relation())
1254        {
1255            Err(AdapterError::AmbiguousSystemColumnReference)
1256        } else {
1257            Ok(())
1258        }
1259    }
1260
1261    #[instrument]
1262    pub(super) async fn sequence_create_type(
1263        &mut self,
1264        session: &Session,
1265        plan: plan::CreateTypePlan,
1266        resolved_ids: ResolvedIds,
1267    ) -> Result<ExecuteResponse, AdapterError> {
1268        let (item_id, global_id) = self.allocate_user_id().await?;
1269        // Validate the type definition (e.g., composite columns) before storing.
1270        plan.typ
1271            .inner
1272            .desc(&self.catalog().for_session(session))
1273            .map_err(AdapterError::from)?;
1274        let typ = Type {
1275            create_sql: Some(plan.typ.create_sql),
1276            global_id,
1277            details: CatalogTypeDetails {
1278                array_id: None,
1279                typ: plan.typ.inner,
1280                pg_metadata: None,
1281            },
1282            resolved_ids,
1283        };
1284        let op = catalog::Op::CreateItem {
1285            id: item_id,
1286            name: plan.name,
1287            item: CatalogItem::Type(typ),
1288            owner_id: *session.current_role_id(),
1289        };
1290        match self.catalog_transact(Some(session), vec![op]).await {
1291            Ok(()) => Ok(ExecuteResponse::CreatedType),
1292            Err(err) => Err(err),
1293        }
1294    }
1295
1296    #[instrument]
1297    pub(super) async fn sequence_comment_on(
1298        &mut self,
1299        session: &Session,
1300        plan: plan::CommentPlan,
1301    ) -> Result<ExecuteResponse, AdapterError> {
1302        let op = catalog::Op::Comment {
1303            object_id: plan.object_id,
1304            sub_component: plan.sub_component,
1305            comment: plan.comment,
1306        };
1307        self.catalog_transact(Some(session), vec![op]).await?;
1308        Ok(ExecuteResponse::Comment)
1309    }
1310
1311    #[instrument]
1312    pub(super) async fn sequence_drop_objects(
1313        &mut self,
1314        ctx: &mut ExecuteContext,
1315        plan::DropObjectsPlan {
1316            drop_ids,
1317            object_type,
1318            referenced_ids,
1319        }: plan::DropObjectsPlan,
1320    ) -> Result<ExecuteResponse, AdapterError> {
1321        let referenced_ids_hashset = referenced_ids.iter().collect::<HashSet<_>>();
1322        let mut objects = Vec::new();
1323        for obj_id in &drop_ids {
1324            if !referenced_ids_hashset.contains(obj_id) {
1325                let object_info = ErrorMessageObjectDescription::from_id(
1326                    obj_id,
1327                    &self.catalog().for_session(ctx.session()),
1328                )
1329                .to_string();
1330                objects.push(object_info);
1331            }
1332        }
1333
1334        if !objects.is_empty() {
1335            ctx.session()
1336                .add_notice(AdapterNotice::CascadeDroppedObject { objects });
1337        }
1338
1339        // Collect GlobalIds for expression cache invalidation.
1340        let expr_cache_invalidate_ids: BTreeSet<_> = drop_ids
1341            .iter()
1342            .filter_map(|id| match id {
1343                ObjectId::Item(item_id) => Some(self.catalog().get_entry(item_id).global_ids()),
1344                _ => None,
1345            })
1346            .flatten()
1347            .collect();
1348
1349        let DropOps {
1350            ops,
1351            dropped_active_db,
1352            dropped_active_cluster,
1353            dropped_in_use_indexes,
1354        } = self.sequence_drop_common(ctx.session(), drop_ids)?;
1355
1356        self.catalog_transact_with_context(None, Some(ctx), ops)
1357            .await?;
1358
1359        // Invalidate expression cache entries for dropped objects.
1360        if !expr_cache_invalidate_ids.is_empty() {
1361            let _fut = self.catalog().update_expression_cache(
1362                Default::default(),
1363                Default::default(),
1364                expr_cache_invalidate_ids,
1365            );
1366        }
1367
1368        fail::fail_point!("after_sequencer_drop_replica");
1369
1370        if dropped_active_db {
1371            ctx.session()
1372                .add_notice(AdapterNotice::DroppedActiveDatabase {
1373                    name: ctx.session().vars().database().to_string(),
1374                });
1375        }
1376        if dropped_active_cluster {
1377            ctx.session()
1378                .add_notice(AdapterNotice::DroppedActiveCluster {
1379                    name: ctx.session().vars().cluster().to_string(),
1380                });
1381        }
1382        for dropped_in_use_index in dropped_in_use_indexes {
1383            ctx.session()
1384                .add_notice(AdapterNotice::DroppedInUseIndex(dropped_in_use_index));
1385            self.metrics
1386                .optimization_notices
1387                .with_label_values(&["DroppedInUseIndex"])
1388                .inc_by(1);
1389        }
1390        Ok(ExecuteResponse::DroppedObject(object_type))
1391    }
1392
1393    fn validate_dropped_role_ownership(
1394        &self,
1395        session: &Session,
1396        dropped_roles: &BTreeMap<RoleId, &str>,
1397    ) -> Result<(), AdapterError> {
1398        fn privilege_check(
1399            privileges: &PrivilegeMap,
1400            dropped_roles: &BTreeMap<RoleId, &str>,
1401            dependent_objects: &mut BTreeMap<String, Vec<String>>,
1402            object_id: &SystemObjectId,
1403            catalog: &ConnCatalog,
1404        ) {
1405            for privilege in privileges.all_values() {
1406                if let Some(role_name) = dropped_roles.get(&privilege.grantee) {
1407                    let grantor_name = catalog.get_role(&privilege.grantor).name();
1408                    let object_description =
1409                        ErrorMessageObjectDescription::from_sys_id(object_id, catalog);
1410                    dependent_objects
1411                        .entry(role_name.to_string())
1412                        .or_default()
1413                        .push(format!(
1414                            "privileges on {object_description} granted by {grantor_name}",
1415                        ));
1416                }
1417                if let Some(role_name) = dropped_roles.get(&privilege.grantor) {
1418                    let grantee_name = catalog.get_role(&privilege.grantee).name();
1419                    let object_description =
1420                        ErrorMessageObjectDescription::from_sys_id(object_id, catalog);
1421                    dependent_objects
1422                        .entry(role_name.to_string())
1423                        .or_default()
1424                        .push(format!(
1425                            "privileges granted on {object_description} to {grantee_name}"
1426                        ));
1427                }
1428            }
1429        }
1430
1431        let catalog = self.catalog().for_session(session);
1432        let mut dependent_objects: BTreeMap<_, Vec<_>> = BTreeMap::new();
1433        for entry in self.catalog.entries() {
1434            let id = SystemObjectId::Object(entry.id().into());
1435            if let Some(role_name) = dropped_roles.get(entry.owner_id()) {
1436                let object_description = ErrorMessageObjectDescription::from_sys_id(&id, &catalog);
1437                dependent_objects
1438                    .entry(role_name.to_string())
1439                    .or_default()
1440                    .push(format!("owner of {object_description}"));
1441            }
1442            privilege_check(
1443                entry.privileges(),
1444                dropped_roles,
1445                &mut dependent_objects,
1446                &id,
1447                &catalog,
1448            );
1449        }
1450        for database in self.catalog.databases() {
1451            let database_id = SystemObjectId::Object(database.id().into());
1452            if let Some(role_name) = dropped_roles.get(&database.owner_id) {
1453                let object_description =
1454                    ErrorMessageObjectDescription::from_sys_id(&database_id, &catalog);
1455                dependent_objects
1456                    .entry(role_name.to_string())
1457                    .or_default()
1458                    .push(format!("owner of {object_description}"));
1459            }
1460            privilege_check(
1461                &database.privileges,
1462                dropped_roles,
1463                &mut dependent_objects,
1464                &database_id,
1465                &catalog,
1466            );
1467            for schema in database.schemas_by_id.values() {
1468                let schema_id = SystemObjectId::Object(
1469                    (ResolvedDatabaseSpecifier::Id(database.id()), *schema.id()).into(),
1470                );
1471                if let Some(role_name) = dropped_roles.get(&schema.owner_id) {
1472                    let object_description =
1473                        ErrorMessageObjectDescription::from_sys_id(&schema_id, &catalog);
1474                    dependent_objects
1475                        .entry(role_name.to_string())
1476                        .or_default()
1477                        .push(format!("owner of {object_description}"));
1478                }
1479                privilege_check(
1480                    &schema.privileges,
1481                    dropped_roles,
1482                    &mut dependent_objects,
1483                    &schema_id,
1484                    &catalog,
1485                );
1486            }
1487        }
1488        for cluster in self.catalog.clusters() {
1489            let cluster_id = SystemObjectId::Object(cluster.id().into());
1490            if let Some(role_name) = dropped_roles.get(&cluster.owner_id) {
1491                let object_description =
1492                    ErrorMessageObjectDescription::from_sys_id(&cluster_id, &catalog);
1493                dependent_objects
1494                    .entry(role_name.to_string())
1495                    .or_default()
1496                    .push(format!("owner of {object_description}"));
1497            }
1498            privilege_check(
1499                &cluster.privileges,
1500                dropped_roles,
1501                &mut dependent_objects,
1502                &cluster_id,
1503                &catalog,
1504            );
1505            for replica in cluster.replicas() {
1506                if let Some(role_name) = dropped_roles.get(&replica.owner_id) {
1507                    let replica_id =
1508                        SystemObjectId::Object((replica.cluster_id(), replica.replica_id()).into());
1509                    let object_description =
1510                        ErrorMessageObjectDescription::from_sys_id(&replica_id, &catalog);
1511                    dependent_objects
1512                        .entry(role_name.to_string())
1513                        .or_default()
1514                        .push(format!("owner of {object_description}"));
1515                }
1516            }
1517        }
1518        privilege_check(
1519            self.catalog().system_privileges(),
1520            dropped_roles,
1521            &mut dependent_objects,
1522            &SystemObjectId::System,
1523            &catalog,
1524        );
1525        for (default_privilege_object, default_privilege_acl_items) in
1526            self.catalog.default_privileges()
1527        {
1528            if let Some(role_name) = dropped_roles.get(&default_privilege_object.role_id) {
1529                dependent_objects
1530                    .entry(role_name.to_string())
1531                    .or_default()
1532                    .push(format!(
1533                        "default privileges on {}S created by {}",
1534                        default_privilege_object.object_type, role_name
1535                    ));
1536            }
1537            for default_privilege_acl_item in default_privilege_acl_items {
1538                if let Some(role_name) = dropped_roles.get(&default_privilege_acl_item.grantee) {
1539                    dependent_objects
1540                        .entry(role_name.to_string())
1541                        .or_default()
1542                        .push(format!(
1543                            "default privileges on {}S granted to {}",
1544                            default_privilege_object.object_type, role_name
1545                        ));
1546                }
1547            }
1548        }
1549
1550        if !dependent_objects.is_empty() {
1551            Err(AdapterError::DependentObject(dependent_objects))
1552        } else {
1553            Ok(())
1554        }
1555    }
1556
1557    #[instrument]
1558    pub(super) async fn sequence_drop_owned(
1559        &mut self,
1560        session: &Session,
1561        plan: plan::DropOwnedPlan,
1562    ) -> Result<ExecuteResponse, AdapterError> {
1563        for role_id in &plan.role_ids {
1564            self.catalog().ensure_not_reserved_role(role_id)?;
1565        }
1566
1567        let mut privilege_revokes = plan.privilege_revokes;
1568
1569        // Make sure this stays in sync with the beginning of `rbac::check_plan`.
1570        let session_catalog = self.catalog().for_session(session);
1571        if rbac::is_rbac_enabled_for_session(session_catalog.system_vars(), session)
1572            && !session.is_superuser()
1573        {
1574            // Obtain all roles that the current session is a member of.
1575            let role_membership =
1576                session_catalog.collect_role_membership(session.current_role_id());
1577            let invalid_revokes: BTreeSet<_> = privilege_revokes
1578                .extract_if(.., |(_, privilege)| {
1579                    !role_membership.contains(&privilege.grantor)
1580                })
1581                .map(|(object_id, _)| object_id)
1582                .collect();
1583            for invalid_revoke in invalid_revokes {
1584                let object_description =
1585                    ErrorMessageObjectDescription::from_sys_id(&invalid_revoke, &session_catalog);
1586                session.add_notice(AdapterNotice::CannotRevoke { object_description });
1587            }
1588        }
1589
1590        // Group revokes by target so each object is rewritten once, not once per privilege.
1591        let mut privilege_revokes_by_target: BTreeMap<SystemObjectId, Vec<MzAclItem>> =
1592            BTreeMap::new();
1593        for (object_id, privilege) in privilege_revokes {
1594            privilege_revokes_by_target
1595                .entry(object_id)
1596                .or_default()
1597                .push(privilege);
1598        }
1599        let privilege_revoke_ops =
1600            privilege_revokes_by_target
1601                .into_iter()
1602                .map(|(target_id, privileges)| catalog::Op::UpdatePrivilege {
1603                    target_id,
1604                    privileges,
1605                    variant: UpdatePrivilegeVariant::Revoke,
1606                });
1607        let default_privilege_revoke_ops = plan.default_privilege_revokes.into_iter().map(
1608            |(privilege_object, privilege_acl_item)| catalog::Op::UpdateDefaultPrivilege {
1609                privilege_object,
1610                privilege_acl_item,
1611                variant: UpdatePrivilegeVariant::Revoke,
1612            },
1613        );
1614        let DropOps {
1615            ops: drop_ops,
1616            dropped_active_db,
1617            dropped_active_cluster,
1618            dropped_in_use_indexes,
1619        } = self.sequence_drop_common(session, plan.drop_ids)?;
1620
1621        let ops = privilege_revoke_ops
1622            .chain(default_privilege_revoke_ops)
1623            .chain(drop_ops.into_iter())
1624            .collect();
1625
1626        self.catalog_transact(Some(session), ops).await?;
1627
1628        if dropped_active_db {
1629            session.add_notice(AdapterNotice::DroppedActiveDatabase {
1630                name: session.vars().database().to_string(),
1631            });
1632        }
1633        if dropped_active_cluster {
1634            session.add_notice(AdapterNotice::DroppedActiveCluster {
1635                name: session.vars().cluster().to_string(),
1636            });
1637        }
1638        for dropped_in_use_index in dropped_in_use_indexes {
1639            session.add_notice(AdapterNotice::DroppedInUseIndex(dropped_in_use_index));
1640        }
1641        Ok(ExecuteResponse::DroppedOwned)
1642    }
1643
1644    fn sequence_drop_common(
1645        &self,
1646        session: &Session,
1647        ids: Vec<ObjectId>,
1648    ) -> Result<DropOps, AdapterError> {
1649        let mut dropped_active_db = false;
1650        let mut dropped_active_cluster = false;
1651        let mut dropped_in_use_indexes = Vec::new();
1652        let mut dropped_roles = BTreeMap::new();
1653        let mut dropped_databases = BTreeSet::new();
1654        let mut dropped_schemas = BTreeSet::new();
1655        // Dropping either the group role or the member role of a role membership will trigger a
1656        // revoke role. We use a Set for the revokes to avoid trying to attempt to revoke the same
1657        // role membership twice.
1658        let mut role_revokes = BTreeSet::new();
1659        // Dropping a database or a schema will revoke all default roles associated with that
1660        // database or schema.
1661        let mut default_privilege_revokes = BTreeSet::new();
1662
1663        // Clusters we're dropping
1664        let mut clusters_to_drop = BTreeSet::new();
1665
1666        let ids_set = ids.iter().collect::<BTreeSet<_>>();
1667        for id in &ids {
1668            match id {
1669                ObjectId::Database(id) => {
1670                    let name = self.catalog().get_database(id).name();
1671                    if name == session.vars().database() {
1672                        dropped_active_db = true;
1673                    }
1674                    dropped_databases.insert(id);
1675                }
1676                ObjectId::Schema((_, spec)) => {
1677                    if let SchemaSpecifier::Id(id) = spec {
1678                        dropped_schemas.insert(id);
1679                    }
1680                }
1681                ObjectId::Cluster(id) => {
1682                    clusters_to_drop.insert(*id);
1683                    if let Some(active_id) = self
1684                        .catalog()
1685                        .active_cluster(session)
1686                        .ok()
1687                        .map(|cluster| cluster.id())
1688                    {
1689                        if id == &active_id {
1690                            dropped_active_cluster = true;
1691                        }
1692                    }
1693                }
1694                ObjectId::Role(id) => {
1695                    let role = self.catalog().get_role(id);
1696                    let name = role.name();
1697                    dropped_roles.insert(*id, name);
1698                    // We must revoke all role memberships that the dropped roles belongs to.
1699                    for (group_id, grantor_id) in &role.membership.map {
1700                        role_revokes.insert((*group_id, *id, *grantor_id));
1701                    }
1702                }
1703                ObjectId::Item(id) => {
1704                    if let Some(index) = self.catalog().get_entry(id).index() {
1705                        let humanizer = self.catalog().for_session(session);
1706                        let dependants = self
1707                            .controller
1708                            .compute
1709                            .collection_reverse_dependencies(index.cluster_id, index.global_id())
1710                            .ok()
1711                            .into_iter()
1712                            .flatten()
1713                            .filter(|dependant_id| {
1714                                // Transient Ids belong to Peeks. We are not interested for now in
1715                                // peeks depending on a dropped index.
1716                                // TODO: show a different notice in this case. Something like
1717                                // "There is an in-progress ad hoc SELECT that uses the dropped
1718                                // index. The resources used by the index will be freed when all
1719                                // such SELECTs complete."
1720                                if dependant_id.is_transient() {
1721                                    return false;
1722                                }
1723                                // The item should exist, but don't panic if it doesn't.
1724                                let Some(dependent_id) = humanizer
1725                                    .try_get_item_by_global_id(dependant_id)
1726                                    .map(|item| item.id())
1727                                else {
1728                                    return false;
1729                                };
1730                                // If the dependent object is also being dropped, then there is no
1731                                // problem, so we don't want a notice.
1732                                !ids_set.contains(&ObjectId::Item(dependent_id))
1733                            })
1734                            .flat_map(|dependant_id| {
1735                                // If we are not able to find a name for this ID it probably means
1736                                // we have already dropped the compute collection, in which case we
1737                                // can ignore it.
1738                                humanizer.humanize_id(dependant_id)
1739                            })
1740                            .collect_vec();
1741                        if !dependants.is_empty() {
1742                            dropped_in_use_indexes.push(DroppedInUseIndex {
1743                                index_name: humanizer
1744                                    .humanize_id(index.global_id())
1745                                    .unwrap_or_else(|| id.to_string()),
1746                                dependant_objects: dependants,
1747                            });
1748                        }
1749                    }
1750                }
1751                _ => {}
1752            }
1753        }
1754
1755        for id in &ids {
1756            match id {
1757                // Validate that `ClusterReplica` drops do not drop replicas of managed clusters,
1758                // unless they are internal replicas, which exist outside the scope
1759                // of managed clusters.
1760                ObjectId::ClusterReplica((cluster_id, replica_id)) => {
1761                    if !clusters_to_drop.contains(cluster_id) {
1762                        let cluster = self.catalog.get_cluster(*cluster_id);
1763                        if cluster.is_managed() {
1764                            let replica =
1765                                cluster.replica(*replica_id).expect("Catalog out of sync");
1766                            if !replica.config.location.internal() {
1767                                coord_bail!("cannot drop replica of managed cluster");
1768                            }
1769                        }
1770                    }
1771                }
1772                _ => {}
1773            }
1774        }
1775
1776        for role_id in dropped_roles.keys() {
1777            self.catalog().ensure_not_reserved_role(role_id)?;
1778        }
1779        self.validate_dropped_role_ownership(session, &dropped_roles)?;
1780        // If any role is a member of a dropped role, then we must revoke that membership.
1781        let dropped_role_ids: BTreeSet<_> = dropped_roles.keys().collect();
1782        for role in self.catalog().user_roles() {
1783            for dropped_role_id in
1784                dropped_role_ids.intersection(&role.membership.map.keys().collect())
1785            {
1786                role_revokes.insert((
1787                    **dropped_role_id,
1788                    role.id(),
1789                    *role
1790                        .membership
1791                        .map
1792                        .get(*dropped_role_id)
1793                        .expect("included in keys above"),
1794                ));
1795            }
1796        }
1797
1798        for (default_privilege_object, default_privilege_acls) in
1799            self.catalog().default_privileges()
1800        {
1801            if matches!(
1802                &default_privilege_object.database_id,
1803                Some(database_id) if dropped_databases.contains(database_id),
1804            ) || matches!(
1805                &default_privilege_object.schema_id,
1806                Some(schema_id) if dropped_schemas.contains(schema_id),
1807            ) {
1808                for default_privilege_acl in default_privilege_acls {
1809                    default_privilege_revokes.insert((
1810                        default_privilege_object.clone(),
1811                        default_privilege_acl.clone(),
1812                    ));
1813                }
1814            }
1815        }
1816
1817        let ops = role_revokes
1818            .into_iter()
1819            .map(|(role_id, member_id, grantor_id)| catalog::Op::RevokeRole {
1820                role_id,
1821                member_id,
1822                grantor_id,
1823            })
1824            .chain(default_privilege_revokes.into_iter().map(
1825                |(privilege_object, privilege_acl_item)| catalog::Op::UpdateDefaultPrivilege {
1826                    privilege_object,
1827                    privilege_acl_item,
1828                    variant: UpdatePrivilegeVariant::Revoke,
1829                },
1830            ))
1831            .chain(iter::once(catalog::Op::DropObjects(
1832                ids.into_iter()
1833                    .map(DropObjectInfo::manual_drop_from_object_id)
1834                    .collect(),
1835            )))
1836            .collect();
1837
1838        Ok(DropOps {
1839            ops,
1840            dropped_active_db,
1841            dropped_active_cluster,
1842            dropped_in_use_indexes,
1843        })
1844    }
1845
1846    pub(super) fn sequence_explain_schema(
1847        &self,
1848        ExplainSinkSchemaPlan { json_schema, .. }: ExplainSinkSchemaPlan,
1849    ) -> Result<ExecuteResponse, AdapterError> {
1850        let json_value: serde_json::Value = serde_json::from_str(&json_schema).map_err(|e| {
1851            AdapterError::Explain(mz_repr::explain::ExplainError::SerdeJsonError(e))
1852        })?;
1853
1854        let json_string = json_string(&json_value);
1855        let row = Row::pack_slice(&[Datum::String(&json_string)]);
1856        Ok(Self::send_immediate_rows(row))
1857    }
1858
1859    pub(super) fn sequence_show_all_variables(
1860        &self,
1861        session: &Session,
1862    ) -> Result<ExecuteResponse, AdapterError> {
1863        let mut rows = viewable_variables(self.catalog().state(), session)
1864            .map(|v| (v.name(), v.value(), v.description()))
1865            .collect::<Vec<_>>();
1866        rows.sort_by_cached_key(|(name, _, _)| name.to_lowercase());
1867
1868        // TODO(parkmycar): Pack all of these into a single RowCollection.
1869        let rows: Vec<_> = rows
1870            .into_iter()
1871            .map(|(name, val, desc)| {
1872                Row::pack_slice(&[
1873                    Datum::String(name),
1874                    Datum::String(&val),
1875                    Datum::String(desc),
1876                ])
1877            })
1878            .collect();
1879        Ok(Self::send_immediate_rows(rows))
1880    }
1881
1882    pub(super) fn sequence_show_variable(
1883        &self,
1884        session: &Session,
1885        plan: plan::ShowVariablePlan,
1886    ) -> Result<ExecuteResponse, AdapterError> {
1887        if &plan.name == SCHEMA_ALIAS {
1888            let schemas = self.catalog.resolve_search_path(session);
1889            let schema = schemas.first();
1890            return match schema {
1891                Some((database_spec, schema_spec)) => {
1892                    let schema_name = &self
1893                        .catalog
1894                        .get_schema(database_spec, schema_spec, session.conn_id())
1895                        .name()
1896                        .schema;
1897                    let row = Row::pack_slice(&[Datum::String(schema_name)]);
1898                    Ok(Self::send_immediate_rows(row))
1899                }
1900                None => {
1901                    if session.vars().current_object_missing_warnings() {
1902                        session.add_notice(AdapterNotice::NoResolvableSearchPathSchema {
1903                            search_path: session
1904                                .vars()
1905                                .search_path()
1906                                .into_iter()
1907                                .map(|schema| schema.to_string())
1908                                .collect(),
1909                        });
1910                    }
1911                    Ok(Self::send_immediate_rows(Row::pack_slice(&[Datum::Null])))
1912                }
1913            };
1914        }
1915
1916        let variable = session
1917            .vars()
1918            .get(self.catalog().system_config(), &plan.name)
1919            .or_else(|_| self.catalog().system_config().get(&plan.name))?;
1920
1921        // In lieu of plumbing the user to all system config functions, just check that the var is
1922        // visible.
1923        variable.visible(session.user(), self.catalog().system_config())?;
1924
1925        let row = Row::pack_slice(&[Datum::String(&variable.value())]);
1926        if variable.name() == vars::DATABASE.name()
1927            && matches!(
1928                self.catalog().resolve_database(&variable.value()),
1929                Err(CatalogError::UnknownDatabase(_))
1930            )
1931            && session.vars().current_object_missing_warnings()
1932        {
1933            let name = variable.value();
1934            session.add_notice(AdapterNotice::DatabaseDoesNotExist { name });
1935        } else if variable.name() == vars::CLUSTER.name()
1936            && matches!(
1937                self.catalog().resolve_cluster(&variable.value()),
1938                Err(CatalogError::UnknownCluster(_))
1939            )
1940            && session.vars().current_object_missing_warnings()
1941        {
1942            let name = variable.value();
1943            session.add_notice(AdapterNotice::ClusterDoesNotExist { name });
1944        }
1945        Ok(Self::send_immediate_rows(row))
1946    }
1947
1948    #[instrument]
1949    pub(super) async fn sequence_inspect_shard(
1950        &self,
1951        session: &Session,
1952        plan: plan::InspectShardPlan,
1953    ) -> Result<ExecuteResponse, AdapterError> {
1954        // TODO: Not thrilled about this rbac special case here, but probably
1955        // sufficient for now.
1956        if !session.user().is_internal() {
1957            return Err(AdapterError::Unauthorized(
1958                rbac::UnauthorizedError::MzSystem {
1959                    action: "inspect".into(),
1960                },
1961            ));
1962        }
1963        let state = self
1964            .controller
1965            .storage
1966            .inspect_persist_state(plan.id)
1967            .await?;
1968        let jsonb = Jsonb::from_serde_json(state)?;
1969        Ok(Self::send_immediate_rows(jsonb.into_row()))
1970    }
1971
1972    #[instrument]
1973    pub(super) fn sequence_set_variable(
1974        &self,
1975        session: &mut Session,
1976        plan: plan::SetVariablePlan,
1977    ) -> Result<ExecuteResponse, AdapterError> {
1978        let (name, local) = (plan.name, plan.local);
1979        if &name == TRANSACTION_ISOLATION_VAR_NAME {
1980            self.validate_set_isolation_level(session)?;
1981        }
1982        if &name == vars::CLUSTER.name() {
1983            self.validate_set_cluster(session)?;
1984        }
1985
1986        let vars = session.vars_mut();
1987        let values = match plan.value {
1988            plan::VariableValue::Default => None,
1989            plan::VariableValue::Values(values) => Some(values),
1990        };
1991
1992        match values {
1993            Some(values) => {
1994                vars.set(
1995                    self.catalog().system_config(),
1996                    &name,
1997                    VarInput::SqlSet(&values),
1998                    local,
1999                )?;
2000
2001                let vars = session.vars();
2002
2003                // Emit a warning when deprecated variables are used.
2004                // TODO(database-issues#8069) remove this after sufficient time has passed
2005                if name == vars::OLD_AUTO_ROUTE_CATALOG_QUERIES {
2006                    session.add_notice(AdapterNotice::AutoRouteIntrospectionQueriesUsage);
2007                } else if name == vars::CLUSTER.name()
2008                    && values[0] == vars::OLD_CATALOG_SERVER_CLUSTER
2009                {
2010                    session.add_notice(AdapterNotice::IntrospectionClusterUsage);
2011                }
2012
2013                // Database or cluster value does not correspond to a catalog item.
2014                if name.as_str() == vars::DATABASE.name()
2015                    && matches!(
2016                        self.catalog().resolve_database(vars.database()),
2017                        Err(CatalogError::UnknownDatabase(_))
2018                    )
2019                    && session.vars().current_object_missing_warnings()
2020                {
2021                    let name = vars.database().to_string();
2022                    session.add_notice(AdapterNotice::DatabaseDoesNotExist { name });
2023                } else if name.as_str() == vars::CLUSTER.name()
2024                    && matches!(
2025                        self.catalog().resolve_cluster(vars.cluster()),
2026                        Err(CatalogError::UnknownCluster(_))
2027                    )
2028                    && session.vars().current_object_missing_warnings()
2029                {
2030                    let name = vars.cluster().to_string();
2031                    session.add_notice(AdapterNotice::ClusterDoesNotExist { name });
2032                } else if name.as_str() == TRANSACTION_ISOLATION_VAR_NAME {
2033                    let v = values.into_first().to_lowercase();
2034                    if v == IsolationLevel::ReadUncommitted.as_variant_str()
2035                        || v == IsolationLevel::ReadCommitted.as_variant_str()
2036                        || v == IsolationLevel::RepeatableRead.as_variant_str()
2037                    {
2038                        session.add_notice(AdapterNotice::UnimplementedIsolationLevel {
2039                            isolation_level: v,
2040                        });
2041                    } else if v == IsolationLevel::StrongSessionSerializable.as_variant_str() {
2042                        session.add_notice(AdapterNotice::StrongSessionSerializable);
2043                    }
2044                }
2045
2046                // Reject incompatible combinations of bounded staleness and
2047                // `real_time_recency` after the variable has been applied. Either
2048                // SET name can introduce the conflict, so check both.
2049                if (name.as_str() == TRANSACTION_ISOLATION_VAR_NAME
2050                    || name.as_str() == vars::REAL_TIME_RECENCY.name())
2051                    && session
2052                        .vars()
2053                        .transaction_isolation()
2054                        .is_bounded_staleness()
2055                    && session.vars().real_time_recency()
2056                {
2057                    return Err(AdapterError::BoundedStalenessRealTimeRecencyConflict);
2058                }
2059            }
2060            None => vars.reset(self.catalog().system_config(), &name, local)?,
2061        }
2062
2063        Ok(ExecuteResponse::SetVariable { name, reset: false })
2064    }
2065
2066    pub(super) fn sequence_reset_variable(
2067        &self,
2068        session: &mut Session,
2069        plan: plan::ResetVariablePlan,
2070    ) -> Result<ExecuteResponse, AdapterError> {
2071        let name = plan.name;
2072        if &name == TRANSACTION_ISOLATION_VAR_NAME {
2073            self.validate_set_isolation_level(session)?;
2074        }
2075        if &name == vars::CLUSTER.name() {
2076            self.validate_set_cluster(session)?;
2077        }
2078        session
2079            .vars_mut()
2080            .reset(self.catalog().system_config(), &name, false)?;
2081        Ok(ExecuteResponse::SetVariable { name, reset: true })
2082    }
2083
2084    pub(super) fn sequence_set_transaction(
2085        &self,
2086        session: &mut Session,
2087        plan: plan::SetTransactionPlan,
2088    ) -> Result<ExecuteResponse, AdapterError> {
2089        // TODO(jkosh44) Only supports isolation levels for now.
2090        for mode in plan.modes {
2091            match mode {
2092                TransactionMode::AccessMode(_) => {
2093                    return Err(AdapterError::Unsupported("SET TRANSACTION <access-mode>"));
2094                }
2095                TransactionMode::IsolationLevel(isolation_level) => {
2096                    self.validate_set_isolation_level(session)?;
2097
2098                    session.vars_mut().set(
2099                        self.catalog().system_config(),
2100                        TRANSACTION_ISOLATION_VAR_NAME,
2101                        VarInput::Flat(&isolation_level.to_ast_string_stable()),
2102                        plan.local,
2103                    )?
2104                }
2105            }
2106        }
2107        Ok(ExecuteResponse::SetVariable {
2108            name: TRANSACTION_ISOLATION_VAR_NAME.to_string(),
2109            reset: false,
2110        })
2111    }
2112
2113    fn validate_set_isolation_level(&self, session: &Session) -> Result<(), AdapterError> {
2114        if session.transaction().contains_ops() {
2115            Err(AdapterError::InvalidSetIsolationLevel)
2116        } else {
2117            Ok(())
2118        }
2119    }
2120
2121    fn validate_set_cluster(&self, session: &Session) -> Result<(), AdapterError> {
2122        if session.transaction().contains_ops() {
2123            Err(AdapterError::InvalidSetCluster)
2124        } else {
2125            Ok(())
2126        }
2127    }
2128
2129    #[instrument]
2130    pub(super) async fn sequence_end_transaction(
2131        &mut self,
2132        mut ctx: ExecuteContext,
2133        mut action: EndTransactionAction,
2134    ) {
2135        // If the transaction has failed, we can only rollback.
2136        if let (EndTransactionAction::Commit, TransactionStatus::Failed(_)) =
2137            (&action, ctx.session().transaction())
2138        {
2139            action = EndTransactionAction::Rollback;
2140        }
2141        let response = match action {
2142            EndTransactionAction::Commit => Ok(PendingTxnResponse::Committed {
2143                params: BTreeMap::new(),
2144            }),
2145            EndTransactionAction::Rollback => Ok(PendingTxnResponse::Rolledback {
2146                params: BTreeMap::new(),
2147            }),
2148        };
2149
2150        let result = self.sequence_end_transaction_inner(&mut ctx, action).await;
2151
2152        let (response, action) = match result {
2153            Ok((Some(TransactionOps::Writes(writes)), _)) if writes.is_empty() => {
2154                (response, action)
2155            }
2156            Ok((Some(TransactionOps::Writes(writes)), write_lock_guards)) => {
2157                // Make sure we have the correct set of write locks for this transaction.
2158                // Aggressively dropping partial sets of locks to prevent deadlocking separate
2159                // transactions.
2160                let validated_locks = match write_lock_guards {
2161                    None => None,
2162                    Some(locks) => match locks.validate(writes.iter().map(|op| op.id)) {
2163                        Ok(locks) => Some(locks),
2164                        Err(missing) => {
2165                            tracing::error!(?missing, "programming error, missing write locks");
2166                            return ctx.retire(Err(AdapterError::WrongSetOfLocks));
2167                        }
2168                    },
2169                };
2170
2171                let mut collected_writes: BTreeMap<CatalogItemId, SmallVec<_>> = BTreeMap::new();
2172                for WriteOp { id, rows } in writes {
2173                    let total_rows = collected_writes.entry(id).or_default();
2174                    total_rows.push(rows);
2175                }
2176
2177                self.submit_write(PendingWriteTxn::User {
2178                    span: Span::current(),
2179                    writes: collected_writes,
2180                    write_locks: validated_locks,
2181                    responder: UserWriteResponder::Session(PendingTxn {
2182                        ctx,
2183                        response,
2184                        action,
2185                    }),
2186                });
2187                return;
2188            }
2189            Ok((
2190                Some(TransactionOps::Peeks {
2191                    determination,
2192                    requires_linearization: RequireLinearization::Required,
2193                    ..
2194                }),
2195                _,
2196            )) if ctx.session().vars().transaction_isolation()
2197                == &IsolationLevel::StrictSerializable =>
2198            {
2199                let conn_id = ctx.session().conn_id().clone();
2200                let pending_read_txn = PendingReadTxn {
2201                    txn: PendingRead::Read {
2202                        txn: PendingTxn {
2203                            ctx,
2204                            response,
2205                            action,
2206                        },
2207                    },
2208                    timestamp_context: determination.timestamp_context,
2209                    created: Instant::now(),
2210                    num_requeues: 0,
2211                    otel_ctx: OpenTelemetryContext::obtain(),
2212                };
2213                self.strict_serializable_reads_tx
2214                    .send((conn_id, pending_read_txn))
2215                    .expect("sending to strict_serializable_reads_tx cannot fail");
2216                return;
2217            }
2218            Ok((
2219                Some(TransactionOps::Peeks {
2220                    determination,
2221                    requires_linearization: RequireLinearization::Required,
2222                    ..
2223                }),
2224                _,
2225            )) if ctx.session().vars().transaction_isolation()
2226                == &IsolationLevel::StrongSessionSerializable =>
2227            {
2228                if let Some((timeline, ts)) = determination.timestamp_context.timeline_timestamp() {
2229                    ctx.session_mut()
2230                        .ensure_timestamp_oracle(timeline.clone())
2231                        .apply_write(*ts);
2232                }
2233                (response, action)
2234            }
2235            Ok((Some(TransactionOps::SingleStatement { stmt, params }), _)) => {
2236                self.internal_cmd_tx
2237                    .send(Message::ExecuteSingleStatementTransaction {
2238                        ctx,
2239                        otel_ctx: OpenTelemetryContext::obtain(),
2240                        stmt,
2241                        params,
2242                    })
2243                    .expect("must send");
2244                return;
2245            }
2246            Ok((_, _)) => (response, action),
2247            Err(err) => (Err(err), EndTransactionAction::Rollback),
2248        };
2249        let changed = ctx.session_mut().vars_mut().end_transaction(action);
2250        // Append any parameters that changed to the response.
2251        let response = response.map(|mut r| {
2252            r.extend_params(changed);
2253            ExecuteResponse::from(r)
2254        });
2255
2256        ctx.retire(response);
2257    }
2258
2259    #[instrument]
2260    async fn sequence_end_transaction_inner(
2261        &mut self,
2262        ctx: &mut ExecuteContext,
2263        action: EndTransactionAction,
2264    ) -> Result<(Option<TransactionOps>, Option<WriteLocks>), AdapterError> {
2265        let (txn, retire_notify) = self.clear_transaction(ctx.session_mut()).await;
2266        ctx.delay_response_until(retire_notify);
2267
2268        if let EndTransactionAction::Commit = action {
2269            if let (Some(mut ops), write_lock_guards) = txn.into_ops_and_lock_guard() {
2270                match &mut ops {
2271                    TransactionOps::Writes(writes) => {
2272                        for WriteOp { id, .. } in &mut writes.iter() {
2273                            // Re-verify this id exists.
2274                            let _ = self.catalog().try_get_entry(id).ok_or_else(|| {
2275                                AdapterError::Catalog(mz_catalog::memory::error::Error {
2276                                    kind: ErrorKind::Sql(CatalogError::UnknownItem(id.to_string())),
2277                                })
2278                            })?;
2279                        }
2280
2281                        // `rows` can be empty if, say, a DELETE's WHERE clause had 0 results.
2282                        writes.retain(|WriteOp { rows, .. }| !rows.is_empty());
2283                    }
2284                    TransactionOps::DDL {
2285                        ops,
2286                        state: _,
2287                        side_effects,
2288                        revision,
2289                        snapshot: _,
2290                    } => {
2291                        // Make sure our catalog hasn't changed.
2292                        if *revision != self.catalog().transient_revision() {
2293                            return Err(AdapterError::DDLTransactionRace);
2294                        }
2295                        // Commit all of our queued ops.
2296                        let ops = std::mem::take(ops);
2297                        let side_effects = std::mem::take(side_effects);
2298                        self.catalog_transact_with_side_effects(
2299                            Some(ctx),
2300                            ops,
2301                            move |a, mut ctx| {
2302                                Box::pin(async move {
2303                                    for side_effect in side_effects {
2304                                        side_effect(a, ctx.as_mut().map(|ctx| &mut **ctx)).await;
2305                                    }
2306                                })
2307                            },
2308                        )
2309                        .await?;
2310                    }
2311                    _ => (),
2312                }
2313                return Ok((Some(ops), write_lock_guards));
2314            }
2315        }
2316
2317        Ok((None, None))
2318    }
2319
2320    pub(super) async fn sequence_side_effecting_func(
2321        &mut self,
2322        ctx: ExecuteContext,
2323        plan: SideEffectingFunc,
2324    ) {
2325        match plan {
2326            SideEffectingFunc::PgCancelBackend { connection_id } => {
2327                let Some(connection_id) = connection_id else {
2328                    // The argument was `NULL`, so, like in PostgreSQL, the
2329                    // function returns `NULL`.
2330                    ctx.retire(Ok(Self::send_immediate_rows(Row::pack_slice(&[
2331                        Datum::Null,
2332                    ]))));
2333                    return;
2334                };
2335
2336                if ctx.session().conn_id().unhandled() == connection_id {
2337                    // As a special case, if we're canceling ourselves, we send
2338                    // back a canceled resposne to the client issuing the query,
2339                    // and so we need to do no further processing of the cancel.
2340                    ctx.retire(Err(AdapterError::Canceled));
2341                    return;
2342                }
2343
2344                let res = if let Some((id_handle, _conn_meta)) =
2345                    self.active_conns.get_key_value(&connection_id)
2346                {
2347                    // check_plan already verified role membership.
2348                    self.handle_privileged_cancel(id_handle.clone()).await;
2349                    Datum::True
2350                } else {
2351                    Datum::False
2352                };
2353                ctx.retire(Ok(Self::send_immediate_rows(Row::pack_slice(&[res]))));
2354            }
2355        }
2356    }
2357
2358    /// Execute a side-effecting function from the frontend peek path.
2359    /// This is separate from `sequence_side_effecting_func` because it doesn't have an
2360    /// ExecuteContext. RBAC is checked by the caller via `rbac::check_plan` before
2361    /// sending `Command::ExecuteSideEffectingFunc`. The caller must hold the target
2362    /// connection's `ConnectionId` handle from its RBAC check until this command
2363    /// completes, so that the connection found in `active_conns` here (if any) is
2364    /// the same one the check was performed against.
2365    ///
2366    /// TODO(peek-seq): Delete `sequence_side_effecting_func` after we delete the old peek
2367    /// sequencing.
2368    pub(crate) async fn execute_side_effecting_func(
2369        &mut self,
2370        plan: SideEffectingFunc,
2371        conn_id: ConnectionId,
2372    ) -> Result<ExecuteResponse, AdapterError> {
2373        match plan {
2374            SideEffectingFunc::PgCancelBackend { connection_id } => {
2375                let Some(connection_id) = connection_id else {
2376                    // The argument was `NULL`, so, like in PostgreSQL, the
2377                    // function returns `NULL`.
2378                    return Ok(Self::send_immediate_rows(Row::pack_slice(&[Datum::Null])));
2379                };
2380
2381                if conn_id.unhandled() == connection_id {
2382                    // As a special case, if we're canceling ourselves, we return
2383                    // a canceled response to the client issuing the query,
2384                    // and so we need to do no further processing of the cancel.
2385                    return Err(AdapterError::Canceled);
2386                }
2387
2388                // The caller verified role membership via rbac::check_plan and
2389                // still holds the target's `ConnectionId` handle, so this entry
2390                // (if present) is the same connection the check was performed
2391                // against.
2392                if let Some((id_handle, _conn_meta)) =
2393                    self.active_conns.get_key_value(&connection_id)
2394                {
2395                    self.handle_privileged_cancel(id_handle.clone()).await;
2396                    Ok(Self::send_immediate_rows(Row::pack_slice(&[Datum::True])))
2397                } else {
2398                    // Connection not found, return false.
2399                    Ok(Self::send_immediate_rows(Row::pack_slice(&[Datum::False])))
2400                }
2401            }
2402        }
2403    }
2404
2405    /// Inner method that performs the actual real-time recency timestamp determination.
2406    /// This is called by both the old peek sequencing code (via `determine_real_time_recent_timestamp`)
2407    /// and the new command handler for `Command::DetermineRealTimeRecentTimestamp`.
2408    pub(crate) async fn determine_real_time_recent_timestamp(
2409        &self,
2410        source_ids: impl Iterator<Item = GlobalId>,
2411        real_time_recency_timeout: Duration,
2412    ) -> Result<Option<RtrTimestampFuture>, AdapterError> {
2413        let item_ids = source_ids
2414            .map(|gid| {
2415                self.catalog
2416                    .try_resolve_item_id(&gid)
2417                    .ok_or_else(|| AdapterError::RtrDropFailure(gid.to_string()))
2418            })
2419            .collect::<Result<Vec<_>, _>>()?;
2420
2421        // Find all dependencies transitively because we need to ensure that
2422        // RTR queries determine the timestamp from the sources' (i.e.
2423        // storage objects that ingest data from external systems) remap
2424        // data. We "cheat" a little bit and filter out any IDs that aren't
2425        // user objects because we know they are not a RTR source.
2426        let mut to_visit = VecDeque::from_iter(item_ids.into_iter().filter(CatalogItemId::is_user));
2427        // If none of the sources are user objects, we don't need to provide
2428        // a RTR timestamp.
2429        if to_visit.is_empty() {
2430            return Ok(None);
2431        }
2432
2433        let mut timestamp_objects = BTreeSet::new();
2434
2435        while let Some(id) = to_visit.pop_front() {
2436            timestamp_objects.insert(id);
2437            to_visit.extend(
2438                self.catalog()
2439                    .get_entry(&id)
2440                    .uses()
2441                    .into_iter()
2442                    .filter(|id| !timestamp_objects.contains(id) && id.is_user()),
2443            );
2444        }
2445        let timestamp_objects = timestamp_objects
2446            .into_iter()
2447            .flat_map(|item_id| self.catalog().get_entry(&item_id).global_ids())
2448            .collect();
2449
2450        let r = self
2451            .controller
2452            .determine_real_time_recent_timestamp(timestamp_objects, real_time_recency_timeout)
2453            .await?;
2454
2455        Ok(Some(r))
2456    }
2457
2458    pub(crate) async fn await_real_time_recent_timestamp<F>(
2459        catalog: Arc<Catalog>,
2460        fut: F,
2461    ) -> Result<Timestamp, AdapterError>
2462    where
2463        F: Future<Output = Result<Timestamp, StorageError>>,
2464    {
2465        fut.await
2466            .map_err(|error| Self::real_time_recent_timestamp_error(&catalog, error))
2467    }
2468
2469    fn real_time_recent_timestamp_error(catalog: &Catalog, error: StorageError) -> AdapterError {
2470        let rtr_name = |id: &GlobalId| {
2471            catalog
2472                .try_get_entry_by_global_id(id)
2473                .map(|e| e.name().item.clone())
2474                .unwrap_or_else(|| id.to_string())
2475        };
2476
2477        match error {
2478            StorageError::RtrTimeout(id) => AdapterError::RtrTimeout(rtr_name(&id)),
2479            StorageError::RtrDropFailure(id) => AdapterError::RtrDropFailure(rtr_name(&id)),
2480            error => error.into(),
2481        }
2482    }
2483
2484    /// Checks to see if the session needs a real time recency timestamp and if so returns
2485    /// a future that will return the timestamp.
2486    pub(crate) async fn determine_real_time_recent_timestamp_if_needed(
2487        &self,
2488        session: &Session,
2489        source_ids: impl Iterator<Item = GlobalId>,
2490    ) -> Result<Option<RtrTimestampFuture>, AdapterError> {
2491        let vars = session.vars();
2492
2493        if vars.real_time_recency()
2494            && vars.transaction_isolation() == &IsolationLevel::StrictSerializable
2495            && !session.contains_read_timestamp()
2496        {
2497            self.determine_real_time_recent_timestamp(source_ids, *vars.real_time_recency_timeout())
2498                .await
2499        } else {
2500            Ok(None)
2501        }
2502    }
2503
2504    #[instrument]
2505    pub(super) async fn sequence_explain_plan(
2506        &mut self,
2507        ctx: ExecuteContext,
2508        plan: plan::ExplainPlanPlan,
2509        target_cluster: TargetCluster,
2510    ) {
2511        match &plan.explainee {
2512            plan::Explainee::Statement(stmt) => match stmt {
2513                plan::ExplaineeStatement::CreateView { .. } => {
2514                    self.explain_create_view(ctx, plan).await;
2515                }
2516                plan::ExplaineeStatement::CreateMaterializedView { .. } => {
2517                    self.explain_create_materialized_view(ctx, plan).await;
2518                }
2519                plan::ExplaineeStatement::CreateIndex { .. } => {
2520                    self.explain_create_index(ctx, plan).await;
2521                }
2522                plan::ExplaineeStatement::Select { .. } => {
2523                    self.explain_peek(ctx, plan, target_cluster).await;
2524                }
2525                plan::ExplaineeStatement::Subscribe { .. } => {
2526                    self.explain_subscribe(ctx, plan, target_cluster).await;
2527                }
2528            },
2529            plan::Explainee::View(_) => {
2530                let result = self.explain_view(&ctx, plan);
2531                ctx.retire(result);
2532            }
2533            plan::Explainee::MaterializedView(_) => {
2534                let result = self.explain_materialized_view(&ctx, plan);
2535                ctx.retire(result);
2536            }
2537            plan::Explainee::Index(_) => {
2538                let result = self.explain_index(&ctx, plan);
2539                ctx.retire(result);
2540            }
2541            plan::Explainee::ReplanView(_) => {
2542                self.explain_replan_view(ctx, plan).await;
2543            }
2544            plan::Explainee::ReplanMaterializedView(_) => {
2545                self.explain_replan_materialized_view(ctx, plan).await;
2546            }
2547            plan::Explainee::ReplanIndex(_) => {
2548                self.explain_replan_index(ctx, plan).await;
2549            }
2550        };
2551    }
2552
2553    pub(super) async fn sequence_explain_pushdown(
2554        &mut self,
2555        ctx: ExecuteContext,
2556        plan: plan::ExplainPushdownPlan,
2557        target_cluster: TargetCluster,
2558    ) {
2559        match plan.explainee {
2560            Explainee::Statement(ExplaineeStatement::Select {
2561                broken: false,
2562                plan,
2563                desc: _,
2564            }) => {
2565                let stage = return_if_err!(
2566                    self.peek_validate(
2567                        ctx.session(),
2568                        plan,
2569                        target_cluster,
2570                        None,
2571                        ExplainContext::Pushdown,
2572                        Some(ctx.session().vars().max_query_result_size()),
2573                    ),
2574                    ctx
2575                );
2576                self.sequence_staged(ctx, Span::current(), stage).await;
2577            }
2578            Explainee::MaterializedView(item_id) => {
2579                self.explain_pushdown_materialized_view(ctx, item_id).await;
2580            }
2581            _ => {
2582                ctx.retire(Err(AdapterError::Unsupported(
2583                    "EXPLAIN FILTER PUSHDOWN queries for this explainee type",
2584                )));
2585            }
2586        };
2587    }
2588
2589    /// Executes an EXPLAIN FILTER PUSHDOWN, with read holds passed in.
2590    async fn execute_explain_pushdown_with_read_holds(
2591        &self,
2592        ctx: ExecuteContext,
2593        as_of: Antichain<Timestamp>,
2594        mz_now: ResultSpec<'static>,
2595        read_holds: Option<ReadHolds>,
2596        imports: impl IntoIterator<Item = (GlobalId, MapFilterProject)> + 'static,
2597    ) {
2598        let fut = self
2599            .explain_pushdown_future(ctx.session(), as_of, mz_now, imports)
2600            .await;
2601        task::spawn(|| "render explain pushdown", async move {
2602            // Transfer the necessary read holds over to the background task
2603            let _read_holds = read_holds;
2604            let res = fut.await;
2605            ctx.retire(res);
2606        });
2607    }
2608
2609    /// Returns a future that will execute EXPLAIN FILTER PUSHDOWN.
2610    async fn explain_pushdown_future<I: IntoIterator<Item = (GlobalId, MapFilterProject)>>(
2611        &self,
2612        session: &Session,
2613        as_of: Antichain<Timestamp>,
2614        mz_now: ResultSpec<'static>,
2615        imports: I,
2616    ) -> impl Future<Output = Result<ExecuteResponse, AdapterError>> + use<I> {
2617        // Get the needed Coordinator stuff and call the freestanding, shared helper.
2618        super::explain_pushdown_future_inner(
2619            session,
2620            &self.catalog,
2621            &self.controller.storage_collections,
2622            as_of,
2623            mz_now,
2624            imports,
2625        )
2626        .await
2627    }
2628
2629    #[instrument]
2630    pub(super) async fn sequence_insert(
2631        &mut self,
2632        mut ctx: ExecuteContext,
2633        plan: plan::InsertPlan,
2634    ) {
2635        // Normally, this would get checked when trying to add "write ops" to
2636        // the transaction but we go down diverging paths below, based on
2637        // whether the INSERT is only constant values or not.
2638        //
2639        // For the non-constant case we sequence an implicit read-then-write,
2640        // which messes with the transaction ops and would allow an implicit
2641        // read-then-write to sneak into a read-only transaction.
2642        if !ctx.session_mut().transaction().allows_writes() {
2643            ctx.retire(Err(AdapterError::ReadOnlyTransaction));
2644            return;
2645        }
2646        if ctx
2647            .session()
2648            .vars()
2649            .transaction_isolation()
2650            .is_bounded_staleness()
2651        {
2652            ctx.retire(Err(AdapterError::BoundedStalenessReadOnly));
2653            return;
2654        }
2655
2656        // The structure of this code originates from a time where
2657        // `ReadThenWritePlan` was carrying an `MirRelationExpr` instead of an
2658        // optimized `MirRelationExpr`.
2659        //
2660        // Ideally, we would like to make the `selection.as_const().is_some()`
2661        // check on `plan.values` instead. However, `VALUES (1), (3)` statements
2662        // are planned as a Wrap($n, $vals) call, so until we can reduce
2663        // HirRelationExpr this will always returns false.
2664        //
2665        // Unfortunately, hitting the default path of the match below also
2666        // causes a lot of tests to fail, so we opted to go with the extra
2667        // `plan.values.clone()` statements when producing the `optimized_mir`
2668        // and re-optimize the values in the `sequence_read_then_write` call.
2669        let optimized_mir = if let Some(..) = &plan.values.as_const() {
2670            // We don't perform any optimizations on an expression that is already
2671            // a constant for writes, as we want to maximize bulk-insert throughput.
2672            let expr = return_if_err!(
2673                plan.values
2674                    .clone()
2675                    .lower(self.catalog().system_config(), None),
2676                ctx
2677            );
2678            OptimizedMirRelationExpr(expr)
2679        } else {
2680            // Collect optimizer parameters.
2681            let optimizer_config = optimize::OptimizerConfig::from(self.catalog().system_config());
2682
2683            // (`optimize::view::Optimizer` has a special case for constant queries.)
2684            let mut optimizer = optimize::view::Optimizer::new(optimizer_config, None);
2685
2686            // HIR ⇒ MIR lowering and MIR ⇒ MIR optimization (local)
2687            return_if_err!(optimizer.optimize(plan.values.clone()), ctx)
2688        };
2689
2690        match optimized_mir.into_inner() {
2691            selection if selection.as_const().is_some() && plan.returning.is_empty() => {
2692                let catalog = self.owned_catalog();
2693                mz_ore::task::spawn(|| "coord::sequence_inner", async move {
2694                    let result =
2695                        Self::insert_constant(&catalog, ctx.session_mut(), plan.id, selection);
2696
2697                    // Test-only synchronization point: parks a blind INSERT once its rows are
2698                    // packed against the table's current RelationDesc, but before retiring
2699                    // triggers the implicit commit that stages them for group commit. Lets a
2700                    // test land a concurrent ALTER TABLE ... ADD COLUMN in that window. Used by
2701                    // test_insert_concurrent_alter_table.
2702                    fail::fail_point!("insert_after_pack_before_commit");
2703
2704                    ctx.retire(result);
2705                });
2706            }
2707            // All non-constant values must be planned as read-then-writes.
2708            _ => {
2709                let desc_arity = match self.catalog().try_get_entry(&plan.id) {
2710                    Some(table) => {
2711                        // Inserts always occur at the latest version of the table.
2712                        let desc = table.relation_desc_latest().expect("table has a desc");
2713                        desc.arity()
2714                    }
2715                    None => {
2716                        ctx.retire(Err(AdapterError::Catalog(
2717                            mz_catalog::memory::error::Error {
2718                                kind: ErrorKind::Sql(CatalogError::UnknownItem(
2719                                    plan.id.to_string(),
2720                                )),
2721                            },
2722                        )));
2723                        return;
2724                    }
2725                };
2726
2727                let finishing = RowSetFinishing {
2728                    order_by: vec![],
2729                    limit: None,
2730                    offset: 0,
2731                    project: (0..desc_arity).collect(),
2732                };
2733
2734                let read_then_write_plan = plan::ReadThenWritePlan {
2735                    id: plan.id,
2736                    selection: plan.values,
2737                    finishing,
2738                    assignments: BTreeMap::new(),
2739                    kind: MutationKind::Insert,
2740                    returning: plan.returning,
2741                };
2742
2743                self.sequence_read_then_write(ctx, read_then_write_plan)
2744                    .await;
2745            }
2746        }
2747    }
2748
2749    /// ReadThenWrite is a plan whose writes depend on the results of a
2750    /// read. This works by doing a Peek then queuing a SendDiffs. No writes
2751    /// or read-then-writes can occur between the Peek and SendDiff otherwise a
2752    /// serializability violation could occur.
2753    #[instrument]
2754    pub(super) async fn sequence_read_then_write(
2755        &mut self,
2756        mut ctx: ExecuteContext,
2757        plan: plan::ReadThenWritePlan,
2758    ) {
2759        if ctx
2760            .session()
2761            .vars()
2762            .transaction_isolation()
2763            .is_bounded_staleness()
2764        {
2765            ctx.retire(Err(AdapterError::BoundedStalenessReadOnly));
2766            return;
2767        }
2768
2769        // The lock-based and OCC paths do not synchronize with each other, so
2770        // reaching this path while frontend sequencing is enabled is a routing
2771        // bug that could corrupt data.
2772        if self.frontend_read_then_write_enabled {
2773            ctx.retire(Err(AdapterError::Internal(
2774                "coordinator read-then-write reached while frontend OCC sequencing is enabled"
2775                    .into(),
2776            )));
2777            return;
2778        }
2779
2780        let mut source_ids: BTreeSet<_> = plan
2781            .selection
2782            .depends_on()
2783            .into_iter()
2784            .map(|gid| self.catalog().resolve_item_id(&gid))
2785            .collect();
2786        source_ids.insert(plan.id);
2787
2788        // If the transaction doesn't already have write locks, acquire them.
2789        if ctx.session().transaction().write_locks().is_none() {
2790            // Pre-define all of the locks we need.
2791            let mut write_locks = WriteLocks::builder(source_ids.iter().copied());
2792
2793            // Try acquiring all of our locks.
2794            for id in &source_ids {
2795                if let Some(lock) = self.try_grant_object_write_lock(*id) {
2796                    write_locks.insert_lock(*id, lock);
2797                }
2798            }
2799
2800            // See if we acquired all of the neccessary locks.
2801            let write_locks = match write_locks.all_or_nothing(ctx.session().conn_id()) {
2802                Ok(locks) => locks,
2803                Err(missing) => {
2804                    // Defer our write if we couldn't acquire all of the locks.
2805                    let role_metadata = ctx.session().role_metadata().clone();
2806                    let acquire_future = self.grant_object_write_lock(missing).map(Option::Some);
2807                    let plan = DeferredPlan {
2808                        ctx,
2809                        plan: Plan::ReadThenWrite(plan),
2810                        validity: PlanValidity::new(
2811                            &self.catalog,
2812                            source_ids.clone(),
2813                            None,
2814                            None,
2815                            role_metadata,
2816                        ),
2817                        requires_locks: source_ids,
2818                        // Writes don't track resolved IDs.
2819                        resolved_ids: ResolvedIds::empty(),
2820                        sql_impl_resolved_ids: ResolvedIds::empty(),
2821                    };
2822                    return self.defer_op(acquire_future, DeferredOp::Plan(plan));
2823                }
2824            };
2825
2826            ctx.session_mut()
2827                .try_grant_write_locks(write_locks)
2828                .expect("session has already been granted write locks");
2829        }
2830
2831        let plan::ReadThenWritePlan {
2832            id,
2833            kind,
2834            selection,
2835            mut assignments,
2836            finishing,
2837            mut returning,
2838        } = plan;
2839
2840        // Read then writes can be queued, so re-verify the id exists.
2841        let desc = match self.catalog().try_get_entry(&id) {
2842            Some(table) => {
2843                // Inserts always occur at the latest version of the table.
2844                table
2845                    .relation_desc_latest()
2846                    .expect("table has a desc")
2847                    .into_owned()
2848            }
2849            None => {
2850                ctx.retire(Err(AdapterError::Catalog(
2851                    mz_catalog::memory::error::Error {
2852                        kind: ErrorKind::Sql(CatalogError::UnknownItem(id.to_string())),
2853                    },
2854                )));
2855                return;
2856            }
2857        };
2858
2859        // Disallow mz_now in any position because read time and write time differ.
2860        let contains_temporal = selection.contains_temporal()
2861            || assignments.values().any(|e| e.contains_temporal())
2862            || returning.iter().any(|e| e.contains_temporal());
2863        if contains_temporal {
2864            ctx.retire(Err(AdapterError::Unsupported(
2865                "calls to mz_now in write statements",
2866            )));
2867            return;
2868        }
2869
2870        // Ensure all objects `selection` depends on are valid for `ReadThenWrite` operations.
2871        let dependency_ids = selection
2872            .depends_on()
2873            .into_iter()
2874            .map(|gid| self.catalog().resolve_item_id(&gid));
2875        let max_rw_dependencies =
2876            READ_THEN_WRITE_MAX_DEPENDENCIES.get(self.catalog().system_config().dyncfgs());
2877        if let Err(err) = validate_read_then_write_dependencies(
2878            self.catalog(),
2879            dependency_ids,
2880            max_rw_dependencies,
2881            DependencyPolicy::UserDml,
2882        ) {
2883            ctx.retire(Err(err));
2884            return;
2885        }
2886
2887        let (peek_tx, peek_rx) = oneshot::channel();
2888        let peek_client_tx = ClientTransmitter::new(peek_tx, self.internal_cmd_tx.clone());
2889        let (tx, _, session, extra, response_barriers) = ctx.into_parts();
2890        // We construct a new execute context for the peek, with a trivial (`Default::default()`)
2891        // execution context, because this peek does not directly correspond to an execute,
2892        // and so we don't need to take any action on its retirement.
2893        // TODO[btv]: we might consider extending statement logging to log the inner
2894        // statement separately, here. That would require us to plumb through the SQL of the inner statement,
2895        // and mint a new "real" execution context here. We'd also have to add some logic to
2896        // make sure such "sub-statements" are always sampled when the top-level statement is
2897        //
2898        // It's debatable whether this makes sense conceptually,
2899        // because the inner fragment here is not actually a
2900        // "statement" in its own right.
2901        let peek_ctx = ExecuteContext::from_parts(
2902            peek_client_tx,
2903            self.internal_cmd_tx.clone(),
2904            session,
2905            Default::default(),
2906        );
2907
2908        self.sequence_peek(
2909            peek_ctx,
2910            plan::SelectPlan {
2911                select: None,
2912                source: selection,
2913                when: QueryWhen::FreshestTableWrite,
2914                finishing,
2915                copy_to: None,
2916            },
2917            TargetCluster::Active,
2918            None,
2919        )
2920        .await;
2921
2922        let internal_cmd_tx = self.internal_cmd_tx.clone();
2923        let strict_serializable_reads_tx = self.strict_serializable_reads_tx.clone();
2924        let catalog = self.owned_catalog();
2925        let max_result_size = self.catalog().system_config().max_result_size();
2926
2927        task::spawn(|| format!("sequence_read_then_write:{id}"), async move {
2928            let (peek_response, session) = match peek_rx.await {
2929                Ok(Response {
2930                    result: Ok(resp),
2931                    session,
2932                    otel_ctx,
2933                }) => {
2934                    otel_ctx.attach_as_parent();
2935                    (resp, session)
2936                }
2937                Ok(Response {
2938                    result: Err(e),
2939                    session,
2940                    otel_ctx,
2941                }) => {
2942                    let ctx = ExecuteContext::from_parts_with_response_barriers(
2943                        tx,
2944                        internal_cmd_tx.clone(),
2945                        session,
2946                        extra,
2947                        response_barriers,
2948                    );
2949                    otel_ctx.attach_as_parent();
2950                    ctx.retire(Err(e));
2951                    return;
2952                }
2953                // It is not an error for these results to be ready after `peek_client_tx` has been dropped.
2954                Err(e) => return warn!("internal_cmd_rx dropped before we could send: {:?}", e),
2955            };
2956            let mut ctx = ExecuteContext::from_parts_with_response_barriers(
2957                tx,
2958                internal_cmd_tx.clone(),
2959                session,
2960                extra,
2961                response_barriers,
2962            );
2963            let mut timeout_dur = *ctx.session().vars().statement_timeout();
2964
2965            // Timeout of 0 is equivalent to "off", meaning we will wait "forever."
2966            if timeout_dur == Duration::ZERO {
2967                timeout_dur = Duration::MAX;
2968            }
2969
2970            let style = ExprPrepOneShot {
2971                logical_time: EvalTime::NotAvailable, // We already errored out on mz_now above.
2972                session: ctx.session(),
2973                catalog_state: catalog.state(),
2974            };
2975            for expr in assignments.values_mut().chain(returning.iter_mut()) {
2976                return_if_err!(style.prep_scalar_expr(expr), ctx);
2977            }
2978
2979            let make_diffs = move |mut rows: Box<dyn RowIterator>|
2980                  -> Result<(Vec<(Row, Diff)>, u64), AdapterError> {
2981                    let arena = RowArena::new();
2982                    let mut diffs = Vec::new();
2983                    let mut datum_vec = mz_repr::DatumVec::new();
2984
2985                    while let Some(row) = rows.next() {
2986                        if !assignments.is_empty() {
2987                            assert!(
2988                                matches!(kind, MutationKind::Update),
2989                                "only updates support assignments"
2990                            );
2991                            let mut datums = datum_vec.borrow_with(row);
2992                            let mut updates = vec![];
2993                            for (idx, expr) in &assignments {
2994                                let updated = match expr.eval(&datums, &arena) {
2995                                    Ok(updated) => updated,
2996                                    Err(e) => return Err(AdapterError::Unstructured(anyhow!(e))),
2997                                };
2998                                updates.push((*idx, updated));
2999                            }
3000                            for (idx, new_value) in updates {
3001                                datums[idx] = new_value;
3002                            }
3003                            let updated = Row::pack_slice(&datums);
3004                            diffs.push((updated, Diff::ONE));
3005                        }
3006                        match kind {
3007                            // Updates and deletes always remove the
3008                            // current row. Updates will also add an
3009                            // updated value.
3010                            MutationKind::Update | MutationKind::Delete => {
3011                                diffs.push((row.to_owned(), Diff::MINUS_ONE))
3012                            }
3013                            MutationKind::Insert => diffs.push((row.to_owned(), Diff::ONE)),
3014                        }
3015                    }
3016
3017                    // Sum of all the rows' byte size, for checking if we go
3018                    // above the max_result_size threshold.
3019                    let mut byte_size: u64 = 0;
3020                    for (row, diff) in &diffs {
3021                        byte_size = byte_size.saturating_add(u64::cast_from(row.byte_len()));
3022                        if diff.is_positive() {
3023                            for (idx, datum) in row.iter().enumerate() {
3024                                desc.constraints_met(idx, &datum)?;
3025                            }
3026                        }
3027                    }
3028                    Ok((diffs, byte_size))
3029                };
3030
3031            let diffs = match peek_response {
3032                ExecuteResponse::SendingRowsStreaming {
3033                    rows: mut rows_stream,
3034                    ..
3035                } => {
3036                    let mut byte_size: u64 = 0;
3037                    let mut diffs = Vec::new();
3038                    let result = loop {
3039                        match tokio::time::timeout(timeout_dur, rows_stream.next()).await {
3040                            Ok(Some(res)) => match res {
3041                                PeekResponseUnary::Rows(new_rows) => {
3042                                    match make_diffs(new_rows) {
3043                                        Ok((mut new_diffs, new_byte_size)) => {
3044                                            byte_size = byte_size.saturating_add(new_byte_size);
3045                                            if byte_size > max_result_size {
3046                                                break Err(AdapterError::ResultSize(format!(
3047                                                    "result exceeds max size of {max_result_size}"
3048                                                )));
3049                                            }
3050                                            diffs.append(&mut new_diffs)
3051                                        }
3052                                        Err(e) => break Err(e),
3053                                    };
3054                                }
3055                                PeekResponseUnary::Canceled => break Err(AdapterError::Canceled),
3056                                PeekResponseUnary::Error(e) => break Err(e),
3057                                PeekResponseUnary::DependencyDropped(dep) => {
3058                                    break Err(dep.to_concurrent_dependency_drop());
3059                                }
3060                            },
3061                            Ok(None) => break Ok(diffs),
3062                            Err(_) => {
3063                                // We timed out, so remove the pending peek. This is
3064                                // best-effort and doesn't guarantee we won't
3065                                // receive a response.
3066                                // It is not an error for this timeout to occur after `internal_cmd_rx` has been dropped.
3067                                let result = internal_cmd_tx.send(Message::CancelPendingPeeks {
3068                                    conn_id: ctx.session().conn_id().clone(),
3069                                });
3070                                if let Err(e) = result {
3071                                    warn!("internal_cmd_rx dropped before we could send: {:?}", e);
3072                                }
3073                                break Err(AdapterError::StatementTimeout);
3074                            }
3075                        }
3076                    };
3077
3078                    result
3079                }
3080                ExecuteResponse::SendingRowsImmediate { rows } => {
3081                    make_diffs(rows).map(|(diffs, _byte_size)| diffs)
3082                }
3083                resp => Err(AdapterError::Unstructured(anyhow!(
3084                    "unexpected peek response: {resp:?}"
3085                ))),
3086            };
3087
3088            let mut returning_rows = Vec::new();
3089            let mut diff_err: Option<AdapterError> = None;
3090            if let (false, Ok(diffs)) = (returning.is_empty(), &diffs) {
3091                let arena = RowArena::new();
3092                for (row, diff) in diffs {
3093                    if !diff.is_positive() {
3094                        continue;
3095                    }
3096                    let mut returning_row = Row::with_capacity(returning.len());
3097                    let mut packer = returning_row.packer();
3098                    for expr in &returning {
3099                        let datums: Vec<_> = row.iter().collect();
3100                        match expr.eval(&datums, &arena) {
3101                            Ok(datum) => {
3102                                packer.push(datum);
3103                            }
3104                            Err(err) => {
3105                                diff_err = Some(err.into());
3106                                break;
3107                            }
3108                        }
3109                    }
3110                    let diff = NonZeroI64::try_from(diff.into_inner()).expect("known to be >= 1");
3111                    let diff = match NonZeroUsize::try_from(diff) {
3112                        Ok(diff) => diff,
3113                        Err(err) => {
3114                            diff_err = Some(err.into());
3115                            break;
3116                        }
3117                    };
3118                    returning_rows.push((returning_row, diff));
3119                    if diff_err.is_some() {
3120                        break;
3121                    }
3122                }
3123            }
3124            let diffs = if let Some(err) = diff_err {
3125                Err(err)
3126            } else {
3127                diffs
3128            };
3129
3130            // We need to clear out the timestamp context so the write doesn't fail due to a
3131            // read only transaction.
3132            let timestamp_context = ctx.session_mut().take_transaction_timestamp_context();
3133            // No matter what isolation level the client is using, we must linearize this
3134            // read. The write will be performed right after this, as part of a single
3135            // transaction, so the write must have a timestamp greater than or equal to the
3136            // read.
3137            //
3138            // Note: It's only OK for the write to have a greater timestamp than the read
3139            // because the write lock prevents any other writes from happening in between
3140            // the read and write.
3141            if let Some(timestamp_context) = timestamp_context {
3142                let (tx, rx) = tokio::sync::oneshot::channel();
3143                let conn_id = ctx.session().conn_id().clone();
3144                let pending_read_txn = PendingReadTxn {
3145                    txn: PendingRead::ReadThenWrite { ctx, tx },
3146                    timestamp_context,
3147                    created: Instant::now(),
3148                    num_requeues: 0,
3149                    otel_ctx: OpenTelemetryContext::obtain(),
3150                };
3151                let result = strict_serializable_reads_tx.send((conn_id, pending_read_txn));
3152                // It is not an error for these results to be ready after `strict_serializable_reads_rx` has been dropped.
3153                if let Err(e) = result {
3154                    warn!(
3155                        "strict_serializable_reads_tx dropped before we could send: {:?}",
3156                        e
3157                    );
3158                    return;
3159                }
3160                let result = rx.await;
3161                // It is not an error for these results to be ready after `tx` has been dropped.
3162                ctx = match result {
3163                    Ok(Some(ctx)) => ctx,
3164                    Ok(None) => {
3165                        // Coordinator took our context and will handle responding to the client.
3166                        // This usually indicates that our transaction was aborted.
3167                        return;
3168                    }
3169                    Err(e) => {
3170                        warn!(
3171                            "tx used to linearize read in read then write transaction dropped before we could send: {:?}",
3172                            e
3173                        );
3174                        return;
3175                    }
3176                };
3177            }
3178
3179            match diffs {
3180                Ok(diffs) => {
3181                    let result = Self::send_diffs(
3182                        ctx.session_mut(),
3183                        plan::SendDiffsPlan {
3184                            id,
3185                            updates: diffs,
3186                            kind,
3187                            returning: returning_rows,
3188                            max_result_size,
3189                        },
3190                    );
3191                    ctx.retire(result);
3192                }
3193                Err(e) => {
3194                    ctx.retire(Err(e));
3195                }
3196            }
3197        });
3198    }
3199
3200    #[instrument]
3201    pub(super) async fn sequence_alter_item_rename(
3202        &mut self,
3203        ctx: &mut ExecuteContext,
3204        plan: plan::AlterItemRenamePlan,
3205    ) -> Result<ExecuteResponse, AdapterError> {
3206        let op = catalog::Op::RenameItem {
3207            id: plan.id,
3208            current_full_name: plan.current_full_name,
3209            to_name: plan.to_name,
3210        };
3211        match self
3212            .catalog_transact_with_ddl_transaction(ctx, vec![op], |_, _| Box::pin(async {}))
3213            .await
3214        {
3215            Ok(()) => Ok(ExecuteResponse::AlteredObject(plan.object_type)),
3216            Err(err) => Err(err),
3217        }
3218    }
3219
3220    #[instrument]
3221    pub(super) async fn sequence_alter_retain_history(
3222        &mut self,
3223        ctx: &mut ExecuteContext,
3224        plan: plan::AlterRetainHistoryPlan,
3225    ) -> Result<ExecuteResponse, AdapterError> {
3226        let ops = vec![catalog::Op::AlterRetainHistory {
3227            id: plan.id,
3228            value: plan.value,
3229            window: plan.window,
3230        }];
3231        self.catalog_transact_with_context(None, Some(ctx), ops)
3232            .await?;
3233        Ok(ExecuteResponse::AlteredObject(plan.object_type))
3234    }
3235
3236    #[instrument]
3237    pub(super) async fn sequence_alter_source_timestamp_interval(
3238        &mut self,
3239        ctx: &mut ExecuteContext,
3240        plan: plan::AlterSourceTimestampIntervalPlan,
3241    ) -> Result<ExecuteResponse, AdapterError> {
3242        let ops = vec![catalog::Op::AlterSourceTimestampInterval {
3243            id: plan.id,
3244            value: plan.value,
3245            interval: plan.interval,
3246        }];
3247        self.catalog_transact_with_context(None, Some(ctx), ops)
3248            .await?;
3249        Ok(ExecuteResponse::AlteredObject(ObjectType::Source))
3250    }
3251
3252    #[instrument]
3253    pub(super) async fn sequence_alter_schema_rename(
3254        &mut self,
3255        ctx: &mut ExecuteContext,
3256        plan: plan::AlterSchemaRenamePlan,
3257    ) -> Result<ExecuteResponse, AdapterError> {
3258        let (database_spec, schema_spec) = plan.cur_schema_spec;
3259        let op = catalog::Op::RenameSchema {
3260            database_spec,
3261            schema_spec,
3262            new_name: plan.new_schema_name,
3263            check_reserved_names: true,
3264        };
3265        match self
3266            .catalog_transact_with_ddl_transaction(ctx, vec![op], |_, _| Box::pin(async {}))
3267            .await
3268        {
3269            Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Schema)),
3270            Err(err) => Err(err),
3271        }
3272    }
3273
3274    #[instrument]
3275    pub(super) async fn sequence_alter_schema_swap(
3276        &mut self,
3277        ctx: &mut ExecuteContext,
3278        plan: plan::AlterSchemaSwapPlan,
3279    ) -> Result<ExecuteResponse, AdapterError> {
3280        let plan::AlterSchemaSwapPlan {
3281            schema_a_spec: (schema_a_db, schema_a),
3282            schema_a_name,
3283            schema_b_spec: (schema_b_db, schema_b),
3284            schema_b_name,
3285            name_temp,
3286        } = plan;
3287
3288        let op_a = catalog::Op::RenameSchema {
3289            database_spec: schema_a_db,
3290            schema_spec: schema_a,
3291            new_name: name_temp,
3292            check_reserved_names: false,
3293        };
3294        let op_b = catalog::Op::RenameSchema {
3295            database_spec: schema_b_db,
3296            schema_spec: schema_b,
3297            new_name: schema_a_name,
3298            check_reserved_names: false,
3299        };
3300        let op_c = catalog::Op::RenameSchema {
3301            database_spec: schema_a_db,
3302            schema_spec: schema_a,
3303            new_name: schema_b_name,
3304            check_reserved_names: false,
3305        };
3306
3307        match self
3308            .catalog_transact_with_ddl_transaction(ctx, vec![op_a, op_b, op_c], |_, _| {
3309                Box::pin(async {})
3310            })
3311            .await
3312        {
3313            Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Schema)),
3314            Err(err) => Err(err),
3315        }
3316    }
3317
3318    #[instrument]
3319    pub(super) async fn sequence_alter_role(
3320        &mut self,
3321        session: &Session,
3322        plan::AlterRolePlan { id, name, option }: plan::AlterRolePlan,
3323    ) -> Result<ExecuteResponse, AdapterError> {
3324        let catalog = self.catalog().for_session(session);
3325        let role = catalog.get_role(&id);
3326
3327        // We'll send these notices to the user, if the operation is successful.
3328        let mut notices = vec![];
3329
3330        // Get the attributes and variables from the role, as they currently are.
3331        let mut attributes: RoleAttributesRaw = role.attributes().clone().into();
3332        let mut vars = role.vars().clone();
3333
3334        // Whether to set the password to NULL. This is a special case since the existing
3335        // password is not stored in the role attributes.
3336        let mut nopassword = false;
3337
3338        // Apply our updates.
3339        match option {
3340            PlannedAlterRoleOption::Attributes(attrs) => {
3341                self.validate_role_attributes(&attrs.clone().into())?;
3342
3343                if let Some(inherit) = attrs.inherit {
3344                    attributes.inherit = inherit;
3345                }
3346
3347                if let Some(password) = attrs.password {
3348                    attributes.password = Some(password);
3349                    attributes.scram_iterations =
3350                        Some(self.catalog().system_config().scram_iterations())
3351                }
3352
3353                if let Some(superuser) = attrs.superuser {
3354                    attributes.superuser = Some(superuser);
3355                }
3356
3357                if let Some(login) = attrs.login {
3358                    attributes.login = Some(login);
3359                }
3360
3361                if attrs.nopassword.unwrap_or(false) {
3362                    nopassword = true;
3363                }
3364
3365                if let Some(notice) = self.should_emit_rbac_notice(session) {
3366                    notices.push(notice);
3367                }
3368            }
3369            PlannedAlterRoleOption::Variable(variable) => {
3370                // Get the variable to make sure it's valid and visible.
3371                let session_var = session.vars().inspect(variable.name())?;
3372                // Return early if it's not visible.
3373                session_var.visible(session.user(), catalog.system_vars())?;
3374
3375                // Emit a warning when deprecated variables are used.
3376                // TODO(database-issues#8069) remove this after sufficient time has passed
3377                if variable.name() == vars::OLD_AUTO_ROUTE_CATALOG_QUERIES {
3378                    notices.push(AdapterNotice::AutoRouteIntrospectionQueriesUsage);
3379                } else if let PlannedRoleVariable::Set {
3380                    name,
3381                    value: VariableValue::Values(vals),
3382                } = &variable
3383                {
3384                    if name == vars::CLUSTER.name() && vals[0] == vars::OLD_CATALOG_SERVER_CLUSTER {
3385                        notices.push(AdapterNotice::IntrospectionClusterUsage);
3386                    }
3387                }
3388
3389                let var_name = match variable {
3390                    PlannedRoleVariable::Set { name, value } => {
3391                        // Update our persisted set.
3392                        match &value {
3393                            VariableValue::Default => {
3394                                vars.remove(&name);
3395                            }
3396                            VariableValue::Values(vals) => {
3397                                let var = match &vals[..] {
3398                                    [val] => OwnedVarInput::Flat(val.clone()),
3399                                    vals => OwnedVarInput::SqlSet(vals.to_vec()),
3400                                };
3401                                // Make sure the input is valid.
3402                                session_var.check(var.borrow())?;
3403
3404                                vars.insert(name.clone(), var);
3405                            }
3406                        };
3407                        name
3408                    }
3409                    PlannedRoleVariable::Reset { name } => {
3410                        // Remove it from our persisted values.
3411                        vars.remove(&name);
3412                        name
3413                    }
3414                };
3415
3416                // Emit a notice that they need to reconnect to see the change take effect.
3417                notices.push(AdapterNotice::VarDefaultUpdated {
3418                    role: Some(name.clone()),
3419                    var_name: Some(var_name),
3420                });
3421            }
3422        }
3423
3424        let op = catalog::Op::AlterRole {
3425            id,
3426            name,
3427            attributes,
3428            nopassword,
3429            vars: RoleVars { map: vars },
3430        };
3431        let response = self
3432            .catalog_transact(Some(session), vec![op])
3433            .await
3434            .map(|_| ExecuteResponse::AlteredRole)?;
3435
3436        // Send all of our queued notices.
3437        session.add_notices(notices);
3438
3439        Ok(response)
3440    }
3441
3442    #[instrument]
3443    pub(super) async fn sequence_alter_sink_prepare(
3444        &mut self,
3445        ctx: ExecuteContext,
3446        plan: plan::AlterSinkPlan,
3447    ) {
3448        // Put a read hold on the new relation
3449        let id_bundle = crate::CollectionIdBundle {
3450            storage_ids: BTreeSet::from_iter([plan.sink.from]),
3451            compute_ids: BTreeMap::new(),
3452        };
3453        let read_hold = self.acquire_read_holds(&id_bundle);
3454
3455        let Some(read_ts) = read_hold.least_valid_read().into_option() else {
3456            ctx.retire(Err(AdapterError::UnreadableSinkCollection));
3457            return;
3458        };
3459
3460        let otel_ctx = OpenTelemetryContext::obtain();
3461        let from_item_id = self.catalog().resolve_item_id(&plan.sink.from);
3462
3463        let plan_validity = PlanValidity::new(
3464            self.catalog(),
3465            BTreeSet::from_iter([plan.item_id, from_item_id]),
3466            Some(plan.in_cluster),
3467            None,
3468            ctx.session().role_metadata().clone(),
3469        );
3470
3471        info!(
3472            "preparing alter sink for {}: frontiers={:?} export={:?}",
3473            plan.global_id,
3474            self.controller
3475                .storage_collections
3476                .collections_frontiers(vec![plan.global_id, plan.sink.from]),
3477            self.controller.storage.export(plan.global_id)
3478        );
3479
3480        // Now we must wait for the sink to make enough progress such that there is overlap between
3481        // the new `from` collection's read hold and the sink's write frontier.
3482        //
3483        // TODO(database-issues#9820): If the sink is dropped while we are waiting for progress,
3484        // the watch set never completes and neither does the `ALTER SINK` command.
3485        self.install_storage_watch_set(
3486            ctx.session().conn_id().clone(),
3487            BTreeSet::from_iter([plan.global_id]),
3488            read_ts,
3489            WatchSetResponse::AlterSinkReady(AlterSinkReadyContext {
3490                ctx: Some(ctx),
3491                otel_ctx,
3492                plan,
3493                plan_validity,
3494                read_hold,
3495            }),
3496        ).expect("plan validity verified above; we are on the coordinator main task, so they couldn't have gone away since then");
3497    }
3498
3499    #[instrument]
3500    pub async fn sequence_alter_sink_finish(&mut self, mut ctx: AlterSinkReadyContext) {
3501        ctx.otel_ctx.attach_as_parent();
3502
3503        let plan::AlterSinkPlan {
3504            item_id,
3505            global_id,
3506            sink: sink_plan,
3507            with_snapshot,
3508            in_cluster,
3509            set_options,
3510            reset_options,
3511        } = ctx.plan.clone();
3512
3513        // We avoid taking the DDL lock for `ALTER SINK` commands, see
3514        // `Coordinator::must_serialize_ddl`. We therefore must assume that the world has
3515        // arbitrarily changed since we performed planning, and we must re-assert that it still
3516        // matches our requirements.
3517        //
3518        // The `PlanValidity` check ensures that both the sink and the new source relation still
3519        // exist. Apart from that we have to ensure that nobody else altered the sink in the mean
3520        // time, which we do by comparing the catalog sink version to the one in the plan.
3521        match ctx.plan_validity.check(self.catalog()) {
3522            Ok(()) => {}
3523            Err(err) => {
3524                ctx.retire(Err(err));
3525                return;
3526            }
3527        }
3528
3529        let entry = self.catalog().get_entry(&item_id);
3530        let CatalogItem::Sink(old_sink) = entry.item() else {
3531            panic!("invalid item kind for `AlterSinkPlan`");
3532        };
3533
3534        if sink_plan.version != old_sink.version + 1 {
3535            ctx.retire(Err(AdapterError::ChangedPlan(
3536                "sink was altered concurrently".into(),
3537            )));
3538            return;
3539        }
3540
3541        info!(
3542            "finishing alter sink for {global_id}: frontiers={:?} export={:?}",
3543            self.controller
3544                .storage_collections
3545                .collections_frontiers(vec![global_id, sink_plan.from]),
3546            self.controller.storage.export(global_id),
3547        );
3548
3549        // Assert that we can recover the updates that happened at the timestamps of the write
3550        // frontier. This must be true in this call.
3551        let write_frontier = &self
3552            .controller
3553            .storage
3554            .export(global_id)
3555            .expect("sink known to exist")
3556            .write_frontier;
3557        let as_of = ctx.read_hold.least_valid_read();
3558        assert!(
3559            write_frontier.iter().all(|t| as_of.less_than(t)),
3560            "{:?} should be strictly less than {:?}",
3561            &*as_of,
3562            &**write_frontier
3563        );
3564
3565        // Parse the `create_sql` so we can update it to the new sink definition.
3566        //
3567        // Note that we need to use the `create_sql` from the catalog here, not the one from the
3568        // sink plan. Even though we ensure that the sink version didn't change since planning, the
3569        // names in the `create_sql` may have changed, for example due to a schema swap.
3570        let create_sql = &old_sink.create_sql;
3571        let parsed = mz_sql::parse::parse(create_sql).expect("valid create_sql");
3572        let Statement::CreateSink(mut stmt) = parsed.into_element().ast else {
3573            unreachable!("invalid statement kind for sink");
3574        };
3575
3576        // Update the sink version.
3577        plan::apply_sink_option_edits(
3578            &mut stmt.with_options,
3579            &[CreateSinkOption {
3580                name: CreateSinkOptionName::Version,
3581                value: Some(WithOptionValue::Value(mz_sql::ast::Value::Number(
3582                    sink_plan.version.to_string(),
3583                ))),
3584            }],
3585            &[],
3586        );
3587
3588        let conn_catalog = self.catalog().for_system_session();
3589        let (mut stmt, resolved_ids) =
3590            mz_sql::names::resolve(&conn_catalog, stmt).expect("resolvable create_sql");
3591
3592        // Re-apply the option edits requested by the `ALTER SINK`.
3593        plan::apply_sink_option_edits(&mut stmt.with_options, &set_options, &reset_options);
3594
3595        // Update the `from` relation.
3596        let from_entry = self.catalog().get_entry_by_global_id(&sink_plan.from);
3597        let full_name = self.catalog().resolve_full_name(from_entry.name(), None);
3598        stmt.from = ResolvedItemName::Item {
3599            id: from_entry.id(),
3600            qualifiers: from_entry.name.qualifiers.clone(),
3601            full_name,
3602            print_id: true,
3603            version: from_entry.version,
3604        };
3605
3606        // `resolved_ids` was derived from the old `create_sql`, so it still
3607        // references the old input. `create_sql` and `from` above already
3608        // point at the new input, so sync the dependency set to match.
3609        // Otherwise the in-memory catalog disagrees with `create_sql` until
3610        // the next reload, and the temporary-dependency check in
3611        // `Op::UpdateItem` (which reads `uses()`) would not see the new input.
3612        let mut resolved_ids = resolved_ids;
3613        resolved_ids.remove_item(&self.catalog().resolve_item_id(&old_sink.from));
3614        resolved_ids.add_item(from_entry.id());
3615
3616        let new_sink = Sink {
3617            create_sql: stmt.to_ast_string_stable(),
3618            global_id,
3619            from: sink_plan.from,
3620            connection: sink_plan.connection.clone(),
3621            envelope: sink_plan.envelope,
3622            version: sink_plan.version,
3623            with_snapshot,
3624            resolved_ids,
3625            cluster_id: in_cluster,
3626            commit_interval: sink_plan.commit_interval,
3627        };
3628
3629        let ops = vec![catalog::Op::UpdateItem {
3630            id: item_id,
3631            name: entry.name().clone(),
3632            to_item: CatalogItem::Sink(new_sink),
3633        }];
3634
3635        match self
3636            .catalog_transact(Some(ctx.ctx().session_mut()), ops)
3637            .await
3638        {
3639            Ok(()) => {}
3640            Err(err) => {
3641                ctx.retire(Err(err));
3642                return;
3643            }
3644        }
3645
3646        let storage_sink_desc = StorageSinkDesc {
3647            from: sink_plan.from,
3648            from_desc: from_entry
3649                .relation_desc()
3650                .expect("sinks can only be built on items with descs")
3651                .into_owned(),
3652            connection: sink_plan
3653                .connection
3654                .clone()
3655                .into_inline_connection(self.catalog().state()),
3656            envelope: sink_plan.envelope,
3657            as_of,
3658            with_snapshot,
3659            version: sink_plan.version,
3660            from_storage_metadata: (),
3661            to_storage_metadata: (),
3662            commit_interval: sink_plan.commit_interval,
3663        };
3664
3665        self.controller
3666            .storage
3667            .alter_export(
3668                global_id,
3669                ExportDescription {
3670                    sink: storage_sink_desc,
3671                    instance_id: in_cluster,
3672                },
3673            )
3674            .await
3675            .unwrap_or_terminate("cannot fail to alter source desc");
3676
3677        ctx.retire(Ok(ExecuteResponse::AlteredObject(ObjectType::Sink)));
3678    }
3679
3680    #[instrument]
3681    pub(super) async fn sequence_alter_connection(
3682        &mut self,
3683        ctx: ExecuteContext,
3684        AlterConnectionPlan { id, action }: AlterConnectionPlan,
3685    ) {
3686        match action {
3687            AlterConnectionAction::RotateKeys => {
3688                self.sequence_rotate_keys(ctx, id).await;
3689            }
3690            AlterConnectionAction::AlterOptions {
3691                set_options,
3692                drop_options,
3693                validate,
3694            } => {
3695                self.sequence_alter_connection_options(ctx, id, set_options, drop_options, validate)
3696                    .await
3697            }
3698        }
3699    }
3700
3701    #[instrument]
3702    async fn sequence_alter_connection_options(
3703        &mut self,
3704        mut ctx: ExecuteContext,
3705        id: CatalogItemId,
3706        set_options: BTreeMap<ConnectionOptionName, Option<WithOptionValue<mz_sql::names::Aug>>>,
3707        drop_options: BTreeSet<ConnectionOptionName>,
3708        validate: bool,
3709    ) {
3710        let cur_entry = self.catalog().get_entry(&id);
3711        let cur_conn = cur_entry.connection().expect("known to be connection");
3712        let connection_gid = cur_conn.global_id();
3713
3714        let inner = || -> Result<Connection, AdapterError> {
3715            // Parse statement.
3716            let create_conn_stmt = match mz_sql::parse::parse(&cur_conn.create_sql)
3717                .expect("invalid create sql persisted to catalog")
3718                .into_element()
3719                .ast
3720            {
3721                Statement::CreateConnection(stmt) => stmt,
3722                _ => unreachable!("proved type is source"),
3723            };
3724
3725            let catalog = self.catalog().for_system_session();
3726
3727            // Resolve items in statement
3728            let (mut create_conn_stmt, resolved_ids) =
3729                mz_sql::names::resolve(&catalog, create_conn_stmt)
3730                    .map_err(|e| AdapterError::internal("ALTER CONNECTION", e))?;
3731
3732            // Retain options that are neither set nor dropped.
3733            create_conn_stmt
3734                .values
3735                .retain(|o| !set_options.contains_key(&o.name) && !drop_options.contains(&o.name));
3736
3737            // Set new values
3738            create_conn_stmt.values.extend(
3739                set_options
3740                    .into_iter()
3741                    .map(|(name, value)| ConnectionOption { name, value }),
3742            );
3743
3744            // Open a new catalog, which we will use to re-plan our
3745            // statement with the desired config.
3746            let mut catalog = self.catalog().for_system_session();
3747            catalog.mark_id_unresolvable_for_replanning(id);
3748
3749            // Re-define our source in terms of the amended statement
3750            let plan = match mz_sql::plan::plan(
3751                None,
3752                &catalog,
3753                Statement::CreateConnection(create_conn_stmt),
3754                &Params::empty(),
3755                &resolved_ids,
3756            )
3757            .map_err(|e| AdapterError::InvalidAlter("CONNECTION", e))?
3758            {
3759                (Plan::CreateConnection(plan), _sql_impl_ids) => plan,
3760                (p, _) => {
3761                    unreachable!("create connection plan is only valid response, got {:?}", p)
3762                }
3763            };
3764
3765            // Parse statement.
3766            let create_conn_stmt = match mz_sql::parse::parse(&plan.connection.create_sql)
3767                .expect("invalid create sql persisted to catalog")
3768                .into_element()
3769                .ast
3770            {
3771                Statement::CreateConnection(stmt) => stmt,
3772                _ => unreachable!("proved type is source"),
3773            };
3774
3775            let catalog = self.catalog().for_system_session();
3776
3777            // Resolve items in statement
3778            let (_, new_deps) = mz_sql::names::resolve(&catalog, create_conn_stmt)
3779                .map_err(|e| AdapterError::internal("ALTER CONNECTION", e))?;
3780
3781            Ok(Connection {
3782                create_sql: plan.connection.create_sql,
3783                global_id: cur_conn.global_id,
3784                details: plan.connection.details,
3785                resolved_ids: new_deps,
3786            })
3787        };
3788
3789        let conn = match inner() {
3790            Ok(conn) => conn,
3791            Err(e) => {
3792                return ctx.retire(Err(e));
3793            }
3794        };
3795
3796        // Replanning uses a system session and discovers retained dependencies
3797        // that `check_plan` could not authorize. Check them before secret guards
3798        // or validation can resolve a secret.
3799        let usage_check = {
3800            let catalog = self.catalog().for_session(ctx.session());
3801            rbac::check_usage(
3802                &catalog,
3803                ctx.session(),
3804                &conn.resolved_ids,
3805                &rbac::CREATE_ITEM_USAGE,
3806            )
3807        };
3808        if let Err(err) = usage_check {
3809            return ctx.retire(Err(err.into()));
3810        }
3811
3812        // `conn` is the whole re-planned connection, so this also rejects a
3813        // stored value the statement did not touch.
3814        if let Err(err) = check_connection_details(&conn.details) {
3815            return ctx.retire(Err(AdapterError::InvalidAlter("CONNECTION", err)));
3816        }
3817
3818        // Inspect guarded secrets whether or not validation was requested,
3819        // before the altered connection is installed in the catalog.
3820        if let Err(err) = self
3821            .check_connection_secret_content_guards(&conn.details)
3822            .await
3823        {
3824            return ctx.retire(Err(err));
3825        }
3826
3827        if validate {
3828            let connection = conn
3829                .details
3830                .to_connection()
3831                .into_inline_connection(self.catalog().state());
3832
3833            let internal_cmd_tx = self.internal_cmd_tx.clone();
3834            let catalog = self.owned_catalog();
3835            let conn_id = ctx.session().conn_id().clone();
3836            let otel_ctx = OpenTelemetryContext::obtain();
3837            let role_metadata = ctx.session().role_metadata().clone();
3838            let current_storage_parameters = self.controller.storage.config().clone();
3839
3840            task::spawn(
3841                || format!("validate_alter_connection:{conn_id}"),
3842                async move {
3843                    let resolved_ids = conn.resolved_ids.clone();
3844                    let dependency_ids: BTreeSet<_> = resolved_ids.items().copied().collect();
3845                    let result = match std::panic::AssertUnwindSafe(
3846                        connection.validate(id, &current_storage_parameters),
3847                    )
3848                    .ore_catch_unwind()
3849                    .await
3850                    {
3851                        Ok(Ok(())) => Ok(conn),
3852                        Ok(Err(err)) => Err(err.into()),
3853                        Err(_panic) => {
3854                            tracing::error!("alter connection validation panicked");
3855                            Err(AdapterError::Internal(
3856                                "connection validation panicked".into(),
3857                            ))
3858                        }
3859                    };
3860
3861                    // It is not an error for validation to complete after `internal_cmd_rx` is dropped.
3862                    let result = internal_cmd_tx.send(Message::AlterConnectionValidationReady(
3863                        AlterConnectionValidationReady {
3864                            ctx,
3865                            result,
3866                            connection_id: id,
3867                            connection_gid,
3868                            plan_validity: PlanValidity::new(
3869                                &catalog,
3870                                dependency_ids.clone(),
3871                                None,
3872                                None,
3873                                role_metadata,
3874                            ),
3875                            otel_ctx,
3876                            resolved_ids,
3877                        },
3878                    ));
3879                    if let Err(e) = result {
3880                        tracing::warn!("internal_cmd_rx dropped before we could send: {:?}", e);
3881                    }
3882                },
3883            );
3884        } else {
3885            let result = self
3886                .sequence_alter_connection_stage_finish(ctx.session_mut(), id, conn)
3887                .await;
3888            ctx.retire(result);
3889        }
3890    }
3891
3892    #[instrument]
3893    pub(crate) async fn sequence_alter_connection_stage_finish(
3894        &mut self,
3895        session: &Session,
3896        id: CatalogItemId,
3897        connection: Connection,
3898    ) -> Result<ExecuteResponse, AdapterError> {
3899        match self.catalog.get_entry(&id).item() {
3900            CatalogItem::Connection(curr_conn) => {
3901                curr_conn
3902                    .details
3903                    .to_connection()
3904                    .alter_compatible(curr_conn.global_id, &connection.details.to_connection())
3905                    .map_err(StorageError::from)?;
3906            }
3907            _ => unreachable!("known to be a connection"),
3908        };
3909
3910        let ops = vec![catalog::Op::UpdateItem {
3911            id,
3912            name: self.catalog.get_entry(&id).name().clone(),
3913            to_item: CatalogItem::Connection(connection.clone()),
3914        }];
3915
3916        self.catalog_transact(Some(session), ops).await?;
3917
3918        // NOTE: The rest of the alter connection logic (updating VPC endpoints
3919        // and propagating connection changes to dependent sources, sinks, and
3920        // tables) is handled in `apply_catalog_implications` via
3921        // `handle_alter_connection`. The catalog transact above triggers that
3922        // code path.
3923
3924        Ok(ExecuteResponse::AlteredObject(ObjectType::Connection))
3925    }
3926
3927    #[instrument]
3928    pub(super) async fn sequence_alter_source(
3929        &mut self,
3930        session: &Session,
3931        plan::AlterSourcePlan {
3932            item_id,
3933            ingestion_id,
3934            action,
3935        }: plan::AlterSourcePlan,
3936    ) -> Result<ExecuteResponse, AdapterError> {
3937        let cur_entry = self.catalog().get_entry(&item_id);
3938        let cur_source = cur_entry.source().expect("known to be source");
3939
3940        let create_sql_to_stmt_deps = |coord: &Coordinator, err_cx, create_source_sql| {
3941            // Parse statement.
3942            let create_source_stmt = match mz_sql::parse::parse(create_source_sql)
3943                .expect("invalid create sql persisted to catalog")
3944                .into_element()
3945                .ast
3946            {
3947                Statement::CreateSource(stmt) => stmt,
3948                _ => unreachable!("proved type is source"),
3949            };
3950
3951            let catalog = coord.catalog().for_system_session();
3952
3953            // Resolve items in statement
3954            mz_sql::names::resolve(&catalog, create_source_stmt)
3955                .map_err(|e| AdapterError::internal(err_cx, e))
3956        };
3957
3958        match action {
3959            plan::AlterSourceAction::AddSubsourceExports {
3960                subsources,
3961                options,
3962            } => {
3963                const ALTER_SOURCE: &str = "ALTER SOURCE...ADD SUBSOURCES";
3964
3965                let mz_sql::plan::AlterSourceAddSubsourceOptionExtracted {
3966                    text_columns: mut new_text_columns,
3967                    exclude_columns: mut new_exclude_columns,
3968                    ..
3969                } = options.try_into()?;
3970
3971                // Resolve items in statement
3972                let (mut create_source_stmt, resolved_ids) =
3973                    create_sql_to_stmt_deps(self, ALTER_SOURCE, cur_entry.create_sql())?;
3974
3975                // Get all currently referred-to items
3976                let catalog = self.catalog();
3977                let curr_references: BTreeSet<_> = catalog
3978                    .get_entry(&item_id)
3979                    .used_by()
3980                    .into_iter()
3981                    .filter_map(|subsource| {
3982                        catalog
3983                            .get_entry(subsource)
3984                            .subsource_details()
3985                            .map(|(_id, reference, _details)| reference)
3986                    })
3987                    .collect();
3988
3989                // We are doing a lot of unwrapping, so just make an error to reference; all of
3990                // these invariants are guaranteed to be true because of how we plan subsources.
3991                let purification_err =
3992                    || AdapterError::internal(ALTER_SOURCE, "error in subsource purification");
3993
3994                // TODO(roshan): Remove all the text-column/ignore-column option merging here once
3995                // we remove support for implicitly created subsources from a `CREATE SOURCE`
3996                // statement.
3997                match &mut create_source_stmt.connection {
3998                    CreateSourceConnection::Postgres {
3999                        options: curr_options,
4000                        ..
4001                    } => {
4002                        let mz_sql::plan::PgConfigOptionExtracted {
4003                            mut text_columns, ..
4004                        } = curr_options.clone().try_into()?;
4005
4006                        // Drop text columns; we will add them back in
4007                        // as appropriate below.
4008                        curr_options.retain(|o| !matches!(o.name, PgConfigOptionName::TextColumns));
4009
4010                        // Drop all text columns that are not currently referred to.
4011                        text_columns.retain(|column_qualified_reference| {
4012                            mz_ore::soft_assert_eq_or_log!(
4013                                column_qualified_reference.0.len(),
4014                                4,
4015                                "all TEXT COLUMNS values must be column-qualified references"
4016                            );
4017                            let mut table = column_qualified_reference.clone();
4018                            table.0.truncate(3);
4019                            curr_references.contains(&table)
4020                        });
4021
4022                        // Merge the current text columns into the new text columns.
4023                        new_text_columns.extend(text_columns);
4024
4025                        // If we have text columns, add them to the options.
4026                        if !new_text_columns.is_empty() {
4027                            new_text_columns.sort();
4028                            let new_text_columns = new_text_columns
4029                                .into_iter()
4030                                .map(WithOptionValue::UnresolvedItemName)
4031                                .collect();
4032
4033                            curr_options.push(PgConfigOption {
4034                                name: PgConfigOptionName::TextColumns,
4035                                value: Some(WithOptionValue::Sequence(new_text_columns)),
4036                            });
4037                        }
4038                    }
4039                    CreateSourceConnection::MySql {
4040                        options: curr_options,
4041                        ..
4042                    } => {
4043                        let mz_sql::plan::MySqlConfigOptionExtracted {
4044                            mut text_columns,
4045                            mut exclude_columns,
4046                            ..
4047                        } = curr_options.clone().try_into()?;
4048
4049                        // Drop both ignore and text columns; we will add them back in
4050                        // as appropriate below.
4051                        curr_options.retain(|o| {
4052                            !matches!(
4053                                o.name,
4054                                MySqlConfigOptionName::TextColumns
4055                                    | MySqlConfigOptionName::ExcludeColumns
4056                            )
4057                        });
4058
4059                        // Drop all text / exclude columns that are not currently referred to.
4060                        let column_referenced =
4061                            |column_qualified_reference: &UnresolvedItemName| {
4062                                mz_ore::soft_assert_eq_or_log!(
4063                                    column_qualified_reference.0.len(),
4064                                    3,
4065                                    "all TEXT COLUMNS & EXCLUDE COLUMNS values must be column-qualified references"
4066                                );
4067                                let mut table = column_qualified_reference.clone();
4068                                table.0.truncate(2);
4069                                curr_references.contains(&table)
4070                            };
4071                        text_columns.retain(column_referenced);
4072                        exclude_columns.retain(column_referenced);
4073
4074                        // Merge the current text / exclude columns into the new text / exclude columns.
4075                        new_text_columns.extend(text_columns);
4076                        new_exclude_columns.extend(exclude_columns);
4077
4078                        // If we have text columns, add them to the options.
4079                        if !new_text_columns.is_empty() {
4080                            new_text_columns.sort();
4081                            let new_text_columns = new_text_columns
4082                                .into_iter()
4083                                .map(WithOptionValue::UnresolvedItemName)
4084                                .collect();
4085
4086                            curr_options.push(MySqlConfigOption {
4087                                name: MySqlConfigOptionName::TextColumns,
4088                                value: Some(WithOptionValue::Sequence(new_text_columns)),
4089                            });
4090                        }
4091                        // If we have exclude columns, add them to the options.
4092                        if !new_exclude_columns.is_empty() {
4093                            new_exclude_columns.sort();
4094                            let new_exclude_columns = new_exclude_columns
4095                                .into_iter()
4096                                .map(WithOptionValue::UnresolvedItemName)
4097                                .collect();
4098
4099                            curr_options.push(MySqlConfigOption {
4100                                name: MySqlConfigOptionName::ExcludeColumns,
4101                                value: Some(WithOptionValue::Sequence(new_exclude_columns)),
4102                            });
4103                        }
4104                    }
4105                    CreateSourceConnection::SqlServer {
4106                        options: curr_options,
4107                        ..
4108                    } => {
4109                        let mz_sql::plan::SqlServerConfigOptionExtracted {
4110                            mut text_columns,
4111                            mut exclude_columns,
4112                            ..
4113                        } = curr_options.clone().try_into()?;
4114
4115                        // Drop both ignore and text columns; we will add them back in
4116                        // as appropriate below.
4117                        curr_options.retain(|o| {
4118                            !matches!(
4119                                o.name,
4120                                SqlServerConfigOptionName::TextColumns
4121                                    | SqlServerConfigOptionName::ExcludeColumns
4122                            )
4123                        });
4124
4125                        // Drop all text / exclude columns that are not currently referred to.
4126                        // SQL Server text/exclude column refs are 3-part (schema.table.col),
4127                        // which truncate to 2-part (schema.table). But external references
4128                        // are 3-part (database.schema.table). Use suffix matching since
4129                        // a SQL Server source connects to a single database.
4130                        let column_referenced =
4131                            |column_qualified_reference: &UnresolvedItemName| {
4132                                mz_ore::soft_assert_eq_or_log!(
4133                                    column_qualified_reference.0.len(),
4134                                    3,
4135                                    "all TEXT COLUMNS & EXCLUDE COLUMNS values must be column-qualified references"
4136                                );
4137                                let mut table = column_qualified_reference.clone();
4138                                table.0.truncate(2);
4139                                curr_references.iter().any(|r| r.0.ends_with(&table.0))
4140                            };
4141                        text_columns.retain(column_referenced);
4142                        exclude_columns.retain(column_referenced);
4143
4144                        // Merge the current text / exclude columns into the new text / exclude columns.
4145                        new_text_columns.extend(text_columns);
4146                        new_exclude_columns.extend(exclude_columns);
4147
4148                        // If we have text columns, add them to the options.
4149                        if !new_text_columns.is_empty() {
4150                            new_text_columns.sort();
4151                            let new_text_columns = new_text_columns
4152                                .into_iter()
4153                                .map(WithOptionValue::UnresolvedItemName)
4154                                .collect();
4155
4156                            curr_options.push(SqlServerConfigOption {
4157                                name: SqlServerConfigOptionName::TextColumns,
4158                                value: Some(WithOptionValue::Sequence(new_text_columns)),
4159                            });
4160                        }
4161                        // If we have exclude columns, add them to the options.
4162                        if !new_exclude_columns.is_empty() {
4163                            new_exclude_columns.sort();
4164                            let new_exclude_columns = new_exclude_columns
4165                                .into_iter()
4166                                .map(WithOptionValue::UnresolvedItemName)
4167                                .collect();
4168
4169                            curr_options.push(SqlServerConfigOption {
4170                                name: SqlServerConfigOptionName::ExcludeColumns,
4171                                value: Some(WithOptionValue::Sequence(new_exclude_columns)),
4172                            });
4173                        }
4174                    }
4175                    _ => return Err(purification_err()),
4176                };
4177
4178                let mut catalog = self.catalog().for_system_session();
4179                catalog.mark_id_unresolvable_for_replanning(cur_entry.id());
4180
4181                // Re-define our source in terms of the amended statement
4182                let planned = mz_sql::plan::plan(
4183                    None,
4184                    &catalog,
4185                    Statement::CreateSource(create_source_stmt),
4186                    &Params::empty(),
4187                    &resolved_ids,
4188                )
4189                .map_err(|e| AdapterError::internal(ALTER_SOURCE, e))?;
4190                let plan = match planned {
4191                    (Plan::CreateSource(plan), _sql_impl_ids) => plan,
4192                    (p, _) => {
4193                        unreachable!("create source plan is only valid response, got {:?}", p)
4194                    }
4195                };
4196
4197                // Asserting that we've done the right thing with dependencies
4198                // here requires mocking out objects in the catalog, which is a
4199                // large task for an operation we have to cover in tests anyway.
4200                let source = Source::new(
4201                    plan,
4202                    cur_source.global_id,
4203                    resolved_ids,
4204                    cur_source.custom_logical_compaction_window,
4205                    cur_source.is_retained_metrics_object,
4206                );
4207
4208                // Get new ingestion description for storage.
4209                let desc = match &source.data_source {
4210                    DataSourceDesc::Ingestion { desc, .. }
4211                    | DataSourceDesc::OldSyntaxIngestion { desc, .. } => {
4212                        desc.clone().into_inline_connection(self.catalog().state())
4213                    }
4214                    _ => unreachable!("already verified of type ingestion"),
4215                };
4216
4217                self.controller
4218                    .storage
4219                    .check_alter_ingestion_source_desc(ingestion_id, &desc)
4220                    .map_err(|e| AdapterError::internal(ALTER_SOURCE, e))?;
4221
4222                // Redefine source. This must be done before we create any new
4223                // subsources so that it has the right ingestion.
4224                let mut ops = vec![catalog::Op::UpdateItem {
4225                    id: item_id,
4226                    // Look this up again so we don't have to hold an immutable reference to the
4227                    // entry for so long.
4228                    name: self.catalog.get_entry(&item_id).name().clone(),
4229                    to_item: CatalogItem::Source(source),
4230                }];
4231
4232                let CreateSourceInner {
4233                    ops: new_ops,
4234                    sources: _,
4235                    if_not_exists_ids,
4236                } = self.create_source_inner(session, subsources).await?;
4237
4238                ops.extend(new_ops.into_iter());
4239
4240                assert!(
4241                    if_not_exists_ids.is_empty(),
4242                    "IF NOT EXISTS not supported for ALTER SOURCE...ADD SUBSOURCES"
4243                );
4244
4245                self.catalog_transact(Some(session), ops).await?;
4246            }
4247            plan::AlterSourceAction::RefreshReferences { references } => {
4248                self.catalog_transact(
4249                    Some(session),
4250                    vec![catalog::Op::UpdateSourceReferences {
4251                        source_id: item_id,
4252                        references: references.into(),
4253                    }],
4254                )
4255                .await?;
4256            }
4257        }
4258
4259        Ok(ExecuteResponse::AlteredObject(ObjectType::Source))
4260    }
4261
4262    #[instrument]
4263    pub(super) async fn sequence_alter_system_set(
4264        &mut self,
4265        session: &Session,
4266        plan::AlterSystemSetPlan { name, value }: plan::AlterSystemSetPlan,
4267    ) -> Result<ExecuteResponse, AdapterError> {
4268        self.is_user_allowed_to_alter_system(session, Some(&name))?;
4269        // We want to ensure that the network policy we're switching too actually exists.
4270        if NETWORK_POLICY.name.to_string().to_lowercase() == name.clone().to_lowercase() {
4271            self.validate_alter_system_network_policy(session, &value)?;
4272        }
4273
4274        let op = match value {
4275            plan::VariableValue::Values(values) => catalog::Op::UpdateSystemConfiguration {
4276                name: name.clone(),
4277                value: OwnedVarInput::SqlSet(values),
4278            },
4279            plan::VariableValue::Default => {
4280                catalog::Op::ResetSystemConfiguration { name: name.clone() }
4281            }
4282        };
4283        self.catalog_transact(Some(session), vec![op]).await?;
4284
4285        Self::notice_if_startup_only(session, &name);
4286        session.add_notice(AdapterNotice::VarDefaultUpdated {
4287            role: None,
4288            var_name: Some(name),
4289        });
4290        Ok(ExecuteResponse::AlteredSystemConfiguration)
4291    }
4292
4293    #[instrument]
4294    pub(super) async fn sequence_alter_system_reset(
4295        &mut self,
4296        session: &Session,
4297        plan::AlterSystemResetPlan { name }: plan::AlterSystemResetPlan,
4298    ) -> Result<ExecuteResponse, AdapterError> {
4299        self.is_user_allowed_to_alter_system(session, Some(&name))?;
4300        let op = catalog::Op::ResetSystemConfiguration { name: name.clone() };
4301        self.catalog_transact(Some(session), vec![op]).await?;
4302        Self::notice_if_startup_only(session, &name);
4303        session.add_notice(AdapterNotice::VarDefaultUpdated {
4304            role: None,
4305            var_name: Some(name),
4306        });
4307        Ok(ExecuteResponse::AlteredSystemConfiguration)
4308    }
4309
4310    #[instrument]
4311    pub(super) async fn sequence_alter_system_reset_all(
4312        &mut self,
4313        session: &Session,
4314        _: plan::AlterSystemResetAllPlan,
4315    ) -> Result<ExecuteResponse, AdapterError> {
4316        self.is_user_allowed_to_alter_system(session, None)?;
4317        // Which parameters `RESET ALL` changes has to be read before the
4318        // transaction applies it, afterwards they all read as their default.
4319        let startup_only_changed = self.startup_only_vars_changed_by_reset_all();
4320        let op = catalog::Op::ResetAllSystemConfiguration;
4321        self.catalog_transact(Some(session), vec![op]).await?;
4322        for name in startup_only_changed {
4323            session.add_notice(AdapterNotice::StartupOnlyVarUpdated {
4324                var_name: name.to_string(),
4325            });
4326        }
4327        session.add_notice(AdapterNotice::VarDefaultUpdated {
4328            role: None,
4329            var_name: None,
4330        });
4331        Ok(ExecuteResponse::AlteredSystemConfiguration)
4332    }
4333
4334    /// System parameters whose value `environmentd` samples once at startup.
4335    ///
4336    /// `enable_adapter_frontend_occ_read_then_write` selects between the
4337    /// lock-based and the OCC read-then-write path. Both are never live in one
4338    /// process, so the choice is fixed at boot and every session inherits it.
4339    /// `max_concurrent_occ_writes` sizes the OCC semaphore at boot.
4340    /// `enable_expression_cache` decides whether catalog open builds the cache,
4341    /// which has already happened by the time a session can ask.
4342    ///
4343    /// `ALTER SYSTEM` on one of these is allowed to go through. The catalog
4344    /// value is what the next process start reads, and the running process
4345    /// cannot observe it, so there is no window where two code paths are live at
4346    /// once.
4347    fn startup_only_vars() -> [&'static str; 3] {
4348        [
4349            FRONTEND_READ_THEN_WRITE.name(),
4350            MAX_CONCURRENT_OCC_WRITES.name(),
4351            ENABLE_EXPRESSION_CACHE.name(),
4352        ]
4353    }
4354
4355    /// Warns that `name` is only read at startup, so the running process keeps
4356    /// the value it sampled at boot.
4357    fn notice_if_startup_only(session: &Session, name: &str) {
4358        if Self::startup_only_vars()
4359            .iter()
4360            .any(|n| n.eq_ignore_ascii_case(name))
4361        {
4362            session.add_notice(AdapterNotice::StartupOnlyVarUpdated {
4363                var_name: name.to_string(),
4364            });
4365        }
4366    }
4367
4368    /// The startup-only parameters whose value `ALTER SYSTEM RESET ALL` would
4369    /// change. Parameters already at their effective default are untouched, so
4370    /// they are not reported.
4371    fn startup_only_vars_changed_by_reset_all(&self) -> Vec<&'static str> {
4372        // Value-based, unlike `notice_if_startup_only`, which warns whenever an
4373        // operator names one of these parameters. `RESET ALL` names every
4374        // parameter, so only a value that actually moves is worth a warning.
4375        let config = self.catalog().system_config();
4376        let defaults = config.defaults();
4377        Self::startup_only_vars()
4378            .into_iter()
4379            .filter(|name| {
4380                // These names are all registered system vars, a lookup failure
4381                // would mean the definitions and this list have drifted apart.
4382                let current = config
4383                    .get(name)
4384                    .expect("startup-only parameter is a registered system var")
4385                    .value();
4386                defaults
4387                    .get(*name)
4388                    .is_some_and(|default| default != &current)
4389            })
4390            .collect()
4391    }
4392
4393    // TODO(jkosh44) Move this into rbac.rs once RBAC is always on.
4394    fn is_user_allowed_to_alter_system(
4395        &self,
4396        session: &Session,
4397        var_name: Option<&str>,
4398    ) -> Result<(), AdapterError> {
4399        match (session.user().kind(), var_name) {
4400            // Only internal superusers can reset all system variables.
4401            (UserKind::Superuser, None) if session.user().is_internal() => Ok(()),
4402            // Whether or not a variable can be modified depends if we're an internal superuser.
4403            (UserKind::Superuser, Some(name))
4404                if session.user().is_internal()
4405                    || self.catalog().system_config().user_modifiable(name) =>
4406            {
4407                // In lieu of plumbing the user to all system config functions, just check that
4408                // the var is visible.
4409                let var = self.catalog().system_config().get(name)?;
4410                var.visible(session.user(), self.catalog().system_config())?;
4411                Ok(())
4412            }
4413            // If we're not a superuser, but the variable is user modifiable, indicate they can use
4414            // session variables.
4415            (UserKind::Regular, Some(name))
4416                if self.catalog().system_config().user_modifiable(name) =>
4417            {
4418                Err(AdapterError::Unauthorized(
4419                    rbac::UnauthorizedError::Superuser {
4420                        action: format!("toggle the '{name}' system configuration parameter"),
4421                    },
4422                ))
4423            }
4424            _ => Err(AdapterError::Unauthorized(
4425                rbac::UnauthorizedError::MzSystem {
4426                    action: "alter system".into(),
4427                },
4428            )),
4429        }
4430    }
4431
4432    fn validate_alter_system_network_policy(
4433        &self,
4434        session: &Session,
4435        policy_value: &plan::VariableValue,
4436    ) -> Result<(), AdapterError> {
4437        let policy_name = match &policy_value {
4438            // Make sure the compiled in default still exists.
4439            plan::VariableValue::Default => Some(NETWORK_POLICY.default_value().format()),
4440            plan::VariableValue::Values(values) if values.len() == 1 => {
4441                values.iter().next().cloned()
4442            }
4443            plan::VariableValue::Values(values) => {
4444                tracing::warn!(?values, "can't set multiple network policies at once");
4445                None
4446            }
4447        };
4448        let maybe_network_policy = policy_name
4449            .as_ref()
4450            .and_then(|name| self.catalog.get_network_policy_by_name(name));
4451        let Some(network_policy) = maybe_network_policy else {
4452            return Err(AdapterError::PlanError(plan::PlanError::VarError(
4453                VarError::InvalidParameterValue {
4454                    name: NETWORK_POLICY.name(),
4455                    invalid_values: vec![policy_name.unwrap_or_else(|| "<none>".to_string())],
4456                    reason: "no network policy with such name exists".to_string(),
4457                },
4458            )));
4459        };
4460        self.validate_alter_network_policy(session, &network_policy.rules)
4461    }
4462
4463    /// Validates that a set of [`NetworkPolicyRule`]s is valid for the current [`Session`].
4464    ///
4465    /// This helps prevent users from modifying network policies in a way that would lock out their
4466    /// current connection.
4467    fn validate_alter_network_policy(
4468        &self,
4469        session: &Session,
4470        policy_rules: &Vec<NetworkPolicyRule>,
4471    ) -> Result<(), AdapterError> {
4472        // If the user is not an internal user attempt to protect them from
4473        // blocking themselves.
4474        if session.user().is_internal() {
4475            return Ok(());
4476        }
4477        if let Some(ip) = session.meta().client_ip() {
4478            validate_ip_with_policy_rules(ip, policy_rules)
4479                .map_err(|_| AdapterError::PlanError(plan::PlanError::NetworkPolicyLockoutError))?;
4480        } else {
4481            // Sessions without IPs are only temporarily constructed for default values
4482            // they should not be permitted here.
4483            return Err(AdapterError::NetworkPolicyDenied(
4484                NetworkPolicyError::MissingIp,
4485            ));
4486        }
4487        Ok(())
4488    }
4489
4490    // Returns the name of the portal to execute.
4491    #[instrument]
4492    pub(super) fn sequence_execute(
4493        &self,
4494        session: &mut Session,
4495        plan: plan::ExecutePlan,
4496    ) -> Result<String, AdapterError> {
4497        // Verify the stmt is still valid.
4498        Self::verify_prepared_statement(self.catalog(), session, &plan.name)?;
4499        let ps = session
4500            .get_prepared_statement_unverified(&plan.name)
4501            .expect("known to exist");
4502        let stmt = ps.stmt().cloned();
4503        let desc = ps.desc().clone();
4504        let state_revision = ps.state_revision;
4505        let logging = Arc::clone(ps.logging());
4506        session.create_new_portal(stmt, logging, desc, plan.params, Vec::new(), state_revision)
4507    }
4508
4509    #[instrument]
4510    pub(super) async fn sequence_grant_privileges(
4511        &mut self,
4512        session: &Session,
4513        plan::GrantPrivilegesPlan {
4514            update_privileges,
4515            grantees,
4516        }: plan::GrantPrivilegesPlan,
4517    ) -> Result<ExecuteResponse, AdapterError> {
4518        self.sequence_update_privileges(
4519            session,
4520            update_privileges,
4521            grantees,
4522            UpdatePrivilegeVariant::Grant,
4523        )
4524        .await
4525    }
4526
4527    #[instrument]
4528    pub(super) async fn sequence_revoke_privileges(
4529        &mut self,
4530        session: &Session,
4531        plan::RevokePrivilegesPlan {
4532            update_privileges,
4533            revokees,
4534        }: plan::RevokePrivilegesPlan,
4535    ) -> Result<ExecuteResponse, AdapterError> {
4536        self.sequence_update_privileges(
4537            session,
4538            update_privileges,
4539            revokees,
4540            UpdatePrivilegeVariant::Revoke,
4541        )
4542        .await
4543    }
4544
4545    #[instrument]
4546    async fn sequence_update_privileges(
4547        &mut self,
4548        session: &Session,
4549        update_privileges: Vec<UpdatePrivilege>,
4550        grantees: Vec<RoleId>,
4551        variant: UpdatePrivilegeVariant,
4552    ) -> Result<ExecuteResponse, AdapterError> {
4553        let mut ops = Vec::with_capacity(update_privileges.len());
4554        let mut warnings = Vec::new();
4555        let catalog = self.catalog().for_session(session);
4556
4557        for UpdatePrivilege {
4558            acl_mode,
4559            target_id,
4560            grantor,
4561            acl_from_all,
4562        } in update_privileges
4563        {
4564            let actual_object_type = catalog.get_system_object_type(&target_id);
4565            // For all relations we allow all applicable table privileges, but send a warning if the
4566            // privilege isn't actually applicable to the object type. We skip the warning when the
4567            // user used the `ALL [PRIVILEGES]` shorthand: the user did not explicitly name a
4568            // non-applicable privilege, and via PostgreSQL-compatible `ON TABLE <view>` syntax
4569            // `ALL` deliberately expands to the full table set.
4570            if actual_object_type.is_relation() && !acl_from_all {
4571                let applicable_privileges = rbac::all_object_privileges(actual_object_type);
4572                let non_applicable_privileges = acl_mode.difference(applicable_privileges);
4573                if !non_applicable_privileges.is_empty() {
4574                    let object_description =
4575                        ErrorMessageObjectDescription::from_sys_id(&target_id, &catalog);
4576                    warnings.push(AdapterNotice::NonApplicablePrivilegeTypes {
4577                        non_applicable_privileges,
4578                        object_description,
4579                    })
4580                }
4581            }
4582
4583            if let SystemObjectId::Object(object_id) = &target_id {
4584                self.catalog()
4585                    .ensure_not_reserved_object(object_id, session.conn_id())?;
4586            }
4587
4588            let privileges = self
4589                .catalog()
4590                .get_privileges(&target_id, session.conn_id())
4591                // Should be unreachable since the parser will refuse to parse grant/revoke
4592                // statements on objects without privileges.
4593                .ok_or(AdapterError::Unsupported(
4594                    "GRANTs/REVOKEs on an object type with no privileges",
4595                ))?;
4596
4597            // Collect every grantee's change to this target into one op, so a bulk grant/revoke
4598            // touching one object is a single durable write rather than one per grantee.
4599            let mut target_privileges = Vec::with_capacity(grantees.len());
4600            for grantee in &grantees {
4601                self.catalog().ensure_not_system_role(grantee)?;
4602                self.catalog().ensure_not_predefined_role(grantee)?;
4603                let existing_privilege = privileges
4604                    .get_acl_item(grantee, &grantor)
4605                    .map(Cow::Borrowed)
4606                    .unwrap_or_else(|| Cow::Owned(MzAclItem::empty(*grantee, grantor)));
4607
4608                // Skip grantees for which the grant/revoke would be a no-op.
4609                let changes = match variant {
4610                    UpdatePrivilegeVariant::Grant => {
4611                        !existing_privilege.acl_mode.contains(acl_mode)
4612                    }
4613                    UpdatePrivilegeVariant::Revoke => !existing_privilege
4614                        .acl_mode
4615                        .intersection(acl_mode)
4616                        .is_empty(),
4617                };
4618                if changes {
4619                    target_privileges.push(MzAclItem {
4620                        grantee: *grantee,
4621                        grantor,
4622                        acl_mode,
4623                    });
4624                }
4625            }
4626            if !target_privileges.is_empty() {
4627                ops.push(catalog::Op::UpdatePrivilege {
4628                    target_id: target_id.clone(),
4629                    privileges: target_privileges,
4630                    variant,
4631                });
4632            }
4633        }
4634
4635        if ops.is_empty() {
4636            session.add_notices(warnings);
4637            return Ok(variant.into());
4638        }
4639
4640        let res = self
4641            .catalog_transact(Some(session), ops)
4642            .await
4643            .map(|_| match variant {
4644                UpdatePrivilegeVariant::Grant => ExecuteResponse::GrantedPrivilege,
4645                UpdatePrivilegeVariant::Revoke => ExecuteResponse::RevokedPrivilege,
4646            });
4647        if res.is_ok() {
4648            session.add_notices(warnings);
4649        }
4650        res
4651    }
4652
4653    #[instrument]
4654    pub(super) async fn sequence_alter_default_privileges(
4655        &mut self,
4656        session: &Session,
4657        plan::AlterDefaultPrivilegesPlan {
4658            privilege_objects,
4659            privilege_acl_items,
4660            is_grant,
4661        }: plan::AlterDefaultPrivilegesPlan,
4662    ) -> Result<ExecuteResponse, AdapterError> {
4663        let mut ops = Vec::with_capacity(privilege_objects.len() * privilege_acl_items.len());
4664        let variant = if is_grant {
4665            UpdatePrivilegeVariant::Grant
4666        } else {
4667            UpdatePrivilegeVariant::Revoke
4668        };
4669        for privilege_object in &privilege_objects {
4670            self.catalog()
4671                .ensure_not_system_role(&privilege_object.role_id)?;
4672            self.catalog()
4673                .ensure_not_predefined_role(&privilege_object.role_id)?;
4674            if let Some(database_id) = privilege_object.database_id {
4675                self.catalog()
4676                    .ensure_not_reserved_object(&database_id.into(), session.conn_id())?;
4677            }
4678            if let Some(schema_id) = privilege_object.schema_id {
4679                let database_spec: ResolvedDatabaseSpecifier = privilege_object.database_id.into();
4680                let schema_spec: SchemaSpecifier = schema_id.into();
4681
4682                self.catalog().ensure_not_reserved_object(
4683                    &(database_spec, schema_spec).into(),
4684                    session.conn_id(),
4685                )?;
4686            }
4687            for privilege_acl_item in &privilege_acl_items {
4688                self.catalog()
4689                    .ensure_not_system_role(&privilege_acl_item.grantee)?;
4690                self.catalog()
4691                    .ensure_not_predefined_role(&privilege_acl_item.grantee)?;
4692                ops.push(catalog::Op::UpdateDefaultPrivilege {
4693                    privilege_object: privilege_object.clone(),
4694                    privilege_acl_item: privilege_acl_item.clone(),
4695                    variant,
4696                })
4697            }
4698        }
4699
4700        self.catalog_transact(Some(session), ops).await?;
4701        Ok(ExecuteResponse::AlteredDefaultPrivileges)
4702    }
4703
4704    #[instrument]
4705    pub(super) async fn sequence_grant_role(
4706        &mut self,
4707        session: &Session,
4708        plan::GrantRolePlan {
4709            role_ids,
4710            member_ids,
4711            grantor_id,
4712        }: plan::GrantRolePlan,
4713    ) -> Result<ExecuteResponse, AdapterError> {
4714        let catalog = self.catalog();
4715        let mut ops = Vec::with_capacity(role_ids.len() * member_ids.len());
4716        for role_id in role_ids {
4717            for member_id in &member_ids {
4718                let member_membership: BTreeSet<_> =
4719                    catalog.get_role(member_id).membership().keys().collect();
4720                if member_membership.contains(&role_id) {
4721                    let role_name = catalog.get_role(&role_id).name().to_string();
4722                    let member_name = catalog.get_role(member_id).name().to_string();
4723                    // We need this check so we don't accidentally return a success on a reserved role.
4724                    catalog.ensure_not_reserved_role(member_id)?;
4725                    catalog.ensure_grantable_role(&role_id)?;
4726                    session.add_notice(AdapterNotice::RoleMembershipAlreadyExists {
4727                        role_name,
4728                        member_name,
4729                    });
4730                } else {
4731                    ops.push(catalog::Op::GrantRole {
4732                        role_id,
4733                        member_id: *member_id,
4734                        grantor_id,
4735                    });
4736                }
4737            }
4738        }
4739
4740        if ops.is_empty() {
4741            return Ok(ExecuteResponse::GrantedRole);
4742        }
4743
4744        self.catalog_transact(Some(session), ops)
4745            .await
4746            .map(|_| ExecuteResponse::GrantedRole)
4747    }
4748
4749    #[instrument]
4750    pub(super) async fn sequence_revoke_role(
4751        &mut self,
4752        session: &Session,
4753        plan::RevokeRolePlan {
4754            role_ids,
4755            member_ids,
4756            grantor_id,
4757        }: plan::RevokeRolePlan,
4758    ) -> Result<ExecuteResponse, AdapterError> {
4759        let catalog = self.catalog();
4760        let mut ops = Vec::with_capacity(role_ids.len() * member_ids.len());
4761        for role_id in role_ids {
4762            for member_id in &member_ids {
4763                let member_membership: BTreeSet<_> =
4764                    catalog.get_role(member_id).membership().keys().collect();
4765                if !member_membership.contains(&role_id) {
4766                    let role_name = catalog.get_role(&role_id).name().to_string();
4767                    let member_name = catalog.get_role(member_id).name().to_string();
4768                    // We need this check so we don't accidentally return a success on a reserved role.
4769                    catalog.ensure_not_reserved_role(member_id)?;
4770                    catalog.ensure_grantable_role(&role_id)?;
4771                    session.add_notice(AdapterNotice::RoleMembershipDoesNotExists {
4772                        role_name,
4773                        member_name,
4774                    });
4775                } else {
4776                    ops.push(catalog::Op::RevokeRole {
4777                        role_id,
4778                        member_id: *member_id,
4779                        grantor_id,
4780                    });
4781                }
4782            }
4783        }
4784
4785        if ops.is_empty() {
4786            return Ok(ExecuteResponse::RevokedRole);
4787        }
4788
4789        self.catalog_transact(Some(session), ops)
4790            .await
4791            .map(|_| ExecuteResponse::RevokedRole)
4792    }
4793
4794    #[instrument]
4795    pub(super) async fn sequence_alter_owner(
4796        &mut self,
4797        session: &Session,
4798        plan::AlterOwnerPlan {
4799            id,
4800            object_type,
4801            new_owner,
4802        }: plan::AlterOwnerPlan,
4803    ) -> Result<ExecuteResponse, AdapterError> {
4804        let mut ops = vec![catalog::Op::UpdateOwner {
4805            id: id.clone(),
4806            new_owner,
4807        }];
4808
4809        match &id {
4810            ObjectId::Item(global_id) => {
4811                let entry = self.catalog().get_entry(global_id);
4812
4813                // Cannot directly change the owner of an index.
4814                if entry.is_index() {
4815                    let name = self
4816                        .catalog()
4817                        .resolve_full_name(entry.name(), Some(session.conn_id()))
4818                        .to_string();
4819                    session.add_notice(AdapterNotice::AlterIndexOwner { name });
4820                    return Ok(ExecuteResponse::AlteredObject(object_type));
4821                }
4822
4823                // Alter owner cascades down to dependent indexes.
4824                let dependent_index_ops = entry
4825                    .used_by()
4826                    .into_iter()
4827                    .filter(|id| self.catalog().get_entry(id).is_index())
4828                    .map(|id| catalog::Op::UpdateOwner {
4829                        id: ObjectId::Item(*id),
4830                        new_owner,
4831                    });
4832                ops.extend(dependent_index_ops);
4833
4834                // Alter owner cascades down to progress collections.
4835                let dependent_subsources =
4836                    entry
4837                        .progress_id()
4838                        .into_iter()
4839                        .map(|item_id| catalog::Op::UpdateOwner {
4840                            id: ObjectId::Item(item_id),
4841                            new_owner,
4842                        });
4843                ops.extend(dependent_subsources);
4844            }
4845            ObjectId::Cluster(cluster_id) => {
4846                let cluster = self.catalog().get_cluster(*cluster_id);
4847                // Alter owner cascades down to cluster replicas.
4848                let managed_cluster_replica_ops =
4849                    cluster.replicas().map(|replica| catalog::Op::UpdateOwner {
4850                        id: ObjectId::ClusterReplica((cluster.id(), replica.replica_id())),
4851                        new_owner,
4852                    });
4853                ops.extend(managed_cluster_replica_ops);
4854            }
4855            _ => {}
4856        }
4857
4858        self.catalog_transact(Some(session), ops)
4859            .await
4860            .map(|_| ExecuteResponse::AlteredObject(object_type))
4861    }
4862
4863    #[instrument]
4864    pub(super) async fn sequence_reassign_owned(
4865        &mut self,
4866        session: &Session,
4867        plan::ReassignOwnedPlan {
4868            old_roles,
4869            new_role,
4870            reassign_ids,
4871        }: plan::ReassignOwnedPlan,
4872    ) -> Result<ExecuteResponse, AdapterError> {
4873        for role_id in old_roles.iter().chain(iter::once(&new_role)) {
4874            self.catalog().ensure_not_reserved_role(role_id)?;
4875        }
4876
4877        let ops = reassign_ids
4878            .into_iter()
4879            .map(|id| catalog::Op::UpdateOwner {
4880                id,
4881                new_owner: new_role,
4882            })
4883            .collect();
4884
4885        self.catalog_transact(Some(session), ops)
4886            .await
4887            .map(|_| ExecuteResponse::ReassignOwned)
4888    }
4889
4890    #[instrument]
4891    pub(crate) async fn handle_deferred_statement(&mut self) {
4892        // It is possible Message::DeferredStatementReady was sent but then a session cancellation
4893        // was processed, removing the single element from deferred_statements, so it is expected
4894        // that this is sometimes empty.
4895        let Some(DeferredPlanStatement { ctx, ps }) = self.serialized_ddl.pop_front() else {
4896            return;
4897        };
4898        match ps {
4899            crate::coord::PlanStatement::Statement { stmt, params } => {
4900                self.handle_execute_inner(stmt, params, ctx).await;
4901            }
4902            crate::coord::PlanStatement::Plan {
4903                plan,
4904                resolved_ids,
4905                sql_impl_resolved_ids,
4906            } => {
4907                self.sequence_plan(ctx, plan, resolved_ids, sql_impl_resolved_ids)
4908                    .await;
4909            }
4910        }
4911    }
4912
4913    #[instrument]
4914    // TODO(parkmycar): Remove this once we have an actual implementation.
4915    #[allow(clippy::unused_async)]
4916    pub(super) async fn sequence_alter_table(
4917        &mut self,
4918        ctx: &mut ExecuteContext,
4919        plan: plan::AlterTablePlan,
4920    ) -> Result<ExecuteResponse, AdapterError> {
4921        let plan::AlterTablePlan {
4922            relation_id,
4923            column_name,
4924            column_type,
4925            raw_sql_type,
4926        } = plan;
4927
4928        // TODO(alter_table): Support allocating GlobalIds without a CatalogItemId.
4929        let (_, new_global_id) = self.allocate_user_id().await?;
4930        let ops = vec![catalog::Op::AlterAddColumn {
4931            id: relation_id,
4932            new_global_id,
4933            name: column_name,
4934            typ: column_type,
4935            sql: raw_sql_type,
4936        }];
4937
4938        self.catalog_transact_with_context(None, Some(ctx), ops)
4939            .await?;
4940
4941        Ok(ExecuteResponse::AlteredObject(ObjectType::Table))
4942    }
4943
4944    /// Prepares to apply a replacement materialized view.
4945    #[instrument]
4946    pub(super) async fn sequence_alter_materialized_view_apply_replacement_prepare(
4947        &mut self,
4948        ctx: ExecuteContext,
4949        plan: AlterMaterializedViewApplyReplacementPlan,
4950    ) {
4951        // To ensure there is no time gap in the output, we can only apply a replacement if the
4952        // target's write frontier has caught up to the replacement dataflow's write frontier. This
4953        // might not be the case initially, so we have to wait. To this end, we install a watch set
4954        // waiting for the target MV's write frontier to advance sufficiently.
4955        //
4956        // Note that the replacement's dataflow is not performing any writes, so it can only be
4957        // ahead of the target initially due to as-of selection. Once the target has caught up, the
4958        // replacement's write frontier is always <= the target's.
4959
4960        let AlterMaterializedViewApplyReplacementPlan { id, replacement_id } = plan.clone();
4961
4962        let plan_validity = PlanValidity::new(
4963            self.catalog(),
4964            BTreeSet::from_iter([id, replacement_id]),
4965            None,
4966            None,
4967            ctx.session().role_metadata().clone(),
4968        );
4969
4970        let target = self.catalog.get_entry(&id);
4971        let target_gid = target.latest_global_id();
4972
4973        let replacement = self.catalog.get_entry(&replacement_id);
4974        let replacement_gid = replacement.latest_global_id();
4975
4976        let target_upper = self
4977            .controller
4978            .storage_collections
4979            .collection_frontiers(target_gid)
4980            .expect("target MV exists")
4981            .write_frontier;
4982        let replacement_upper = self
4983            .controller
4984            .compute
4985            .collection_frontiers(replacement_gid, replacement.cluster_id())
4986            .expect("replacement MV exists")
4987            .write_frontier;
4988
4989        info!(
4990            %id, %replacement_id, ?target_upper, ?replacement_upper,
4991            "preparing materialized view replacement application",
4992        );
4993
4994        let Some(replacement_upper_ts) = replacement_upper.into_option() else {
4995            // A replacement's write frontier can only become empty if the target's write frontier
4996            // has advanced to the empty frontier. In this case the MV is sealed for all times and
4997            // applying the replacement wouldn't have any effect. We use this opportunity to alert
4998            // the user by returning an error, rather than applying the useless replacement.
4999            //
5000            // Note that we can't assert on `target_upper` being empty here, because the reporting
5001            // of the target's frontier might be delayed. We'd have to fetch the current frontier
5002            // from persist, which we cannot do without incurring I/O.
5003            ctx.retire(Err(AdapterError::ReplaceMaterializedViewSealed {
5004                name: target.name().item.clone(),
5005            }));
5006            return;
5007        };
5008
5009        // A watch set resolves when the watched objects' frontier becomes _greater_ than the
5010        // specified timestamp. Since we only need to wait until the target frontier is >= the
5011        // replacement's frontier, we can step back the timestamp.
5012        let replacement_upper_ts = replacement_upper_ts.step_back().unwrap_or(Timestamp::MIN);
5013
5014        // TODO(database-issues#9820): If the target MV is dropped while we are waiting for
5015        // progress, the watch set never completes and neither does the `ALTER MATERIALIZED VIEW`
5016        // command.
5017        self.install_storage_watch_set(
5018            ctx.session().conn_id().clone(),
5019            BTreeSet::from_iter([target_gid]),
5020            replacement_upper_ts,
5021            WatchSetResponse::AlterMaterializedViewReady(AlterMaterializedViewReadyContext {
5022                ctx: Some(ctx),
5023                otel_ctx: OpenTelemetryContext::obtain(),
5024                plan,
5025                plan_validity,
5026            }),
5027        )
5028        .expect("target collection exists");
5029    }
5030
5031    /// Finishes applying a replacement materialized view after the frontier wait completed.
5032    #[instrument]
5033    pub async fn sequence_alter_materialized_view_apply_replacement_finish(
5034        &mut self,
5035        mut ctx: AlterMaterializedViewReadyContext,
5036    ) {
5037        ctx.otel_ctx.attach_as_parent();
5038
5039        let AlterMaterializedViewApplyReplacementPlan { id, replacement_id } = ctx.plan;
5040
5041        // We avoid taking the DDL lock for `ALTER MATERIALIZED VIEW ... APPLY REPLACEMENT`
5042        // commands, see `Coordinator::must_serialize_ddl`. We therefore must assume that the
5043        // world has arbitrarily changed since we performed planning, and we must re-assert
5044        // that it still matches our requirements.
5045        if let Err(err) = ctx.plan_validity.check(self.catalog()) {
5046            ctx.retire(Err(err));
5047            return;
5048        }
5049
5050        info!(
5051            %id, %replacement_id,
5052            "finishing materialized view replacement application",
5053        );
5054
5055        // Applying a replacement changes the target's definition while it
5056        // keeps its GlobalIds, so the cached expressions under those ids are
5057        // stale. Entries record the item's version and `ExpressionCache::open`
5058        // drops them on the next boot; invalidating here only reclaims them
5059        // eagerly.
5060        let invalidate_ids = self.catalog().get_entry(&id).global_ids().collect();
5061        self.catalog()
5062            .update_expression_cache(Vec::new(), Vec::new(), invalidate_ids)
5063            .await;
5064
5065        let ops = vec![catalog::Op::AlterMaterializedViewApplyReplacement { id, replacement_id }];
5066        match self
5067            .catalog_transact(Some(ctx.ctx().session_mut()), ops)
5068            .await
5069        {
5070            Ok(()) => ctx.retire(Ok(ExecuteResponse::AlteredObject(
5071                ObjectType::MaterializedView,
5072            ))),
5073            Err(err) => ctx.retire(Err(err)),
5074        }
5075    }
5076
5077    pub(super) async fn statistics_oracle(
5078        &self,
5079        session: &Session,
5080        source_ids: &BTreeSet<GlobalId>,
5081        query_as_of: &Antichain<Timestamp>,
5082        is_oneshot: bool,
5083    ) -> Result<Box<dyn mz_transform::StatisticsOracle>, AdapterError> {
5084        super::statistics_oracle(
5085            session,
5086            source_ids,
5087            query_as_of,
5088            is_oneshot,
5089            self.catalog().system_config(),
5090            self.controller.storage_collections.as_ref(),
5091        )
5092        .await
5093    }
5094}
5095
5096impl Coordinator {
5097    /// Emit the raw optimizer notices in `notices` to the user's session, if
5098    /// any.
5099    ///
5100    /// This intentionally consumes `RawOptimizerNotice`s (not pre-rendered
5101    /// ones) because the user-facing rendering goes through the user's
5102    /// session-aware humanizer, which produces e.g. schema-qualified names
5103    /// relative to the user's current database/schema.
5104    pub(crate) fn emit_raw_optimizer_notices_to_user(
5105        &self,
5106        ctx: &ExecuteContext,
5107        notices: &[RawOptimizerNotice],
5108    ) {
5109        emit_optimizer_notices(&*self.catalog, ctx.session(), notices);
5110    }
5111
5112    /// Renders `raw_df_meta`'s optimizer notices against a humanizer that resolves the
5113    /// about-to-be-created item's own `global_id` to `name`, rather than to the bare transient id
5114    /// a system-session humanizer would produce. `source_desc` supplies the column names the
5115    /// notices humanize against, and is the desc of the relation the new item reads.
5116    ///
5117    /// Callers render before the catalog transaction that creates the item, so the persisted
5118    /// notice text can already refer to the item by its intended name. The raw notices stay with
5119    /// the caller: they go to the user session only once the transaction succeeds, so a failed
5120    /// transaction does not tell the user about an item that was never created.
5121    fn render_create_item_notices(
5122        &self,
5123        name: &QualifiedItemName,
5124        global_id: GlobalId,
5125        source_desc: &RelationDesc,
5126        raw_df_meta: &DataflowMetainfo,
5127    ) -> DataflowMetainfo<Arc<OptimizerNotice>> {
5128        let notice_ids = std::iter::repeat_with(|| self.allocate_transient_id())
5129            .map(|(_item_id, notice_id)| notice_id)
5130            .take(raw_df_meta.optimizer_notices.len())
5131            .collect::<Vec<_>>();
5132
5133        let system_catalog = self.catalog().for_system_session();
5134        let full_name = self.catalog().resolve_full_name(name, None);
5135        let transient_items = btreemap! {
5136            global_id => TransientItem::new(
5137                Some(full_name.into_parts()),
5138                Some(source_desc.iter_names().map(|c| c.to_string()).collect()),
5139            )
5140        };
5141        let humanizer = ExprHumanizerExt::new(transient_items, &system_catalog);
5142        CatalogState::render_notices_core(
5143            &humanizer,
5144            (self.catalog().config().now)(),
5145            raw_df_meta,
5146            notice_ids,
5147            Some(global_id),
5148        )
5149    }
5150
5151    /// Sets `df_desc`'s as-of from a read hold on `id_bundle`, ships the dataflow, and drops the
5152    /// hold once compute has taken its own (compute puts in its own read holds during
5153    /// `create_dataflow`, so it is safe to release this one right after shipping).
5154    ///
5155    /// The read hold across shipping keeps the since of `id_bundle` from advancing underneath the
5156    /// as-of just picked.
5157    async fn ship_new_dataflow(
5158        &mut self,
5159        id_bundle: &CollectionIdBundle,
5160        mut df_desc: DataflowDescription<LirRelationExpr>,
5161        instance: ComputeInstanceId,
5162        notice_builtin_updates_fut: Option<BuiltinTableAppendNotify>,
5163    ) {
5164        let read_holds = self.acquire_read_holds(id_bundle);
5165        let since = read_holds.least_valid_read();
5166        df_desc.set_as_of(since);
5167
5168        self.ship_dataflow_and_notice_builtin_table_updates(
5169            df_desc,
5170            instance,
5171            notice_builtin_updates_fut,
5172            None,
5173        )
5174        .await;
5175
5176        drop(read_holds);
5177    }
5178
5179    /// Persist already-rendered optimizer notices for a newly created
5180    /// non-transient dataflow.
5181    ///
5182    /// This:
5183    /// - packs builtin-table updates for `mz_optimizer_notices` (if enabled),
5184    /// - stores the rendered metainfo on the catalog object via
5185    ///   `set_dataflow_metainfo`,
5186    /// - and returns a future that resolves once the builtin-table append
5187    ///   has been observed, or `None` if nothing was appended.
5188    fn persist_dataflow_metainfo(
5189        &mut self,
5190        df_meta: DataflowMetainfo<Arc<OptimizerNotice>>,
5191        export_id: GlobalId,
5192    ) -> Option<BuiltinTableAppendNotify> {
5193        // Attend to optimization notice builtin tables and save the metainfo in the catalog's
5194        // in-memory state.
5195        if self.catalog().state().system_config().enable_mz_notices()
5196            && !df_meta.optimizer_notices.is_empty()
5197        {
5198            let mut builtin_table_updates = Vec::with_capacity(df_meta.optimizer_notices.len());
5199            self.catalog().state().pack_optimizer_notices(
5200                &mut builtin_table_updates,
5201                df_meta.optimizer_notices.iter(),
5202                Diff::ONE,
5203            );
5204
5205            // Save the metainfo.
5206            self.catalog_mut().set_dataflow_metainfo(export_id, df_meta);
5207
5208            Some(self.builtin_table_update().execute(builtin_table_updates))
5209        } else {
5210            // Save the metainfo.
5211            self.catalog_mut().set_dataflow_metainfo(export_id, df_meta);
5212
5213            None
5214        }
5215    }
5216}