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