Skip to main content

mz_adapter/coord/sequencer/
inner.rs

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