Skip to main content

mz_adapter/coord/
command_handler.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
10//! Logic for  processing client [`Command`]s. Each [`Command`] is initiated by a
11//! client via some external Materialize API (ex: HTTP and psql).
12
13use base64::prelude::*;
14use differential_dataflow::lattice::Lattice;
15use mz_adapter_types::dyncfgs::ALLOW_USER_SESSIONS;
16use mz_auth::AuthenticatorKind;
17use mz_auth::password::Password;
18use mz_repr::namespaces::MZ_INTERNAL_SCHEMA;
19use mz_sql::catalog::AutoProvisionSource;
20use mz_sql::session::metadata::SessionMetadata;
21use std::collections::{BTreeMap, BTreeSet};
22use std::net::IpAddr;
23use std::sync::Arc;
24
25use futures::FutureExt;
26use futures::future::LocalBoxFuture;
27use mz_adapter_types::connection::{ConnectionId, ConnectionIdType};
28use mz_catalog::SYSTEM_CONN_ID;
29use mz_catalog::memory::objects::{
30    CatalogItem, DataSourceDesc, Role, Source, Table, TableDataSource,
31};
32use mz_ore::task;
33use mz_ore::tracing::OpenTelemetryContext;
34use mz_ore::{instrument, soft_panic_or_log};
35use mz_repr::role_id::RoleId;
36use mz_repr::{Diff, GlobalId, SqlScalarType, Timestamp};
37use mz_sql::ast::{
38    AlterConnectionAction, AlterConnectionStatement, AlterSourceAction, AstInfo, ConstantVisitor,
39    CopyRelation, CopyStatement, CreateSourceOptionName, Raw, Statement, StatementKind,
40    SubscribeStatement,
41};
42use mz_sql::catalog::RoleAttributesRaw;
43use mz_sql::names::{Aug, PartialItemName, ResolvedIds};
44use mz_sql::plan::{
45    AbortTransactionPlan, CommitTransactionPlan, CreateRolePlan, Params, Plan,
46    StatementClassification, TransactionType,
47};
48use mz_sql::pure::{
49    materialized_view_option_contains_temporal, purify_create_materialized_view_options,
50};
51use mz_sql::rbac;
52use mz_sql::rbac::CREATE_ITEM_USAGE;
53use mz_sql::session::user::User;
54use mz_sql::session::vars::{
55    EndTransactionAction, NETWORK_POLICY, OwnedVarInput, STATEMENT_LOGGING_SAMPLE_RATE,
56    TRANSACTION_ISOLATION_VAR_NAME, Value, Var, check_transaction_isolation_feature_flag,
57};
58use mz_sql_parser::ast::display::AstDisplay;
59use mz_sql_parser::ast::{
60    CreateMaterializedViewStatement, ExplainPlanStatement, Explainee, InsertStatement,
61    WithOptionValue,
62};
63use mz_storage_types::sources::Timeline;
64use opentelemetry::trace::TraceContextExt;
65use tokio::sync::{mpsc, oneshot};
66use tracing::{Instrument, debug_span, info, warn};
67use tracing_opentelemetry::OpenTelemetrySpanExt;
68use uuid::Uuid;
69
70use crate::command::{
71    CatalogSnapshot, Command, ExecuteResponse, Response, SASLChallengeResponse,
72    SASLVerifyProofResponse, StartupResponse, SuperuserAttribute,
73};
74use crate::coord::appends::{PendingWriteTxn, UserWriteResponder};
75use crate::coord::peek::PendingPeek;
76use crate::coord::{
77    ConnMeta, Coordinator, DeferredPlanStatement, Message, PendingTxn, PlanStatement, PlanValidity,
78    PurifiedStatementReady, validate_ip_with_policy_rules,
79};
80use crate::error::{AdapterError, AuthenticationError};
81use crate::notice::AdapterNotice;
82use crate::session::{Session, TransactionOps, TransactionStatus};
83use crate::statement_logging::{StatementEndedExecutionReason, WatchSetCreation};
84use crate::util::{ClientTransmitter, ResultExt};
85use crate::webhook::{
86    AppendWebhookResponse, AppendWebhookValidator, WebhookAppender, WebhookAppenderInvalidator,
87};
88use crate::{AppendWebhookError, ExecuteContext, catalog, metrics};
89
90use super::ExecuteContextGuard;
91
92/// The login status of a role, used by authentication handlers to check role
93/// existence and login permission before proceeding to credential verification.
94enum RoleLoginStatus {
95    /// The role does not exist in the catalog.
96    NotFound,
97    /// The role exists and has the LOGIN attribute.
98    CanLogin,
99    /// The role exists but does not have the LOGIN attribute.
100    NonLogin,
101}
102
103fn role_login_status(role: Option<&Role>) -> RoleLoginStatus {
104    match role {
105        None => RoleLoginStatus::NotFound,
106        Some(role) => match role.attributes.login {
107            Some(login) if login => RoleLoginStatus::CanLogin,
108            _ => RoleLoginStatus::NonLogin,
109        },
110    }
111}
112
113impl Coordinator {
114    /// BOXED FUTURE: As of Nov 2023 the returned Future from this function was 58KB. This would
115    /// get stored on the stack which is bad for runtime performance, and blow up our stack usage.
116    /// Because of that we purposefully move this Future onto the heap (i.e. Box it).
117    pub(crate) fn handle_command(&mut self, mut cmd: Command) -> LocalBoxFuture<'_, ()> {
118        async move {
119            if let Some(session) = cmd.session_mut() {
120                session.apply_external_metadata_updates();
121            }
122            match cmd {
123                Command::Startup {
124                    tx,
125                    user,
126                    conn_id,
127                    secret_key,
128                    uuid,
129                    client_ip,
130                    application_name,
131                    notice_tx,
132                } => {
133                    // Note: A ClientTransmitter is not applicable here. Its purpose is to
134                    // rescue the Session from an undeliverable Response, and startup
135                    // responses carry no Session, the Session stays with the client. An
136                    // undeliverable startup response is instead handled by handle_startup's
137                    // failed-send path together with the cleanup guard in Client::startup.
138                    self.handle_startup(
139                        tx,
140                        user,
141                        conn_id,
142                        secret_key,
143                        uuid,
144                        client_ip,
145                        application_name,
146                        notice_tx,
147                    )
148                    .await;
149                }
150
151                Command::AuthenticatePassword {
152                    tx,
153                    role_name,
154                    password,
155                } => {
156                    self.handle_authenticate_password(tx, role_name, password)
157                        .await;
158                }
159
160                Command::AuthenticateGetSASLChallenge {
161                    tx,
162                    role_name,
163                    nonce,
164                } => {
165                    self.handle_generate_sasl_challenge(tx, role_name, nonce)
166                        .await;
167                }
168
169                Command::AuthenticateVerifySASLProof {
170                    tx,
171                    role_name,
172                    proof,
173                    mock_hash,
174                    auth_message,
175                } => {
176                    self.handle_authenticate_verify_sasl_proof(
177                        tx,
178                        role_name,
179                        proof,
180                        auth_message,
181                        mock_hash,
182                    );
183                }
184
185                Command::CheckRoleCanLogin { tx, role_name } => {
186                    self.handle_role_can_login(tx, role_name);
187                }
188
189                Command::Execute {
190                    portal_name,
191                    session,
192                    tx,
193                    outer_ctx_extra,
194                } => {
195                    let tx = ClientTransmitter::new(tx, self.internal_cmd_tx.clone());
196
197                    self.handle_execute(portal_name, session, tx, outer_ctx_extra)
198                        .await;
199                }
200
201                Command::StartCopyFromStdin {
202                    target_id,
203                    target_name,
204                    columns,
205                    row_desc,
206                    params,
207                    session,
208                    tx,
209                } => {
210                    let otel_ctx = OpenTelemetryContext::obtain();
211                    let result = self.setup_copy_from_stdin(
212                        &session,
213                        target_id,
214                        target_name,
215                        columns,
216                        row_desc,
217                        params,
218                    );
219                    let _ = tx.send(Response {
220                        result,
221                        session,
222                        otel_ctx,
223                    });
224                }
225
226                Command::RetireExecute { data, reason } => self.retire_execution(reason, data),
227
228                Command::CancelRequest {
229                    conn_id,
230                    secret_key,
231                } => {
232                    self.handle_cancel(conn_id, secret_key).await;
233                }
234
235                Command::PrivilegedCancelRequest { conn_id } => {
236                    self.handle_privileged_cancel(conn_id).await;
237                }
238
239                Command::GetWebhook {
240                    database,
241                    schema,
242                    name,
243                    tx,
244                } => {
245                    self.handle_get_webhook(database, schema, name, tx);
246                }
247
248                Command::GetSystemVars { tx } => {
249                    let _ = tx.send(self.catalog.system_config().clone());
250                }
251
252                Command::SetSystemVars { vars, conn_id, tx } => {
253                    let mut ops = Vec::with_capacity(vars.len());
254                    let conn = &self.active_conns[&conn_id];
255
256                    for (name, value) in vars {
257                        if let Err(e) =
258                            self.catalog().system_config().get(&name).and_then(|var| {
259                                var.visible(conn.user(), self.catalog.system_config())
260                            })
261                        {
262                            let _ = tx.send(Err(e.into()));
263                            return;
264                        }
265
266                        ops.push(catalog::Op::UpdateSystemConfiguration {
267                            name,
268                            value: OwnedVarInput::Flat(value),
269                        });
270                    }
271
272                    let result = self
273                        .catalog_transact_with_context(Some(&conn_id), None, ops)
274                        .await;
275                    let _ = tx.send(result);
276                }
277
278                Command::UpdateScopedSystemParameters {
279                    overrides,
280                    prune_scope,
281                    tx,
282                } => {
283                    // Store the new working copy, persist it durably, and
284                    // reconcile it into the per-scope resolution boundaries.
285                    self.reconcile_scoped_system_parameters(overrides, prune_scope)
286                        .await;
287                    let _ = tx.send(());
288                }
289
290                Command::InstallScopedSystemParameterFrontend { frontend } => {
291                    // Keep the shared frontend so create-cluster / create-replica
292                    // can resolve scoped overrides synchronously at create time.
293                    self.scoped_frontend = Some(frontend);
294                }
295
296                Command::InjectAuditEvents {
297                    events,
298                    conn_id,
299                    tx,
300                } => {
301                    let ops = vec![catalog::Op::InjectAuditEvents { events }];
302                    let result = self
303                        .catalog_transact_with_context(Some(&conn_id), None, ops)
304                        .await;
305                    let _ = tx.send(result);
306                }
307
308                Command::Terminate { conn_id, tx } => {
309                    self.handle_terminate(conn_id).await;
310                    // Note: We purposefully do not use a ClientTransmitter here because we're already
311                    // terminating the provided session.
312                    if let Some(tx) = tx {
313                        let _ = tx.send(Ok(()));
314                    }
315                }
316
317                Command::Commit {
318                    action,
319                    session,
320                    tx,
321                } => {
322                    let tx = ClientTransmitter::new(tx, self.internal_cmd_tx.clone());
323                    // We reach here not through a statement execution, but from the
324                    // "commit" pgwire command. Thus, we just generate a default statement
325                    // execution context (once statement logging is implemented, this will cause nothing to be logged
326                    // when the execution finishes.)
327                    let ctx = ExecuteContext::from_parts(
328                        tx,
329                        self.internal_cmd_tx.clone(),
330                        session,
331                        Default::default(),
332                    );
333                    let plan = match action {
334                        EndTransactionAction::Commit => {
335                            Plan::CommitTransaction(CommitTransactionPlan {
336                                transaction_type: TransactionType::Implicit,
337                            })
338                        }
339                        EndTransactionAction::Rollback => {
340                            Plan::AbortTransaction(AbortTransactionPlan {
341                                transaction_type: TransactionType::Implicit,
342                            })
343                        }
344                    };
345
346                    let conn_id = ctx.session().conn_id().clone();
347                    self.sequence_plan(ctx, plan, ResolvedIds::empty(), ResolvedIds::empty())
348                        .await;
349                    // Part of the Command::Commit contract is that the Coordinator guarantees that
350                    // it has cleared its transaction state for the connection.
351                    let retire_notify = self.clear_connection(&conn_id).await;
352                    // `sequence_plan` has already handled the client response.
353                    // This call only satisfies the internal cleanup contract.
354                    drop(retire_notify);
355                }
356
357                Command::CatalogSnapshot { tx } => {
358                    let _ = tx.send(CatalogSnapshot {
359                        catalog: self.owned_catalog(),
360                    });
361                }
362
363                Command::CheckConsistency { tx } => {
364                    let _ = tx.send(self.check_consistency());
365                }
366
367                Command::Dump { tx } => {
368                    let _ = tx.send(self.dump().await);
369                }
370
371                Command::GetComputeInstanceClient { instance_id, tx } => {
372                    let _ = tx.send(self.controller.compute.instance_client(instance_id));
373                }
374
375                Command::GetOracle { timeline, tx } => {
376                    let oracle = self
377                        .global_timelines
378                        .get(&timeline)
379                        .map(|timeline_state| Arc::clone(&timeline_state.oracle))
380                        .ok_or(AdapterError::ChangedPlan(
381                            "timeline has disappeared during planning".to_string(),
382                        ));
383                    let _ = tx.send(oracle);
384                }
385
386                Command::DetermineRealTimeRecentTimestamp {
387                    source_ids,
388                    real_time_recency_timeout,
389                    tx,
390                } => {
391                    let result = self
392                        .determine_real_time_recent_timestamp(
393                            source_ids.iter().copied(),
394                            real_time_recency_timeout,
395                        )
396                        .await;
397
398                    match result {
399                        Ok(Some(fut)) => {
400                            let catalog = Arc::clone(&self.catalog);
401                            task::spawn(|| "determine real time recent timestamp", async move {
402                                let result =
403                                    Coordinator::await_real_time_recent_timestamp(catalog, fut)
404                                        .await
405                                        .map(Some);
406                                let _ = tx.send(result);
407                            });
408                        }
409                        Ok(None) => {
410                            let _ = tx.send(Ok(None));
411                        }
412                        Err(e) => {
413                            let _ = tx.send(Err(e));
414                        }
415                    }
416                }
417
418                Command::GetTransactionReadHoldsBundle { conn_id, tx } => {
419                    let read_holds = self.txn_read_holds.get(&conn_id).cloned();
420                    let _ = tx.send(read_holds);
421                }
422
423                Command::StoreTransactionReadHolds {
424                    conn_id,
425                    read_holds,
426                    tx,
427                } => {
428                    self.store_transaction_read_holds(conn_id, read_holds);
429                    let _ = tx.send(());
430                }
431
432                Command::ExecuteSlowPathPeek {
433                    dataflow_plan,
434                    determination,
435                    finishing,
436                    compute_instance,
437                    target_replica,
438                    intermediate_result_type,
439                    source_ids,
440                    conn_id,
441                    max_result_size,
442                    max_query_result_size,
443                    watch_set,
444                    tx,
445                } => {
446                    let result = self
447                        .implement_slow_path_peek(
448                            *dataflow_plan,
449                            determination,
450                            finishing,
451                            compute_instance,
452                            target_replica,
453                            intermediate_result_type,
454                            source_ids,
455                            conn_id,
456                            max_result_size,
457                            max_query_result_size,
458                            watch_set,
459                        )
460                        .await;
461                    let _ = tx.send(result);
462                }
463
464                Command::ExecuteSubscribe {
465                    df_desc,
466                    dependency_ids,
467                    cluster_id,
468                    replica_id,
469                    conn_id,
470                    session_uuid,
471                    read_holds,
472                    plan,
473                    statement_logging_id,
474                    tx,
475                } => {
476                    let mut ctx_extra = ExecuteContextGuard::new(
477                        statement_logging_id,
478                        self.internal_cmd_tx.clone(),
479                    );
480                    match self
481                        .implement_subscribe(
482                            &mut ctx_extra,
483                            df_desc,
484                            dependency_ids,
485                            cluster_id,
486                            replica_id,
487                            conn_id,
488                            session_uuid,
489                            read_holds,
490                            plan,
491                        )
492                        .await
493                    {
494                        Ok((resp, write_notify)) => {
495                            // Wait for the `mz_subscriptions` bookkeeping write off the
496                            // coordinator loop before returning the `SUBSCRIBE` response to
497                            // the subscribing session.
498                            task::spawn(|| "execute_subscribe::await_bookkeeping", async move {
499                                write_notify.await;
500                                let _ = tx.send(Ok(resp));
501                            });
502                        }
503                        Err(e) => {
504                            // On success the guard's contents moved into the
505                            // `Subscribing` response. On error the frontend
506                            // logs the error end, so we defuse rather than
507                            // let the guard's `Drop` emit a spurious
508                            // `Aborted`.
509                            let _ = ctx_extra.defuse();
510                            let _ = tx.send(Err(e));
511                        }
512                    }
513                }
514
515                Command::CopyToPreflight {
516                    s3_sink_connection,
517                    sink_id,
518                    tx,
519                } => {
520                    // Spawn a background task to perform the slow S3 preflight operations.
521                    // This avoids blocking the coordinator's main task.
522                    let connection_context = self.connection_context().clone();
523                    let enforce_external_addresses =
524                        mz_storage_types::dyncfgs::ENFORCE_EXTERNAL_ADDRESSES
525                            .get(self.controller.storage.config().config_set());
526                    task::spawn(|| "copy_to_preflight", async move {
527                        let result = mz_storage_types::sinks::s3_oneshot_sink::preflight(
528                            connection_context,
529                            &s3_sink_connection.aws_connection,
530                            &s3_sink_connection.upload_info,
531                            s3_sink_connection.connection_id,
532                            sink_id,
533                            enforce_external_addresses,
534                        )
535                        .await
536                        .map_err(AdapterError::from);
537                        let _ = tx.send(result);
538                    });
539                }
540
541                Command::ExecuteCopyTo {
542                    df_desc,
543                    compute_instance,
544                    target_replica,
545                    source_ids,
546                    conn_id,
547                    watch_set,
548                    tx,
549                } => {
550                    // implement_copy_to spawns a background task that sends the response
551                    // through tx when the COPY TO completes (or immediately if setup fails).
552                    // We just call it and let it handle all response sending.
553                    self.implement_copy_to(
554                        *df_desc,
555                        compute_instance,
556                        target_replica,
557                        source_ids,
558                        conn_id,
559                        watch_set,
560                        tx,
561                    )
562                    .await;
563                }
564
565                Command::ExecuteSideEffectingFunc { plan, conn_id, tx } => {
566                    let result = self.execute_side_effecting_func(plan, conn_id).await;
567                    let _ = tx.send(result);
568                }
569                Command::LookupConnection { connection_id, tx } => {
570                    let conn =
571                        self.active_conns
572                            .get_key_value(&connection_id)
573                            .map(|(id_handle, meta)| {
574                                (id_handle.clone(), *meta.authenticated_role_id())
575                            });
576                    let _ = tx.send(conn);
577                }
578                Command::RegisterFrontendPeek {
579                    uuid,
580                    conn_id,
581                    cluster_id,
582                    depends_on,
583                    is_fast_path,
584                    watch_set,
585                    tx,
586                } => {
587                    self.handle_register_frontend_peek(
588                        uuid,
589                        conn_id,
590                        cluster_id,
591                        depends_on,
592                        is_fast_path,
593                        watch_set,
594                        tx,
595                    );
596                }
597                Command::UnregisterFrontendPeek { uuid, reason, tx } => {
598                    self.handle_unregister_frontend_peek(uuid, reason, tx);
599                }
600                Command::ExplainTimestamp {
601                    conn_id,
602                    session_wall_time,
603                    cluster_id,
604                    id_bundle,
605                    determination,
606                    tx,
607                } => {
608                    let explanation = self.explain_timestamp(
609                        &conn_id,
610                        session_wall_time,
611                        cluster_id,
612                        &id_bundle,
613                        determination,
614                    );
615                    let _ = tx.send(explanation);
616                }
617                Command::FrontendStatementLogging(event) => {
618                    self.handle_frontend_statement_logging_event(event);
619                }
620            }
621        }
622        .instrument(debug_span!("handle_command"))
623        .boxed_local()
624    }
625
626    fn handle_role_can_login(
627        &self,
628        tx: oneshot::Sender<Result<(), AdapterError>>,
629        role_name: String,
630    ) {
631        let result =
632            match role_login_status(self.catalog().try_get_role_by_name(role_name.as_str())) {
633                RoleLoginStatus::NotFound => Err(AdapterError::AuthenticationError(
634                    AuthenticationError::RoleNotFound,
635                )),
636                RoleLoginStatus::NonLogin => Err(AdapterError::AuthenticationError(
637                    AuthenticationError::NonLogin,
638                )),
639                RoleLoginStatus::CanLogin => Ok(()),
640            };
641        let _ = tx.send(result);
642    }
643
644    fn handle_authenticate_verify_sasl_proof(
645        &self,
646        tx: oneshot::Sender<Result<SASLVerifyProofResponse, AdapterError>>,
647        role_name: String,
648        proof: String,
649        auth_message: String,
650        mock_hash: String,
651    ) {
652        let role = self.catalog().try_get_role_by_name(role_name.as_str());
653        let login_status = role_login_status(role);
654        let role_auth = role.and_then(|r| self.catalog().try_get_role_auth_by_id(&r.id));
655        let real_hash = role_auth
656            .as_ref()
657            .and_then(|auth| auth.password_hash.as_ref());
658        let hash_ref = real_hash.map(|s| s.as_str()).unwrap_or(&mock_hash);
659
660        match mz_auth::hash::sasl_verify(hash_ref, &proof, &auth_message) {
661            Ok(verifier) => {
662                // Success only if role exists, allows login, and a real password hash was used.
663                if matches!(login_status, RoleLoginStatus::CanLogin) && real_hash.is_some() {
664                    let _ = tx.send(Ok(SASLVerifyProofResponse { verifier }));
665                } else {
666                    let _ = tx.send(Err(AdapterError::AuthenticationError(match login_status {
667                        RoleLoginStatus::NonLogin => AuthenticationError::NonLogin,
668                        RoleLoginStatus::NotFound => AuthenticationError::RoleNotFound,
669                        RoleLoginStatus::CanLogin => AuthenticationError::InvalidCredentials,
670                    })));
671                }
672            }
673            Err(_) => {
674                let _ = tx.send(Err(AdapterError::AuthenticationError(
675                    AuthenticationError::InvalidCredentials,
676                )));
677            }
678        }
679    }
680
681    #[mz_ore::instrument(level = "debug")]
682    async fn handle_generate_sasl_challenge(
683        &self,
684        tx: oneshot::Sender<Result<SASLChallengeResponse, AdapterError>>,
685        role_name: String,
686        client_nonce: String,
687    ) {
688        let role_auth = self
689            .catalog()
690            .try_get_role_by_name(&role_name)
691            .and_then(|role| self.catalog().try_get_role_auth_by_id(&role.id));
692
693        let nonce = match mz_auth::hash::generate_nonce(&client_nonce) {
694            Ok(n) => n,
695            Err(e) => {
696                let msg = format!(
697                    "failed to generate nonce for client nonce {}: {}",
698                    client_nonce, e
699                );
700                let _ = tx.send(Err(AdapterError::Internal(msg.clone())));
701                soft_panic_or_log!("{msg}");
702                return;
703            }
704        };
705
706        // It's important that the mock_nonce is deterministic per role, otherwise the purpose of
707        // doing mock authentication is defeated. We use a catalog-wide nonce, and combine that
708        // with the role name to get a per-role mock nonce.
709        let send_mock_challenge =
710            |role_name: String,
711             mock_nonce: String,
712             nonce: String,
713             tx: oneshot::Sender<Result<SASLChallengeResponse, AdapterError>>| {
714                let opts = mz_auth::hash::mock_sasl_challenge(
715                    &role_name,
716                    &mock_nonce,
717                    &self.catalog().system_config().scram_iterations(),
718                );
719                let _ = tx.send(Ok(SASLChallengeResponse {
720                    iteration_count: mz_ore::cast::u32_to_usize(opts.iterations.get()),
721                    salt: BASE64_STANDARD.encode(opts.salt),
722                    nonce,
723                }));
724            };
725
726        match role_auth {
727            Some(auth) if auth.password_hash.is_some() => {
728                let hash = auth.password_hash.as_ref().expect("checked above");
729                match mz_auth::hash::scram256_parse_opts(hash) {
730                    Ok(opts) => {
731                        let _ = tx.send(Ok(SASLChallengeResponse {
732                            iteration_count: mz_ore::cast::u32_to_usize(opts.iterations.get()),
733                            salt: BASE64_STANDARD.encode(opts.salt),
734                            nonce,
735                        }));
736                    }
737                    Err(_) => {
738                        send_mock_challenge(
739                            role_name,
740                            self.catalog().state().mock_authentication_nonce(),
741                            nonce,
742                            tx,
743                        );
744                    }
745                }
746            }
747            _ => {
748                send_mock_challenge(
749                    role_name,
750                    self.catalog().state().mock_authentication_nonce(),
751                    nonce,
752                    tx,
753                );
754            }
755        }
756    }
757
758    #[mz_ore::instrument(level = "debug")]
759    async fn handle_authenticate_password(
760        &self,
761        tx: oneshot::Sender<Result<(), AdapterError>>,
762        role_name: String,
763        password: Option<Password>,
764    ) {
765        let Some(password) = password else {
766            // The user did not provide a password.
767            let _ = tx.send(Err(AdapterError::AuthenticationError(
768                AuthenticationError::PasswordRequired,
769            )));
770            return;
771        };
772        let role = self.catalog().try_get_role_by_name(role_name.as_str());
773
774        match role_login_status(role) {
775            RoleLoginStatus::NotFound => {
776                let _ = tx.send(Err(AdapterError::AuthenticationError(
777                    AuthenticationError::RoleNotFound,
778                )));
779                return;
780            }
781            RoleLoginStatus::NonLogin => {
782                let _ = tx.send(Err(AdapterError::AuthenticationError(
783                    AuthenticationError::NonLogin,
784                )));
785                return;
786            }
787            RoleLoginStatus::CanLogin => {}
788        }
789
790        let role_auth = role.and_then(|r| self.catalog().try_get_role_auth_by_id(&r.id));
791
792        if let Some(auth) = role_auth {
793            if let Some(hash) = &auth.password_hash {
794                let hash = hash.clone();
795                task::spawn_blocking(
796                    || "auth-check-hash",
797                    move || {
798                        let _ = match mz_auth::hash::scram256_verify(&password, &hash) {
799                            Ok(_) => tx.send(Ok(())),
800                            Err(_) => tx.send(Err(AdapterError::AuthenticationError(
801                                AuthenticationError::InvalidCredentials,
802                            ))),
803                        };
804                    },
805                );
806                return;
807            }
808        }
809        // Authentication failed due to missing password hash.
810        let _ = tx.send(Err(AdapterError::AuthenticationError(
811            AuthenticationError::InvalidCredentials,
812        )));
813    }
814
815    #[mz_ore::instrument(level = "debug")]
816    async fn handle_startup(
817        &mut self,
818        tx: oneshot::Sender<Result<StartupResponse, AdapterError>>,
819        user: User,
820        conn_id: ConnectionId,
821        secret_key: u32,
822        uuid: uuid::Uuid,
823        client_ip: Option<IpAddr>,
824        application_name: String,
825        notice_tx: mpsc::UnboundedSender<AdapterNotice>,
826    ) {
827        // Early return if successful, otherwise cleanup any possible state.
828        match self
829            .handle_startup_inner(&user, &conn_id, &client_ip, &notice_tx)
830            .await
831        {
832            Ok((role_id, superuser_attribute, session_defaults)) => {
833                let session_type = metrics::session_type_label_value(&user);
834                self.metrics
835                    .active_sessions
836                    .with_label_values(&[session_type])
837                    .inc();
838                let conn = ConnMeta {
839                    secret_key,
840                    notice_tx,
841                    drop_sinks: BTreeSet::new(),
842                    pending_cluster_alters: BTreeSet::new(),
843                    connected_at: self.now(),
844                    user,
845                    application_name,
846                    uuid,
847                    client_ip,
848                    conn_id: conn_id.clone(),
849                    authenticated_role: role_id,
850                    deferred_lock: None,
851                };
852                let update = self.catalog().state().pack_session_update(&conn, Diff::ONE);
853                let update = self.catalog().state().resolve_builtin_table_update(update);
854                self.begin_session_for_statement_logging(&conn);
855                self.active_conns.insert(conn_id.clone(), conn);
856
857                // Note: Do NOT await the notify here, we pass this back to
858                // whatever requested the startup to prevent blocking startup
859                // and the Coordinator on a builtin table update.
860                let updates = vec![update];
861                // It's not a hard error if our list is missing a builtin table, but we want to
862                // make sure these two things stay in-sync.
863                if mz_ore::assert::soft_assertions_enabled() {
864                    let required_tables: BTreeSet<_> = super::appends::REQUIRED_BUILTIN_TABLES
865                        .iter()
866                        .map(|table| self.catalog().resolve_builtin_table(*table))
867                        .collect();
868                    let updates_tracked = updates
869                        .iter()
870                        .all(|update| required_tables.contains(&update.id));
871                    let all_mz_internal = super::appends::REQUIRED_BUILTIN_TABLES
872                        .iter()
873                        .all(|table| table.schema == MZ_INTERNAL_SCHEMA);
874                    mz_ore::soft_assert_or_log!(
875                        updates_tracked,
876                        "not tracking all required builtin table updates!"
877                    );
878                    // TODO(parkmycar): When checking if a query depends on these builtin table
879                    // writes we do not check the transitive dependencies of the query, because
880                    // we don't support creating views on mz_internal objects. If one of these
881                    // tables is promoted out of mz_internal then we'll need to add this check.
882                    mz_ore::soft_assert_or_log!(
883                        all_mz_internal,
884                        "not all builtin tables are in mz_internal! need to check transitive depends",
885                    )
886                }
887                let notify = self.builtin_table_update().background(updates);
888
889                let catalog = self.owned_catalog();
890                let build_info_human_version =
891                    catalog.state().config().build_info.human_version(None);
892
893                let statement_logging_frontend = self
894                    .statement_logging
895                    .create_frontend(build_info_human_version);
896
897                let resp = Ok(StartupResponse {
898                    role_id,
899                    write_notify: notify,
900                    session_defaults,
901                    catalog,
902                    storage_collections: Arc::clone(&self.controller.storage_collections),
903                    transient_id_gen: Arc::clone(&self.transient_id_gen),
904                    optimizer_metrics: self.optimizer_metrics.clone(),
905                    persist_client: self.persist_client.clone(),
906                    statement_logging_frontend,
907                    superuser_attribute,
908                });
909                if tx.send(resp).is_err() {
910                    // Failed to send to adapter, but everything is setup so we can terminate
911                    // normally.
912                    self.handle_terminate(conn_id).await;
913                }
914            }
915            Err(e) => {
916                // Nothing to clean up and `handle_terminate` must not be called. Per the
917                // `handle_startup_inner` invariant, no per-connection state exists and the
918                // connection was never registered in `active_conns`. An auto-provisioned role
919                // stays, it is role-scoped rather than tied to this connection. Temporary
920                // schemas are created lazily, so none exists yet. For the same reason, a
921                // failure to send the error back to the client needs no handling.
922                let _ = tx.send(Err(e));
923            }
924        }
925    }
926
927    /// Fallible startup work.
928    ///
929    /// Invariant: when this returns an error, no per-connection coordinator
930    /// state exists. Per-connection state is registered only in
931    /// `handle_startup`'s Ok arm, after this function has succeeded. The
932    /// cleanup guard in `Client::startup` relies on this invariant by not
933    /// sending `Terminate` when startup fails, and `handle_terminate` panics
934    /// on connections it does not know about.
935    ///
936    /// Durable catalog changes made before an error (an auto-provisioned
937    /// role, synced role memberships) are role-scoped rather than
938    /// connection-scoped and are intentionally kept.
939    async fn handle_startup_inner(
940        &mut self,
941        user: &User,
942        _conn_id: &ConnectionId,
943        client_ip: &Option<IpAddr>,
944        notice_tx: &mpsc::UnboundedSender<AdapterNotice>,
945    ) -> Result<(RoleId, SuperuserAttribute, BTreeMap<String, OwnedVarInput>), AdapterError> {
946        if self.catalog().try_get_role_by_name(&user.name).is_none() {
947            // If the user has made it to this point, that means they have been fully authenticated.
948            // This includes preventing any user, except a pre-defined set of system users, from
949            // connecting to an internal port. Therefore it's ok to always create a new role for the
950            // user.
951            let mut attributes = RoleAttributesRaw::new();
952            // When auto-provisioning, we store the authenticator that was used to provision the role.
953            attributes.auto_provision_source = match user.authenticator_kind {
954                Some(AuthenticatorKind::Oidc) => Some(AutoProvisionSource::Oidc),
955                Some(AuthenticatorKind::Frontegg) => Some(AutoProvisionSource::Frontegg),
956                Some(AuthenticatorKind::None) => Some(AutoProvisionSource::None),
957                _ => {
958                    warn!(
959                        "auto-provisioning role with unexpected authenticator kind: {:?}",
960                        user.authenticator_kind
961                    );
962                    None
963                }
964            };
965
966            // Auto-provision roles with the LOGIN attribute to distinguish
967            // them as users.
968            attributes.login = Some(true);
969
970            let plan = CreateRolePlan {
971                name: user.name.to_string(),
972                attributes,
973            };
974            self.sequence_create_role_for_startup(plan).await?;
975        }
976        let role = self
977            .catalog()
978            .try_get_role_by_name(&user.name)
979            .expect("created above");
980        let role_id = role.id;
981        let superuser_attribute = role.attributes.superuser;
982
983        // JWT group-to-role sync: reconcile role memberships with JWT group claims.
984        // Missing groups claim (None) → skip sync; empty (Some([])) → revoke all.
985        self.maybe_sync_jwt_groups(role_id, user.groups.as_deref(), notice_tx)
986            .await?;
987
988        if role_id.is_user() && !ALLOW_USER_SESSIONS.get(self.catalog().system_config().dyncfgs()) {
989            return Err(AdapterError::UserSessionsDisallowed);
990        }
991
992        // Initialize the default session variables for this role.
993        let mut session_defaults = BTreeMap::new();
994        let system_config = self.catalog().state().system_config();
995
996        // Override the session with any system defaults.
997        session_defaults.extend(
998            system_config
999                .iter_session()
1000                .map(|v| (v.name().to_string(), OwnedVarInput::Flat(v.value()))),
1001        );
1002        // Special case.
1003        let statement_logging_default = system_config
1004            .statement_logging_default_sample_rate()
1005            .format();
1006        session_defaults.insert(
1007            STATEMENT_LOGGING_SAMPLE_RATE.name().to_string(),
1008            OwnedVarInput::Flat(statement_logging_default),
1009        );
1010        // Override system defaults with role defaults.
1011        session_defaults.extend(
1012            self.catalog()
1013                .get_role(&role_id)
1014                .vars()
1015                .map(|(name, val)| (name.to_string(), val.clone())),
1016        );
1017
1018        // If the resolved `transaction_isolation` default names a feature-flagged
1019        // isolation level whose flag is now disabled (e.g. a role default set
1020        // while `bounded staleness` was enabled, then the flag turned off), drop
1021        // it so the session falls back to the built-in default rather than
1022        // silently using a gated level.
1023        if let Some(value) = session_defaults.get(TRANSACTION_ISOLATION_VAR_NAME) {
1024            if check_transaction_isolation_feature_flag(
1025                TRANSACTION_ISOLATION_VAR_NAME,
1026                value.borrow(),
1027                system_config,
1028            )
1029            .is_err()
1030            {
1031                session_defaults.remove(TRANSACTION_ISOLATION_VAR_NAME);
1032            }
1033        }
1034
1035        // Validate network policies for external users. Internal users can only connect on the
1036        // internal interfaces (internal HTTP/ pgwire). It is up to the person deploying the system
1037        // to ensure these internal interfaces are well secured.
1038        //
1039        // HACKY(parkmycar): We don't have a fully formed session yet for this role, but we want
1040        // the default network policy for this role, so we read directly out of what the session
1041        // will get initialized with.
1042        if !user.is_internal() {
1043            let network_policy_name = session_defaults
1044                .get(NETWORK_POLICY.name())
1045                .and_then(|value| match value {
1046                    OwnedVarInput::Flat(name) => Some(name.clone()),
1047                    OwnedVarInput::SqlSet(names) => {
1048                        tracing::error!(?names, "found multiple network policies");
1049                        None
1050                    }
1051                })
1052                .unwrap_or_else(|| system_config.default_network_policy_name());
1053            let maybe_network_policy = self
1054                .catalog()
1055                .get_network_policy_by_name(&network_policy_name);
1056
1057            let Some(network_policy) = maybe_network_policy else {
1058                // We should prevent dropping the default network policy, or setting the policy
1059                // to something that doesn't exist, so complain loudly if this occurs.
1060                tracing::error!(
1061                    network_policy_name,
1062                    "default network policy does not exist. All user traffic will be blocked"
1063                );
1064                let reason = match client_ip {
1065                    Some(ip) => super::NetworkPolicyError::AddressDenied(ip.clone()),
1066                    None => super::NetworkPolicyError::MissingIp,
1067                };
1068                return Err(AdapterError::NetworkPolicyDenied(reason));
1069            };
1070
1071            if let Some(ip) = client_ip {
1072                match validate_ip_with_policy_rules(ip, &network_policy.rules) {
1073                    Ok(_) => {}
1074                    Err(e) => return Err(AdapterError::NetworkPolicyDenied(e)),
1075                }
1076            } else {
1077                // Only temporary and internal representation of a session
1078                // should be missing a client_ip. These sessions should not be
1079                // making requests or going through handle_startup.
1080                return Err(AdapterError::NetworkPolicyDenied(
1081                    super::NetworkPolicyError::MissingIp,
1082                ));
1083            }
1084        }
1085
1086        // Temporary schemas are now created lazily when the first temporary object is created,
1087        // rather than eagerly on connection startup. This avoids expensive catalog_mut() calls
1088        // for the common case where connections never create temporary objects.
1089
1090        Ok((
1091            role_id,
1092            SuperuserAttribute(superuser_attribute),
1093            session_defaults,
1094        ))
1095    }
1096
1097    /// Handles an execute command.
1098    #[instrument(name = "coord::handle_execute", fields(session = session.uuid().to_string()))]
1099    pub(crate) async fn handle_execute(
1100        &mut self,
1101        portal_name: String,
1102        mut session: Session,
1103        tx: ClientTransmitter<ExecuteResponse>,
1104        // If this command was part of another execute command
1105        // (for example, executing a `FETCH` statement causes an execute to be
1106        //  issued for the cursor it references),
1107        // then `outer_context` should be `Some`.
1108        // This instructs the coordinator that the
1109        // outer execute should be considered finished once the inner one is.
1110        outer_context: Option<ExecuteContextGuard>,
1111    ) {
1112        // A new statement is starting, so discard any cancellation that was signaled while no
1113        // statement was running. Such a cancellation targeted an earlier statement and must not
1114        // cancel the new one. (Like in PostgreSQL, a cancel request that arrives when nothing is
1115        // running has no effect.) The watch would otherwise retain a stale `true` within an
1116        // explicit transaction, because it is removed only when the transaction is cleared, not
1117        // at statement end.
1118        //
1119        // Don't do this for nested executes (e.g., FETCH executing its cursor's statement): the
1120        // outer statement is still running and a pending cancellation may target it.
1121        if outer_context.is_none() {
1122            self.connection_cancel_watches.remove(session.conn_id());
1123        }
1124
1125        if session.vars().emit_trace_id_notice() {
1126            let span_context = tracing::Span::current()
1127                .context()
1128                .span()
1129                .span_context()
1130                .clone();
1131            if span_context.is_valid() {
1132                session.add_notice(AdapterNotice::QueryTrace {
1133                    trace_id: span_context.trace_id(),
1134                });
1135            }
1136        }
1137
1138        if let Err(err) = Self::verify_portal(self.catalog(), &mut session, &portal_name) {
1139            // If statement logging hasn't started yet, we don't need
1140            // to add any "end" event, so just make up a no-op
1141            // `ExecuteContextExtra` here, via `Default::default`.
1142            //
1143            // It's a bit unfortunate because the edge case of failed
1144            // portal verifications won't show up in statement
1145            // logging, but there seems to be nothing else we can do,
1146            // because we need access to the portal to begin logging.
1147            //
1148            // Another option would be to log a begin and end event, but just fill in NULLs
1149            // for everything we get from the portal (prepared statement id, params).
1150            let extra = outer_context.unwrap_or_else(Default::default);
1151            let ctx = ExecuteContext::from_parts(tx, self.internal_cmd_tx.clone(), session, extra);
1152            return ctx.retire(Err(err));
1153        }
1154
1155        // The reference to `portal` can't outlive `session`, which we
1156        // use to construct the context, so scope the reference to this block where we
1157        // get everything we need from the portal for later.
1158        let (stmt, ctx, params) = {
1159            let portal = session
1160                .get_portal_unverified(&portal_name)
1161                .expect("known to exist");
1162            let params = portal.parameters.clone();
1163            let stmt = portal.stmt.clone();
1164            let logging = Arc::clone(&portal.logging);
1165            let lifecycle_timestamps = portal.lifecycle_timestamps.clone();
1166
1167            let extra = if let Some(extra) = outer_context {
1168                // We are executing in the context of another SQL statement, so we don't
1169                // want to begin statement logging anew. The context of the actual statement
1170                // being executed is the one that should be retired once this finishes.
1171                extra
1172            } else {
1173                // This is a new statement, log it and return the context
1174                let maybe_uuid = self.begin_statement_execution(
1175                    &mut session,
1176                    &params,
1177                    &logging,
1178                    lifecycle_timestamps,
1179                );
1180
1181                ExecuteContextGuard::new(maybe_uuid, self.internal_cmd_tx.clone())
1182            };
1183            let ctx = ExecuteContext::from_parts(tx, self.internal_cmd_tx.clone(), session, extra);
1184            (stmt, ctx, params)
1185        };
1186
1187        let stmt = match stmt {
1188            Some(stmt) => stmt,
1189            None => return ctx.retire(Ok(ExecuteResponse::EmptyQuery)),
1190        };
1191
1192        let session_type = metrics::session_type_label_value(ctx.session().user());
1193        let stmt_type = metrics::statement_type_label_value(&stmt);
1194        self.metrics
1195            .query_total
1196            .with_label_values(&[session_type, stmt_type])
1197            .inc();
1198        match &*stmt {
1199            Statement::Subscribe(SubscribeStatement { output, .. })
1200            | Statement::Copy(CopyStatement {
1201                relation: CopyRelation::Subscribe(SubscribeStatement { output, .. }),
1202                ..
1203            }) => {
1204                self.metrics
1205                    .subscribe_outputs
1206                    .with_label_values(&[
1207                        session_type,
1208                        metrics::subscribe_output_label_value(output),
1209                    ])
1210                    .inc();
1211            }
1212            _ => {}
1213        }
1214
1215        self.handle_execute_inner(stmt, params, ctx).await
1216    }
1217
1218    #[instrument(name = "coord::handle_execute_inner", fields(stmt = stmt.to_ast_string_redacted()))]
1219    pub(crate) async fn handle_execute_inner(
1220        &mut self,
1221        stmt: Arc<Statement<Raw>>,
1222        params: Params,
1223        mut ctx: ExecuteContext,
1224    ) {
1225        // This comment describes the various ways DDL can execute (the ordered operations: name
1226        // resolve, purify, plan, sequence), all of which are managed by this function. DDL has
1227        // three notable properties that all partially interact.
1228        //
1229        // 1. Most DDL statements (and a few others) support single-statement transaction delayed
1230        //    execution. This occurs when a session executes `BEGIN`, a single DDL, then `COMMIT`.
1231        //    We announce success of the single DDL when it is executed, but do not attempt to plan
1232        //    or sequence it until `COMMIT`, which is able to error if needed while sequencing the
1233        //    DDL (this behavior is Postgres-compatible). The purpose of this is because some
1234        //    drivers or tools wrap all statements in `BEGIN` and `COMMIT` and we would like them to
1235        //    work. When the single DDL is announced as successful we also put the session's
1236        //    transaction ops into `SingleStatement` which will produce an error if any other
1237        //    statement is run in the transaction except `COMMIT`. Additionally, this will cause
1238        //    `handle_execute_inner` to stop further processing (no planning, etc.) of the
1239        //    statement.
1240        // 2. A few other DDL statements (`ALTER .. RENAME/SWAP`) enter the `DDL` ops which allows
1241        //    any number of only these DDL statements to be executed in a transaction. During
1242        //    sequencing we run an incremental catalog dry run against in-memory transaction state
1243        //    and store the resulting `CatalogState` in `TransactionOps::DDL`, but nothing is yet
1244        //    committed to the durable catalog. At `COMMIT`, all accumulated ops are applied in one
1245        //    catalog transaction. The purpose of this is to allow multiple, atomic renames in the
1246        //    same transaction.
1247        // 3. Some DDLs do off-thread work during purification or sequencing that is expensive or
1248        //    makes network calls (interfacing with secrets, optimization of views/indexes, source
1249        //    purification). These must guarantee correctness when they return to the main
1250        //    coordinator thread because the catalog state could have changed while they were doing
1251        //    the off-thread work. Previously we would use `PlanValidity::Checks` to specify a bunch
1252        //    of IDs that we needed to exist. We discovered the way we were doing that was not
1253        //    always correct. Instead of attempting to get that completely right, we have opted to
1254        //    serialize DDL. Getting this right is difficult because catalog changes can affect name
1255        //    resolution, planning, sequencing, and optimization. Correctly writing logic that is
1256        //    aware of all possible catalog changes that would affect any of those parts is not
1257        //    something our current code has been designed to be helpful at. Even if a DDL statement
1258        //    is doing off-thread work, another DDL must not yet execute at all. Executing these
1259        //    serially will guarantee that no off-thread work has affected the state of the catalog.
1260        //    This is done by adding a VecDeque of deferred statements and a lock to the
1261        //    Coordinator. When a DDL is run in `handle_execute_inner` (after applying whatever
1262        //    transaction ops are needed to the session as described above), it attempts to own the
1263        //    lock (a tokio Mutex). If acquired, it stashes the lock in the connection`s `ConnMeta`
1264        //    struct in `active_conns` and proceeds. The lock is dropped at transaction end in
1265        //    `clear_transaction` and a message sent to the Coordinator to execute the next queued
1266        //    DDL. If the lock could not be acquired, the DDL is put into the VecDeque where it
1267        //    awaits dequeuing caused by the lock being released.
1268
1269        // For `Started`, this separates the first statement of an extended-protocol
1270        // pipeline from a later one that joins the ops staged before it.
1271        let txn_contains_ops = ctx.session().transaction().contains_ops();
1272
1273        // Verify that this statement type can be executed in the current
1274        // transaction state.
1275        match ctx.session().transaction() {
1276            // By this point we should be in a running transaction.
1277            TransactionStatus::Default => unreachable!(),
1278
1279            // Failed transactions have already been checked in pgwire for a safe statement
1280            // (COMMIT, ROLLBACK, etc.) and can proceed.
1281            TransactionStatus::Failed(_) => {}
1282
1283            // Started is a deceptive name, and means different things depending on which
1284            // protocol was used. It's either exactly one statement (known because this
1285            // is the simple protocol and the parser parsed the entire string, and it had
1286            // one statement). Or from the extended protocol, it means *some* query is
1287            // being executed, but there might be others after it before the Sync (commit)
1288            // message. Postgres handles this by teaching Started to eagerly commit certain
1289            // statements that can't be run in a transaction block.
1290            TransactionStatus::Started(_) if !txn_contains_ops => {
1291                if let Statement::Declare(_) = &*stmt {
1292                    // Declare is an exception. Although it's not against any spec to execute
1293                    // it, it will always result in nothing happening, since all portals will be
1294                    // immediately closed. Users don't know this detail, so this error helps them
1295                    // understand what's going wrong. Postgres does this too.
1296                    return ctx.retire(Err(AdapterError::OperationRequiresTransaction(
1297                        "DECLARE CURSOR".into(),
1298                    )));
1299                }
1300            }
1301
1302            // Implicit or explicit transactions.
1303            //
1304            // Implicit transactions happen when a multi-statement query is executed
1305            // (a "simple query"). However if a "BEGIN" appears somewhere in there,
1306            // then the existing implicit transaction will be upgraded to an explicit
1307            // transaction. Thus, we should not separate what implicit and explicit
1308            // transactions can do unless there's some additional checking to make sure
1309            // something disallowed in explicit transactions did not previously take place
1310            // in the implicit portion.
1311            //
1312            // A `Started` transaction with staged ops belongs here too: this statement
1313            // runs alongside them, so a DDL or read-then-write that cannot see them must
1314            // be rejected rather than run against a state that lacks them.
1315            TransactionStatus::Started(_)
1316            | TransactionStatus::InTransactionImplicit(_)
1317            | TransactionStatus::InTransaction(_) => {
1318                match &*stmt {
1319                    // Statements that are safe in a transaction. We still need to verify that we
1320                    // don't interleave reads and writes since we can't perform those serializably.
1321                    Statement::Close(_)
1322                    | Statement::Commit(_)
1323                    | Statement::Copy(_)
1324                    | Statement::Deallocate(_)
1325                    | Statement::Declare(_)
1326                    | Statement::Discard(_)
1327                    | Statement::Execute(_)
1328                    | Statement::ExplainPlan(_)
1329                    | Statement::ExplainPushdown(_)
1330                    | Statement::ExplainAnalyzeObject(_)
1331                    | Statement::ExplainAnalyzeCluster(_)
1332                    | Statement::ExplainTimestamp(_)
1333                    | Statement::ExplainSinkSchema(_)
1334                    | Statement::Fetch(_)
1335                    | Statement::Prepare(_)
1336                    | Statement::Rollback(_)
1337                    | Statement::Select(_)
1338                    | Statement::SetTransaction(_)
1339                    | Statement::Show(_)
1340                    | Statement::SetVariable(_)
1341                    | Statement::ResetVariable(_)
1342                    | Statement::StartTransaction(_)
1343                    | Statement::Subscribe(_)
1344                    | Statement::Raise(_) => {
1345                        // Always safe.
1346                    }
1347
1348                    Statement::Insert(InsertStatement {
1349                        source, returning, ..
1350                    }) if returning.is_empty() && ConstantVisitor::insert_source(source) => {
1351                        // Inserting from constant values statements that do not need to execute on
1352                        // any cluster (no RETURNING) is always safe.
1353                    }
1354
1355                    // These statements must be kept in-sync with `must_serialize_ddl()`.
1356                    Statement::AlterObjectRename(_)
1357                    | Statement::AlterObjectSwap(_)
1358                    | Statement::CreateTableFromSource(_)
1359                    | Statement::CreateSource(_) => {
1360                        let state = self.catalog().for_session(ctx.session()).state().clone();
1361                        let revision = self.catalog().transient_revision();
1362
1363                        // Initialize our transaction with a set of empty ops, or return an error
1364                        // if we can't run a DDL transaction
1365                        let txn_status = ctx.session_mut().transaction_mut();
1366                        if let Err(err) = txn_status.add_ops(TransactionOps::DDL {
1367                            ops: vec![],
1368                            state,
1369                            revision,
1370                            side_effects: vec![],
1371                            snapshot: None,
1372                        }) {
1373                            return ctx.retire(Err(err));
1374                        }
1375                    }
1376
1377                    // Statements below must by run singly (in Started).
1378                    Statement::AlterCluster(_)
1379                    | Statement::AlterConnection(_)
1380                    | Statement::AlterDefaultPrivileges(_)
1381                    | Statement::AlterIndex(_)
1382                    | Statement::AlterMaterializedViewApplyReplacement(_)
1383                    | Statement::AlterSetCluster(_)
1384                    | Statement::AlterOwner(_)
1385                    | Statement::AlterRetainHistory(_)
1386                    | Statement::AlterRole(_)
1387                    | Statement::AlterSecret(_)
1388                    | Statement::AlterSink(_)
1389                    | Statement::AlterSource(_)
1390                    | Statement::AlterSystemReset(_)
1391                    | Statement::AlterSystemResetAll(_)
1392                    | Statement::AlterSystemSet(_)
1393                    | Statement::AlterTableAddColumn(_)
1394                    | Statement::AlterNetworkPolicy(_)
1395                    | Statement::CreateCluster(_)
1396                    | Statement::CreateClusterReplica(_)
1397                    | Statement::CreateConnection(_)
1398                    | Statement::CreateDatabase(_)
1399                    | Statement::CreateIndex(_)
1400                    | Statement::CreateMaterializedView(_)
1401                    | Statement::CreateRole(_)
1402                    | Statement::CreateSchema(_)
1403                    | Statement::CreateSecret(_)
1404                    | Statement::CreateSink(_)
1405                    | Statement::CreateSubsource(_)
1406                    | Statement::CreateTable(_)
1407                    | Statement::CreateType(_)
1408                    | Statement::CreateView(_)
1409                    | Statement::CreateWebhookSource(_)
1410                    | Statement::CreateNetworkPolicy(_)
1411                    | Statement::Delete(_)
1412                    | Statement::DropObjects(_)
1413                    | Statement::DropOwned(_)
1414                    | Statement::GrantPrivileges(_)
1415                    | Statement::GrantRole(_)
1416                    | Statement::Insert(_)
1417                    | Statement::ReassignOwned(_)
1418                    | Statement::RevokePrivileges(_)
1419                    | Statement::RevokeRole(_)
1420                    | Statement::Update(_)
1421                    | Statement::ValidateConnection(_)
1422                    | Statement::Comment(_)
1423                    | Statement::ExecuteUnitTest(_) => {
1424                        let txn_status = ctx.session_mut().transaction_mut();
1425
1426                        // If we're not in an implicit transaction and we could generate exactly one
1427                        // valid ExecuteResponse, we can delay execution until commit.
1428                        if !txn_status.is_implicit() {
1429                            // Statements whose tag is trivial (known only from an unexecuted statement) can
1430                            // be run in a special single-statement explicit mode. In this mode (`BEGIN;
1431                            // <stmt>; COMMIT`), we generate the expected tag from a successful <stmt>, but
1432                            // delay execution until `COMMIT`.
1433                            if let Ok(resp) = ExecuteResponse::try_from(&*stmt) {
1434                                if let Err(err) = txn_status
1435                                    .add_ops(TransactionOps::SingleStatement { stmt, params })
1436                                {
1437                                    ctx.retire(Err(err));
1438                                    return;
1439                                }
1440                                ctx.retire(Ok(resp));
1441                                return;
1442                            }
1443                        }
1444
1445                        // For statements that can carry sensitive material, redact
1446                        // literals so they don't leak into the error message, which
1447                        // is persisted in `mz_statement_execution_history` (matching
1448                        // how their SQL text is redacted). Other statements keep
1449                        // their literals for a clearer error.
1450                        let op = if StatementKind::from(&*stmt).is_sensitive() {
1451                            stmt.to_ast_string_redacted()
1452                        } else {
1453                            stmt.to_string()
1454                        };
1455                        return ctx.retire(Err(AdapterError::OperationProhibitsTransaction(op)));
1456                    }
1457                }
1458            }
1459        }
1460
1461        // DDLs must be planned and sequenced serially. We do not rely on PlanValidity checking
1462        // various IDs because we have incorrectly done that in the past. Attempt to acquire the
1463        // ddl lock. The lock is stashed in the ConnMeta which is dropped at transaction end. If
1464        // acquired, proceed with sequencing. If not, enqueue and return. This logic assumes that
1465        // Coordinator::clear_transaction is correctly called when session transactions are ended
1466        // because that function will release the held lock from active_conns.
1467        if Self::must_serialize_ddl(&stmt, &ctx) {
1468            if let Ok(guard) = self.serialized_ddl.try_lock_owned() {
1469                let prev = self
1470                    .active_conns
1471                    .get_mut(ctx.session().conn_id())
1472                    .expect("connection must exist")
1473                    .deferred_lock
1474                    .replace(guard);
1475                assert!(
1476                    prev.is_none(),
1477                    "connections should have at most one lock guard"
1478                );
1479            } else {
1480                if self
1481                    .active_conns
1482                    .get(ctx.session().conn_id())
1483                    .expect("connection must exist")
1484                    .deferred_lock
1485                    .is_some()
1486                {
1487                    // This session *already* has the lock, and incorrectly tried to execute another
1488                    // DDL while still holding the lock, violating the assumption documented above.
1489                    // This is an internal error, probably in some AdapterClient user (pgwire or
1490                    // http). Because the session is now in some unexpected state, return an error
1491                    // which should cause the AdapterClient user to fail the transaction.
1492                    // (Terminating the connection is maybe what we would prefer to do, but is not
1493                    // currently a thing we can do from the coordinator: calling handle_terminate
1494                    // cleans up Coordinator state for the session but doesn't inform the
1495                    // AdapterClient that the session should terminate.)
1496                    soft_panic_or_log!(
1497                        "session {} attempted to get ddl lock while already owning it",
1498                        ctx.session().conn_id()
1499                    );
1500                    ctx.retire(Err(AdapterError::Internal(
1501                        "session attempted to get ddl lock while already owning it".to_string(),
1502                    )));
1503                    return;
1504                }
1505                self.serialized_ddl.push_back(DeferredPlanStatement {
1506                    ctx,
1507                    ps: PlanStatement::Statement { stmt, params },
1508                });
1509                return;
1510            }
1511        }
1512
1513        let catalog = self.catalog();
1514        let catalog = catalog.for_session(ctx.session());
1515        let original_stmt = Arc::clone(&stmt);
1516        // `resolved_ids` should be derivable from `stmt`. If `stmt` is transformed to remove/add
1517        // IDs, then `resolved_ids` should be updated to also remove/add those IDs.
1518        let (stmt, mut resolved_ids) = match mz_sql::names::resolve(&catalog, (*stmt).clone()) {
1519            Ok(resolved) => resolved,
1520            Err(e) => return ctx.retire(Err(e.into())),
1521        };
1522        // N.B. The catalog can change during purification so we must validate that the dependencies still exist after
1523        // purification.  This should be done back on the main thread.
1524        // We do the validation:
1525        //   - In the handler for `Message::PurifiedStatementReady`, before we handle the purified statement.
1526        // If we add special handling for more types of `Statement`s, we'll need to ensure similar verification
1527        // occurs.
1528        let (stmt, resolved_ids) = match stmt {
1529            // Various statements must be purified off the main coordinator thread of control.
1530            stmt if Self::must_spawn_purification(&stmt) => {
1531                let internal_cmd_tx = self.internal_cmd_tx.clone();
1532                let conn_id = ctx.session().conn_id().clone();
1533                let catalog = self.owned_catalog();
1534                let now = self.now();
1535                let otel_ctx = OpenTelemetryContext::obtain();
1536                let current_storage_configuration = self.controller.storage.config().clone();
1537                task::spawn(|| format!("purify:{conn_id}"), async move {
1538                    let conn_catalog = catalog.for_session(ctx.session());
1539
1540                    // Checks if the session is authorized to purify a statement. Usually
1541                    // authorization is checked after planning, however purification happens before
1542                    // planning, which may require the use of some connections and secrets.
1543                    if let Err(e) = rbac::check_usage(
1544                        &conn_catalog,
1545                        ctx.session(),
1546                        &resolved_ids,
1547                        &CREATE_ITEM_USAGE,
1548                    ) {
1549                        return ctx.retire(Err(e.into()));
1550                    }
1551
1552                    let (result, cluster_id) = mz_sql::pure::purify_statement(
1553                        conn_catalog,
1554                        now,
1555                        stmt,
1556                        &current_storage_configuration,
1557                    )
1558                    .await;
1559                    let result = result.map_err(|e| e.into());
1560                    let dependency_ids = resolved_ids.items().copied().collect();
1561                    let plan_validity = PlanValidity::new(
1562                        &catalog,
1563                        dependency_ids,
1564                        cluster_id,
1565                        None,
1566                        ctx.session().role_metadata().clone(),
1567                    );
1568                    // It is not an error for purification to complete after `internal_cmd_rx` is dropped.
1569                    let result = internal_cmd_tx.send(Message::PurifiedStatementReady(
1570                        PurifiedStatementReady {
1571                            ctx,
1572                            result,
1573                            params,
1574                            plan_validity,
1575                            original_stmt,
1576                            otel_ctx,
1577                        },
1578                    ));
1579                    if let Err(e) = result {
1580                        tracing::warn!("internal_cmd_rx dropped before we could send: {:?}", e);
1581                    }
1582                });
1583                return;
1584            }
1585
1586            // `CREATE SUBSOURCE` statements are disallowed for users and are only generated
1587            // automatically as part of purification
1588            Statement::CreateSubsource(_) => {
1589                ctx.retire(Err(AdapterError::Unsupported(
1590                    "CREATE SUBSOURCE statements",
1591                )));
1592                return;
1593            }
1594
1595            Statement::CreateMaterializedView(mut cmvs) => {
1596                // `CREATE MATERIALIZED VIEW ... AS OF ...` syntax is disallowed for users and is
1597                // only used for storing initial frontiers in the catalog.
1598                if cmvs.as_of.is_some() {
1599                    return ctx.retire(Err(AdapterError::Unsupported(
1600                        "CREATE MATERIALIZED VIEW ... AS OF statements",
1601                    )));
1602                }
1603
1604                let mz_now = match self
1605                    .resolve_mz_now_for_create_materialized_view(
1606                        &cmvs,
1607                        &resolved_ids,
1608                        ctx.session_mut(),
1609                        true,
1610                    )
1611                    .await
1612                {
1613                    Ok(mz_now) => mz_now,
1614                    Err(e) => return ctx.retire(Err(e)),
1615                };
1616
1617                let catalog = self.catalog().for_session(ctx.session());
1618
1619                purify_create_materialized_view_options(
1620                    catalog,
1621                    mz_now,
1622                    &mut cmvs,
1623                    &mut resolved_ids,
1624                );
1625
1626                let purified_stmt =
1627                    Statement::CreateMaterializedView(CreateMaterializedViewStatement::<Aug> {
1628                        if_exists: cmvs.if_exists,
1629                        name: cmvs.name,
1630                        columns: cmvs.columns,
1631                        replacement_for: cmvs.replacement_for,
1632                        in_cluster: cmvs.in_cluster,
1633                        in_cluster_replica: cmvs.in_cluster_replica,
1634                        query: cmvs.query,
1635                        with_options: cmvs.with_options,
1636                        as_of: None,
1637                    });
1638
1639                // (Purifying CreateMaterializedView doesn't happen async, so no need to send
1640                // `Message::PurifiedStatementReady` here.)
1641                (purified_stmt, resolved_ids)
1642            }
1643
1644            Statement::ExplainPlan(ExplainPlanStatement {
1645                stage,
1646                with_options,
1647                format,
1648                explainee: Explainee::CreateMaterializedView(box_cmvs, broken),
1649            }) => {
1650                let mut cmvs = *box_cmvs;
1651                let mz_now = match self
1652                    .resolve_mz_now_for_create_materialized_view(
1653                        &cmvs,
1654                        &resolved_ids,
1655                        ctx.session_mut(),
1656                        false,
1657                    )
1658                    .await
1659                {
1660                    Ok(mz_now) => mz_now,
1661                    Err(e) => return ctx.retire(Err(e)),
1662                };
1663
1664                let catalog = self.catalog().for_session(ctx.session());
1665
1666                purify_create_materialized_view_options(
1667                    catalog,
1668                    mz_now,
1669                    &mut cmvs,
1670                    &mut resolved_ids,
1671                );
1672
1673                let purified_stmt = Statement::ExplainPlan(ExplainPlanStatement {
1674                    stage,
1675                    with_options,
1676                    format,
1677                    explainee: Explainee::CreateMaterializedView(Box::new(cmvs), broken),
1678                });
1679
1680                (purified_stmt, resolved_ids)
1681            }
1682
1683            // All other statements are handled immediately.
1684            _ => (stmt, resolved_ids),
1685        };
1686
1687        match self.plan_statement(ctx.session(), stmt, &params, &resolved_ids) {
1688            Ok((plan, sql_impl_ids)) => {
1689                self.sequence_plan(ctx, plan, resolved_ids, sql_impl_ids)
1690                    .await
1691            }
1692            Err(e) => ctx.retire(Err(e)),
1693        }
1694    }
1695
1696    /// Whether the statement must be serialized and is DDL.
1697    fn must_serialize_ddl(stmt: &Statement<Raw>, ctx: &ExecuteContext) -> bool {
1698        // Non-DDL is not serialized here.
1699        if !StatementClassification::from(&*stmt).is_ddl() {
1700            return false;
1701        }
1702        // Off-thread, pre-planning purification can perform arbitrarily slow network calls so must
1703        // not be serialized. These all use PlanValidity for their checking, and we must ensure
1704        // those checks are sufficient.
1705        if Self::must_spawn_purification(stmt) {
1706            return false;
1707        }
1708
1709        // Statements that support multiple DDLs in a single transaction aren't serialized here.
1710        // Their operations are serialized when applied to the catalog, guaranteeing that any
1711        // off-thread DDLs concurrent with a multiple DDL transaction will have a serial order.
1712        if ctx.session.transaction().is_ddl() {
1713            return false;
1714        }
1715
1716        // Some DDL is exempt. It is not great that we are matching on Statements here because
1717        // different plans can be produced from the same top-level statement type (i.e., `ALTER
1718        // CONNECTION ROTATE KEYS`). But the whole point of this is to prevent things from being
1719        // planned in the first place, so we accept the abstraction leak.
1720        match stmt {
1721            // Secrets have a small and understood set of dependencies, and their off-thread work
1722            // interacts with k8s.
1723            Statement::AlterSecret(_) => false,
1724            Statement::CreateSecret(_) => false,
1725            Statement::AlterConnection(AlterConnectionStatement { actions, .. })
1726                if actions
1727                    .iter()
1728                    .all(|action| matches!(action, AlterConnectionAction::RotateKeys)) =>
1729            {
1730                false
1731            }
1732
1733            // The off-thread work that altering a cluster may do (waiting for replicas to spin-up),
1734            // does not affect its catalog names or ids and so is safe to not serialize. This could
1735            // change the set of replicas that exist. For queries that name replicas or use the
1736            // current_replica session var, the `replica_id` field of `PlanValidity` serves to
1737            // ensure that those replicas exist during the query finish stage. Additionally, that
1738            // work can take hours (configured by the user), so would also be a bad experience for
1739            // users.
1740            Statement::AlterCluster(_) => false,
1741
1742            // `ALTER SINK` waits for the sink to make enough progress for a clean cutover to the
1743            // new configuration. If the sink is stalled, it may block forever. Checks in
1744            // sequencing ensure that the operation fails if any one of these happens concurrently:
1745            //   * the sink is dropped
1746            //   * the source relation is dropped
1747            //   * another `ALTER SINK` for the same sink is applied first
1748            Statement::AlterSink(_) => false,
1749
1750            // `ALTER MATERIALIZED VIEW ... APPLY REPLACEMENT` waits for the target MV to make
1751            // enough progress for a clean cutover. If the target MV is stalled, it may block
1752            // forever. Checks in sequencing ensure the operation fails if any of these happens
1753            // concurrently:
1754            //   * the target MV is dropped
1755            //   * the replacement MV is dropped
1756            Statement::AlterMaterializedViewApplyReplacement(_) => false,
1757
1758            // Everything else must be serialized.
1759            _ => true,
1760        }
1761    }
1762
1763    /// Whether the statement must be purified off of the Coordinator thread.
1764    fn must_spawn_purification<A: AstInfo>(stmt: &Statement<A>) -> bool {
1765        // `CREATE` and `ALTER` `SOURCE` and `SINK` statements must be purified off the main
1766        // coordinator thread.
1767        if !matches!(
1768            stmt,
1769            Statement::CreateSource(_)
1770                | Statement::AlterSource(_)
1771                | Statement::CreateSink(_)
1772                | Statement::CreateTableFromSource(_)
1773        ) {
1774            return false;
1775        }
1776
1777        // However `ALTER SOURCE RETAIN HISTORY` should be excluded from off-thread purification.
1778        if let Statement::AlterSource(stmt) = stmt {
1779            let names: Vec<CreateSourceOptionName> = match &stmt.action {
1780                AlterSourceAction::SetOptions(options) => {
1781                    options.iter().map(|o| o.name.clone()).collect()
1782                }
1783                AlterSourceAction::ResetOptions(names) => names.clone(),
1784                _ => vec![],
1785            };
1786            if !names.is_empty()
1787                && names
1788                    .iter()
1789                    .all(|n| matches!(n, CreateSourceOptionName::RetainHistory))
1790            {
1791                return false;
1792            }
1793        }
1794
1795        true
1796    }
1797
1798    /// Chooses a timestamp for `mz_now()`, if `mz_now()` occurs in a REFRESH option of the
1799    /// materialized view. Additionally, if `acquire_read_holds` is true and the MV has any REFRESH
1800    /// option, this function grabs read holds at the earliest possible time on input collections
1801    /// that might be involved in the MV.
1802    ///
1803    /// Note that this is NOT what handles `mz_now()` in the query part of the MV. (handles it only
1804    /// in `with_options`).
1805    ///
1806    /// (Note that the chosen timestamp won't be the same timestamp as the system table inserts,
1807    /// unfortunately.)
1808    async fn resolve_mz_now_for_create_materialized_view(
1809        &mut self,
1810        cmvs: &CreateMaterializedViewStatement<Aug>,
1811        resolved_ids: &ResolvedIds,
1812        session: &Session,
1813        acquire_read_holds: bool,
1814    ) -> Result<Option<Timestamp>, AdapterError> {
1815        if cmvs
1816            .with_options
1817            .iter()
1818            .any(|wo| matches!(wo.value, Some(WithOptionValue::Refresh(..))))
1819        {
1820            let catalog = self.catalog().for_session(session);
1821            let cluster = mz_sql::plan::resolve_cluster_for_materialized_view(&catalog, cmvs)?;
1822            let ids = self
1823                .index_oracle(cluster)
1824                .sufficient_collections(resolved_ids.collections().copied());
1825
1826            // If there is any REFRESH option, then acquire read holds. (Strictly speaking, we'd
1827            // need this only if there is a `REFRESH AT`, not for `REFRESH EVERY`, because later
1828            // we want to check the AT times against the read holds that we acquire here. But
1829            // we do it for any REFRESH option, to avoid having so many code paths doing different
1830            // things.)
1831            //
1832            // It's important that we acquire read holds _before_ we determine the least valid read.
1833            // Otherwise, we're not guaranteed that the since frontier doesn't
1834            // advance forward from underneath us.
1835            let read_holds = self.acquire_read_holds(&ids);
1836
1837            // Does `mz_now()` occur?
1838            let mz_now_ts = if cmvs
1839                .with_options
1840                .iter()
1841                .any(materialized_view_option_contains_temporal)
1842            {
1843                let timeline_context = self
1844                    .catalog()
1845                    .validate_timeline_context(resolved_ids.collections().copied())?;
1846
1847                // We default to EpochMilliseconds, similarly to `determine_timestamp_for`,
1848                // but even in the TimestampIndependent case.
1849                // Note that we didn't accurately decide whether we are TimestampDependent
1850                // or TimestampIndependent, because for this we'd need to also check whether
1851                // `query.contains_temporal()`, similarly to how `peek_stage_validate` does.
1852                // However, this doesn't matter here, as we are just going to default to
1853                // EpochMilliseconds in both cases.
1854                let timeline = timeline_context
1855                    .timeline()
1856                    .unwrap_or(&Timeline::EpochMilliseconds);
1857
1858                // Let's start with the timestamp oracle read timestamp.
1859                let mut timestamp = self.get_timestamp_oracle(timeline).read_ts().await;
1860
1861                // If `least_valid_read` is later than the oracle, then advance to that time.
1862                // If we didn't do this, then there would be a danger of missing the first refresh,
1863                // which might cause the materialized view to be unreadable for hours. This might
1864                // be what was happening here:
1865                // https://github.com/MaterializeInc/database-issues/issues/7265#issuecomment-1931856361
1866                //
1867                // In the long term, it would be good to actually block the MV creation statement
1868                // until `least_valid_read`. https://github.com/MaterializeInc/database-issues/issues/7504
1869                // Without blocking, we have the problem that a REFRESH AT CREATION is not linearized
1870                // with the CREATE MATERIALIZED VIEW statement, in the sense that a query from the MV
1871                // after its creation might see input changes that happened after the CRATE MATERIALIZED
1872                // VIEW statement returned.
1873                let oracle_timestamp = timestamp;
1874                let least_valid_read = read_holds.least_valid_read();
1875                timestamp.advance_by(least_valid_read.borrow());
1876
1877                if oracle_timestamp != timestamp {
1878                    warn!(%cmvs.name, %oracle_timestamp, %timestamp, "REFRESH MV's inputs are not readable at the oracle read ts");
1879                }
1880
1881                info!("Resolved `mz_now()` to {timestamp} for REFRESH MV");
1882                Ok(Some(timestamp))
1883            } else {
1884                Ok(None)
1885            };
1886
1887            // NOTE: The Drop impl of ReadHolds makes sure that the hold is
1888            // released when we don't use it.
1889            if acquire_read_holds {
1890                self.store_transaction_read_holds(session.conn_id().clone(), read_holds);
1891            }
1892
1893            mz_now_ts
1894        } else {
1895            Ok(None)
1896        }
1897    }
1898
1899    /// Instruct the dataflow layer to cancel any ongoing, interactive work for
1900    /// the named `conn_id` if the correct secret key is specified.
1901    ///
1902    /// Note: Here we take a [`ConnectionIdType`] as opposed to an owned
1903    /// `ConnectionId` because this method gets called by external clients when
1904    /// they request to cancel a request.
1905    #[mz_ore::instrument(level = "debug")]
1906    async fn handle_cancel(&mut self, conn_id: ConnectionIdType, secret_key: u32) {
1907        if let Some((id_handle, conn_meta)) = self.active_conns.get_key_value(&conn_id) {
1908            // If the secret key specified by the client doesn't match the
1909            // actual secret key for the target connection, we treat this as a
1910            // rogue cancellation request and ignore it.
1911            if conn_meta.secret_key != secret_key {
1912                return;
1913            }
1914
1915            // Now that we've verified the secret key, this is a privileged
1916            // cancellation request. We can upgrade the raw connection ID to a
1917            // proper `IdHandle`.
1918            self.handle_privileged_cancel(id_handle.clone()).await;
1919        }
1920    }
1921
1922    /// Unconditionally instructs the dataflow layer to cancel any ongoing,
1923    /// interactive work for the named `conn_id`.
1924    #[mz_ore::instrument(level = "debug")]
1925    pub(crate) async fn handle_privileged_cancel(&mut self, conn_id: ConnectionId) {
1926        let mut maybe_ctx = None;
1927
1928        // Cancel pending writes. There is at most one pending write per session.
1929        let pending_write_idx = self.pending_writes.iter().position(|pending_write_txn| {
1930            matches!(pending_write_txn, PendingWriteTxn::User {
1931                responder: UserWriteResponder::Session(PendingTxn { ctx, .. }),
1932                ..
1933            } if *ctx.session().conn_id() == conn_id)
1934        });
1935        if let Some(idx) = pending_write_idx {
1936            if let PendingWriteTxn::User {
1937                responder: UserWriteResponder::Session(PendingTxn { ctx, .. }),
1938                ..
1939            } = self.pending_writes.remove(idx)
1940            {
1941                maybe_ctx = Some(ctx);
1942            }
1943        }
1944
1945        // Cancel deferred writes.
1946        if let Some(write_op) = self.deferred_write_ops.remove(&conn_id) {
1947            maybe_ctx = Some(write_op.into_ctx());
1948        }
1949
1950        // Cancel deferred statements.
1951        let deferred_ddl_idx = self
1952            .serialized_ddl
1953            .iter()
1954            .position(|deferred| *deferred.ctx.session().conn_id() == conn_id);
1955        if let Some(idx) = deferred_ddl_idx {
1956            let deferred = self
1957                .serialized_ddl
1958                .remove(idx)
1959                .expect("known to exist from call to `position` above");
1960            maybe_ctx = Some(deferred.ctx);
1961        }
1962
1963        // Cancel reads waiting on being linearized. There is at most one linearized read per
1964        // session.
1965        if let Some(pending_read_txn) = self.pending_linearize_read_txns.remove(&conn_id) {
1966            let ctx = pending_read_txn.take_context();
1967            maybe_ctx = Some(ctx);
1968        }
1969
1970        if let Some(ctx) = maybe_ctx {
1971            ctx.retire(Err(AdapterError::Canceled));
1972        }
1973
1974        self.cancel_pending_peeks(&conn_id);
1975        self.cancel_pending_watchsets(&conn_id);
1976        let retire_notify = self.cancel_compute_sinks_for_conn(&conn_id).await;
1977        // SQL cancellation has no success response to delay. Each subscribe
1978        // still waits for its own retraction before it observes retirement.
1979        drop(retire_notify);
1980        self.cancel_cluster_reconfigurations_for_conn(&conn_id)
1981            .await;
1982        self.cancel_pending_copy(&conn_id);
1983        if let Some((tx, _rx)) = self.connection_cancel_watches.get_mut(&conn_id) {
1984            let _ = tx.send(true);
1985        }
1986    }
1987
1988    /// Handle termination of a client session.
1989    ///
1990    /// This cleans up any state in the coordinator associated with the session.
1991    ///
1992    /// Must only be called for connections that completed a successful
1993    /// startup, i.e. that are present in `active_conns`. A failed startup
1994    /// leaves no state behind (see `handle_startup_inner`), so no Terminate
1995    /// may be sent for it.
1996    #[mz_ore::instrument(level = "debug")]
1997    async fn handle_terminate(&mut self, conn_id: ConnectionId) {
1998        // If the session doesn't exist in `active_conns`, then this method will panic later on.
1999        // Instead we explicitly panic here while dumping the entire Coord to the logs to help
2000        // debug. This panic is very infrequent so we want as much information as possible.
2001        // See https://github.com/MaterializeInc/database-issues/issues/5627.
2002        assert!(
2003            self.active_conns.contains_key(&conn_id),
2004            "unknown connection: {conn_id:?}\n\n{self:?}"
2005        );
2006
2007        // We do not need to call clear_transaction here because there are no side effects to run
2008        // based on any session transaction state.
2009        let retire_notify = self.clear_connection(&conn_id).await;
2010        // Termination has no statement response to delay. Each subscribe still
2011        // waits for its own retraction before it observes retirement.
2012        drop(retire_notify);
2013
2014        self.drop_temp_items(&conn_id).await;
2015        // Only call catalog_mut() if a temporary schema actually exists for this connection.
2016        // This avoids an expensive Arc::make_mut clone for the common case where the connection
2017        // never created any temporary objects.
2018        if self.catalog().state().has_temporary_schema(&conn_id) {
2019            self.catalog_mut()
2020                .drop_temporary_schema(&conn_id)
2021                .unwrap_or_terminate("unable to drop temporary schema");
2022        }
2023        let conn = self.active_conns.remove(&conn_id).expect("conn must exist");
2024        let session_type = metrics::session_type_label_value(conn.user());
2025        self.metrics
2026            .active_sessions
2027            .with_label_values(&[session_type])
2028            .dec();
2029        self.cancel_pending_peeks(conn.conn_id());
2030        self.cancel_pending_watchsets(&conn_id);
2031        self.cancel_pending_copy(&conn_id);
2032        self.end_session_for_statement_logging(conn.uuid());
2033
2034        // Queue the builtin table update, but do not wait for it to complete. We explicitly do
2035        // this to prevent blocking the Coordinator in the case that a lot of connections are
2036        // closed at once, which occurs regularly in some workflows.
2037        let update = self
2038            .catalog()
2039            .state()
2040            .pack_session_update(&conn, Diff::MINUS_ONE);
2041        let update = self.catalog().state().resolve_builtin_table_update(update);
2042
2043        let _builtin_update_notify = self.builtin_table_update().defer(vec![update]);
2044    }
2045
2046    /// Returns the necessary metadata for appending to a webhook source, and a channel to send
2047    /// rows.
2048    #[mz_ore::instrument(level = "debug")]
2049    fn handle_get_webhook(
2050        &mut self,
2051        database: String,
2052        schema: String,
2053        name: String,
2054        tx: oneshot::Sender<Result<AppendWebhookResponse, AppendWebhookError>>,
2055    ) {
2056        /// Attempts to resolve a Webhook source from a provided `database.schema.name` path.
2057        ///
2058        /// Returns a struct that can be used to append data to the underlying storate collection, and the
2059        /// types we should cast the request to.
2060        fn resolve(
2061            coord: &mut Coordinator,
2062            database: String,
2063            schema: String,
2064            name: String,
2065        ) -> Result<AppendWebhookResponse, PartialItemName> {
2066            // Resolve our collection.
2067            let name = PartialItemName {
2068                database: Some(database),
2069                schema: Some(schema),
2070                item: name,
2071            };
2072            let Ok(entry) = coord
2073                .catalog()
2074                .resolve_entry(None, &vec![], &name, &SYSTEM_CONN_ID)
2075            else {
2076                return Err(name);
2077            };
2078
2079            // Webhooks can be created with `CREATE SOURCE` or `CREATE TABLE`.
2080            let (data_source, desc, global_id) = match entry.item() {
2081                CatalogItem::Source(Source {
2082                    data_source: data_source @ DataSourceDesc::Webhook { .. },
2083                    desc,
2084                    global_id,
2085                    ..
2086                }) => (data_source, desc.clone(), *global_id),
2087                CatalogItem::Table(
2088                    table @ Table {
2089                        desc,
2090                        data_source:
2091                            TableDataSource::DataSource {
2092                                desc: data_source @ DataSourceDesc::Webhook { .. },
2093                                ..
2094                            },
2095                        ..
2096                    },
2097                ) => (data_source, desc.latest(), table.global_id_writes()),
2098                _ => return Err(name),
2099            };
2100
2101            let DataSourceDesc::Webhook {
2102                validate_using,
2103                body_format,
2104                headers,
2105                ..
2106            } = data_source
2107            else {
2108                mz_ore::soft_panic_or_log!("programming error! checked above for webhook");
2109                return Err(name);
2110            };
2111            let body_format = body_format.clone();
2112            let header_tys = headers.clone();
2113
2114            // Assert we have one column for the body, and how ever many are required for
2115            // the headers.
2116            let num_columns = headers.num_columns() + 1;
2117            mz_ore::soft_assert_or_log!(
2118                desc.arity() <= num_columns,
2119                "expected at most {} columns, but got {}",
2120                num_columns,
2121                desc.arity()
2122            );
2123
2124            // Double check that the body column of the webhook source matches the type
2125            // we're about to deserialize as.
2126            let body_column = desc
2127                .get_by_name(&"body".into())
2128                .map(|(_idx, ty)| ty.clone())
2129                .ok_or_else(|| name.clone())?;
2130            assert!(!body_column.nullable, "webhook body column is nullable!?");
2131            assert_eq!(body_column.scalar_type, SqlScalarType::from(body_format));
2132
2133            // Create a validator that can be called to validate a webhook request.
2134            let validator = validate_using.as_ref().map(|v| {
2135                let validation = v.clone();
2136                AppendWebhookValidator::new(validation, coord.caching_secrets_reader.clone())
2137            });
2138
2139            // Get a channel so we can queue updates to be written.
2140            let row_tx = coord
2141                .controller
2142                .storage
2143                .monotonic_appender(global_id)
2144                .map_err(|_| name.clone())?;
2145            let stats = coord
2146                .controller
2147                .storage
2148                .webhook_statistics(global_id)
2149                .map_err(|_| name)?;
2150            let invalidator = coord
2151                .active_webhooks
2152                .entry(entry.id())
2153                .or_insert_with(WebhookAppenderInvalidator::new);
2154            let tx = WebhookAppender::new(row_tx, invalidator.guard(), stats);
2155
2156            Ok(AppendWebhookResponse {
2157                tx,
2158                body_format,
2159                header_tys,
2160                validator,
2161            })
2162        }
2163
2164        let response = resolve(self, database, schema, name).map_err(|name| {
2165            AppendWebhookError::UnknownWebhook {
2166                database: name.database.expect("provided"),
2167                schema: name.schema.expect("provided"),
2168                name: name.item,
2169            }
2170        });
2171        let _ = tx.send(response);
2172    }
2173
2174    /// Handle registration of a frontend peek, for statement logging and query cancellation
2175    /// handling.
2176    fn handle_register_frontend_peek(
2177        &mut self,
2178        uuid: Uuid,
2179        conn_id: ConnectionId,
2180        cluster_id: mz_controller_types::ClusterId,
2181        depends_on: BTreeSet<GlobalId>,
2182        is_fast_path: bool,
2183        watch_set: Option<WatchSetCreation>,
2184        tx: oneshot::Sender<Result<(), AdapterError>>,
2185    ) {
2186        let statement_logging_id = watch_set.as_ref().map(|ws| ws.logging_id);
2187        if let Some(ws) = watch_set {
2188            if let Err(e) = self.install_peek_watch_sets(conn_id.clone(), ws) {
2189                let _ = tx.send(Err(
2190                    AdapterError::concurrent_dependency_drop_from_watch_set_install_error(e),
2191                ));
2192                return;
2193            }
2194        }
2195
2196        // Store the peek in pending_peeks for later retrieval when results arrive
2197        self.pending_peeks.insert(
2198            uuid,
2199            PendingPeek {
2200                conn_id: conn_id.clone(),
2201                cluster_id,
2202                depends_on,
2203                ctx_extra: ExecuteContextGuard::new(
2204                    statement_logging_id,
2205                    self.internal_cmd_tx.clone(),
2206                ),
2207                is_fast_path,
2208            },
2209        );
2210
2211        // Also track it by connection ID for cancellation support
2212        self.client_pending_peeks
2213            .entry(conn_id)
2214            .or_default()
2215            .insert(uuid, cluster_id);
2216
2217        let _ = tx.send(Ok(()));
2218    }
2219
2220    /// Handles [`Command::UnregisterFrontendPeek`]; see its documentation for
2221    /// the end-of-execution ownership contract.
2222    fn handle_unregister_frontend_peek(
2223        &mut self,
2224        uuid: Uuid,
2225        reason: StatementEndedExecutionReason,
2226        tx: oneshot::Sender<()>,
2227    ) {
2228        // A peek missing from `pending_peeks` was already retired, and its end
2229        // logged, by a concurrent teardown.
2230        if let Some(pending_peek) = self.remove_pending_peek(&uuid) {
2231            self.retire_execution(reason, pending_peek.ctx_extra.defuse());
2232        }
2233        let _ = tx.send(());
2234    }
2235}