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