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