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