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