Skip to main content

mz_adapter/coord/sequencer/
inner.rs

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