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