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