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