Skip to main content

mz_adapter/
client.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
10use std::borrow::Cow;
11use std::collections::BTreeMap;
12use std::fmt::{Debug, Display, Formatter};
13use std::future::Future;
14use std::pin::{self, Pin};
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17
18use anyhow::bail;
19use chrono::{DateTime, Utc};
20use derivative::Derivative;
21use futures::{Stream, StreamExt};
22use itertools::Itertools;
23use mz_adapter_types::connection::{ConnectionId, ConnectionIdType};
24use mz_auth::password::Password;
25use mz_auth::{Authenticated, AuthenticatorKind};
26use mz_build_info::BuildInfo;
27use mz_compute_types::ComputeInstanceId;
28use mz_ore::channel::OneshotReceiverExt;
29use mz_ore::collections::CollectionExt;
30use mz_ore::id_gen::{IdAllocator, IdAllocatorInnerBitSet, MAX_ORG_ID, org_id_conn_bits};
31use mz_ore::instrument;
32use mz_ore::now::{EpochMillis, NowFn, to_datetime};
33use mz_ore::str::StrExt;
34use mz_ore::task::AbortOnDropHandle;
35use mz_ore::thread::JoinOnDropHandle;
36use mz_ore::tracing::OpenTelemetryContext;
37use mz_repr::user::InternalUserMetadata;
38use mz_repr::{CatalogItemId, ColumnIndex, SqlScalarType};
39use mz_sql::ast::{Raw, Statement};
40use mz_sql::catalog::{EnvironmentId, SessionCatalog};
41use mz_sql::session::hint::ApplicationNameHint;
42use mz_sql::session::metadata::SessionMetadata;
43use mz_sql::session::user::SUPPORT_USER;
44use mz_sql::session::vars::{
45    CLUSTER, ENABLE_FRONTEND_PEEK_SEQUENCING, OwnedVarInput, SystemVars, Var,
46};
47use mz_sql_parser::ast::display::AstDisplay;
48use mz_sql_parser::parser::{ParserStatementError, StatementParseResult};
49use prometheus::Histogram;
50use serde_json::json;
51use tokio::sync::{mpsc, oneshot};
52use tracing::{debug, error};
53use uuid::Uuid;
54
55use crate::catalog::Catalog;
56use crate::command::{
57    CatalogDump, CatalogSnapshot, Command, CopyFromStdinWriter, ExecuteResponse, Response,
58    SASLChallengeResponse, SASLVerifyProofResponse, SuperuserAttribute,
59};
60use crate::config::{ScopedParameters, ScopedParametersScope, SystemParameterFrontend};
61use crate::coord::{Coordinator, ExecuteContextGuard};
62use crate::error::AdapterError;
63use crate::metrics::{self, Metrics};
64use crate::session::{
65    EndTransactionAction, PreparedStatement, Session, SessionConfig, StateRevision, TransactionId,
66};
67use crate::statement_logging::{StatementEndedExecutionReason, StatementExecutionStrategy};
68use crate::telemetry::{self, EventDetails, SegmentClientExt, StatementFailureType};
69use crate::webhook::AppendWebhookResponse;
70use crate::{AdapterNotice, AppendWebhookError, PeekClient, PeekResponseUnary, StartupResponse};
71
72/// A handle to a running coordinator.
73///
74/// The coordinator runs on its own thread. Dropping the handle will wait for
75/// the coordinator's thread to exit, which will only occur after all
76/// outstanding [`Client`]s for the coordinator have dropped.
77pub struct Handle {
78    pub(crate) session_id: Uuid,
79    pub(crate) start_instant: Instant,
80    pub(crate) _thread: JoinOnDropHandle<()>,
81}
82
83impl Handle {
84    /// Returns the session ID associated with this coordinator.
85    ///
86    /// The session ID is generated on coordinator boot. It lasts for the
87    /// lifetime of the coordinator. Restarting the coordinator will result
88    /// in a new session ID.
89    pub fn session_id(&self) -> Uuid {
90        self.session_id
91    }
92
93    /// Returns the instant at which the coordinator booted.
94    pub fn start_instant(&self) -> Instant {
95        self.start_instant
96    }
97}
98
99/// A coordinator client.
100///
101/// A coordinator client is a simple handle to a communication channel with the
102/// coordinator. It can be cheaply cloned.
103///
104/// Clients keep the coordinator alive. The coordinator will not exit until all
105/// outstanding clients have dropped.
106#[derive(Debug, Clone)]
107pub struct Client {
108    build_info: &'static BuildInfo,
109    inner_cmd_tx: mpsc::UnboundedSender<(OpenTelemetryContext, Command)>,
110    id_alloc: IdAllocator<IdAllocatorInnerBitSet>,
111    now: NowFn,
112    metrics: Metrics,
113    environment_id: EnvironmentId,
114    segment_client: Option<mz_segment::Client>,
115}
116
117impl Client {
118    pub(crate) fn new(
119        build_info: &'static BuildInfo,
120        cmd_tx: mpsc::UnboundedSender<(OpenTelemetryContext, Command)>,
121        metrics: Metrics,
122        now: NowFn,
123        environment_id: EnvironmentId,
124        segment_client: Option<mz_segment::Client>,
125    ) -> Client {
126        // Connection ids are 32 bits and have 3 parts.
127        // 1. MSB bit is always 0 because these are interpreted as an i32, and it is possible some
128        //    driver will not handle a negative id since postgres has never produced one because it
129        //    uses process ids.
130        // 2. Next 12 bits are the lower 12 bits of the org id. This allows balancerd to route
131        //    incoming cancel messages to a subset of the environments.
132        // 3. Last 19 bits are random.
133        let env_lower = org_id_conn_bits(&environment_id.organization_id());
134        Client {
135            build_info,
136            inner_cmd_tx: cmd_tx,
137            id_alloc: IdAllocator::new(1, MAX_ORG_ID, env_lower),
138            now,
139            metrics,
140            environment_id,
141            segment_client,
142        }
143    }
144
145    /// Allocates a client for an incoming connection.
146    pub fn new_conn_id(&self) -> Result<ConnectionId, AdapterError> {
147        self.id_alloc.alloc().ok_or(AdapterError::IdExhaustionError)
148    }
149
150    /// Creates a new session associated with this client for the given user.
151    ///
152    /// It is the caller's responsibility to have authenticated the user.
153    /// We pass in an Authenticated marker as a guardrail to ensure the
154    /// user has authenticated with an authenticator before creating a session.
155    pub fn new_session(&self, config: SessionConfig, _authenticated: Authenticated) -> Session {
156        // We use the system clock to determine when a session connected to Materialize. This is not
157        // intended to be 100% accurate and correct, so we don't burden the timestamp oracle with
158        // generating a more correct timestamp.
159        Session::new(self.build_info, config, self.metrics().session_metrics())
160    }
161
162    /// Used by [mz_auth::AuthenticatorKind::Password]
163    /// to verify the provided user's password against the
164    /// stored credentials in the catalog.
165    pub async fn authenticate(
166        &self,
167        user: &String,
168        password: &Password,
169    ) -> Result<Authenticated, AdapterError> {
170        let (tx, rx) = oneshot::channel();
171        self.send(Command::AuthenticatePassword {
172            role_name: user.to_string(),
173            password: Some(password.clone()),
174            tx,
175        });
176        rx.await.expect("sender dropped")?;
177        Ok(Authenticated)
178    }
179
180    /// Used by [mz_auth::AuthenticatorKind::Sasl] for SASL-SCRAM authentication.
181    /// This is used prior to [Client::verify_sasl_proof].
182    pub async fn generate_sasl_challenge(
183        &self,
184        user: &String,
185        client_nonce: &String,
186    ) -> Result<SASLChallengeResponse, AdapterError> {
187        let (tx, rx) = oneshot::channel();
188        self.send(Command::AuthenticateGetSASLChallenge {
189            role_name: user.to_string(),
190            nonce: client_nonce.to_string(),
191            tx,
192        });
193        let response = rx.await.expect("sender dropped")?;
194        Ok(response)
195    }
196
197    /// Used by [mz_auth::AuthenticatorKind::Sasl] for SASL-SCRAM authentication.
198    /// This is used after [Client::generate_sasl_challenge].
199    pub async fn verify_sasl_proof(
200        &self,
201        user: &String,
202        proof: &String,
203        nonce: &String,
204        mock_hash: &String,
205    ) -> Result<(SASLVerifyProofResponse, Authenticated), AdapterError> {
206        let (tx, rx) = oneshot::channel();
207        self.send(Command::AuthenticateVerifySASLProof {
208            role_name: user.to_string(),
209            proof: proof.to_string(),
210            auth_message: nonce.to_string(),
211            mock_hash: mock_hash.to_string(),
212            tx,
213        });
214        let response = rx.await.expect("sender dropped")?;
215        Ok((response, Authenticated))
216    }
217
218    /// Checks if a role exists and has the `LOGIN` attribute.
219    pub async fn role_can_login(&self, role_name: &str) -> Result<(), AdapterError> {
220        let (tx, rx) = oneshot::channel();
221        self.send(Command::CheckRoleCanLogin {
222            role_name: role_name.to_string(),
223            tx,
224        });
225        rx.await.expect("sender dropped")
226    }
227
228    /// Upgrades this client to a session client.
229    ///
230    /// A session is a connection that has successfully negotiated parameters,
231    /// like the user. Most coordinator operations are available only after
232    /// upgrading a connection to a session.
233    ///
234    /// Returns a new client that is bound to the session and a response
235    /// containing various details about the startup.
236    #[mz_ore::instrument(level = "debug")]
237    pub async fn startup(&self, session: Session) -> Result<SessionClient, AdapterError> {
238        let user = session.user().clone();
239        let conn_id = session.conn_id().clone();
240        let secret_key = session.secret_key();
241        let uuid = session.uuid();
242        let client_ip = session.client_ip();
243        let application_name = session.application_name().into();
244        let notice_tx = session.retain_notice_transmitter();
245
246        let (tx, rx) = oneshot::channel();
247
248        // ~~SPOOKY ZONE~~
249        //
250        // This guard prevents a race where the startup command finishes, but the Future returned
251        // by this function is concurrently dropped, so we never create a `SessionClient` and thus
252        // never cleanup the initialized Session.
253        //
254        // NOTE: Terminate must only be sent for a successful startup. On a failed startup the
255        // Coordinator never registered the connection, so a Terminate for it would trip the
256        // Coordinator's "unknown connection" assertion. There is also nothing that needs
257        // cleaning up, see the invariant on `Coordinator::handle_startup_inner`.
258        let rx = rx.with_guard(|resp: Result<StartupResponse, _>| {
259            if resp.is_ok() {
260                self.send(Command::Terminate {
261                    conn_id: conn_id.clone(),
262                    tx: None,
263                });
264            }
265        });
266
267        self.send(Command::Startup {
268            tx,
269            user,
270            conn_id: conn_id.clone(),
271            secret_key,
272            uuid,
273            client_ip: client_ip.copied(),
274            application_name,
275            notice_tx,
276        });
277
278        // When startup fails, no need to call terminate (handle_startup does this). Delay creating
279        // the client until after startup to sidestep the panic in its `Drop` implementation.
280        let response = rx.await.expect("sender dropped")?;
281
282        // Create the client as soon as startup succeeds (before any await points) so its `Drop` can
283        // handle termination.
284        // Build the PeekClient with controller handles returned from startup.
285        let StartupResponse {
286            role_id,
287            write_notify,
288            session_defaults,
289            catalog,
290            storage_collections,
291            transient_id_gen,
292            optimizer_metrics,
293            persist_client,
294            statement_logging_frontend,
295            superuser_attribute,
296        } = response;
297
298        let peek_client = PeekClient::new(
299            self.clone(),
300            &catalog,
301            storage_collections,
302            transient_id_gen,
303            optimizer_metrics,
304            persist_client,
305            statement_logging_frontend,
306        );
307
308        let mut client = SessionClient {
309            inner: Some(self.clone()),
310            session: Some(session),
311            timeouts: Timeout::new(),
312            environment_id: self.environment_id.clone(),
313            segment_client: self.segment_client.clone(),
314            peek_client,
315            enable_frontend_peek_sequencing: false, // initialized below, once we have a ConnCatalog
316        };
317
318        let session = client.session();
319
320        // Apply the superuser attribute to the session's user if
321        // it exists.
322        if let SuperuserAttribute(Some(superuser)) = superuser_attribute {
323            session.apply_internal_user_metadata(InternalUserMetadata { superuser });
324        }
325
326        session.initialize_role_metadata(role_id);
327        let vars_mut = session.vars_mut();
328        for (name, val) in session_defaults {
329            if let Err(err) = vars_mut.set_default(&name, val.borrow()) {
330                // Note: erroring here is unexpected, but we don't want to panic if somehow our
331                // assumptions are wrong.
332                tracing::error!("failed to set peristed default, {err:?}");
333            }
334        }
335        session
336            .vars_mut()
337            .end_transaction(EndTransactionAction::Commit);
338
339        // Stash the future that notifies us of builtin table writes completing, we'll block on
340        // this future before allowing queries from this session against relevant relations.
341        //
342        // Note: We stash the future as opposed to waiting on it here to prevent blocking session
343        // creation on builtin table updates. This improves the latency for session creation and
344        // reduces scheduling load on any dataflows that read from these builtin relations, since
345        // it allows updates to be batched.
346        session.set_builtin_table_updates(write_notify);
347
348        let catalog = catalog.for_session(session);
349
350        let cluster_active = session.vars().cluster().to_string();
351        if session.vars().welcome_message() {
352            let cluster_info = if catalog.resolve_cluster(Some(&cluster_active)).is_err() {
353                format!("{cluster_active} (does not exist)")
354            } else {
355                cluster_active.to_string()
356            };
357
358            // Emit a welcome message, optimized for readability by humans using
359            // interactive tools. If you change the message, make sure that it
360            // formats nicely in both `psql` and the console's SQL shell.
361            session.add_notice(AdapterNotice::Welcome(format!(
362                "connected to Materialize v{}
363  Environment ID: {}
364  Region: {}
365  User: {}
366  Cluster: {}
367  Database: {}
368  {}
369  Session UUID: {}
370
371Issue a SQL query to get started. Need help?
372  View documentation: https://materialize.com/s/docs
373  Join our Slack community: https://materialize.com/s/chat
374    ",
375                session.vars().build_info().semver_version(),
376                self.environment_id,
377                self.environment_id.region(),
378                session.vars().user().name,
379                cluster_info,
380                session.vars().database(),
381                match session.vars().search_path() {
382                    [schema] => format!("Schema: {}", schema),
383                    schemas => format!(
384                        "Search path: {}",
385                        schemas.iter().map(|id| id.to_string()).join(", ")
386                    ),
387                },
388                session.uuid(),
389            )));
390        }
391
392        if session.vars().current_object_missing_warnings() {
393            if catalog.active_database().is_none() {
394                let db = session.vars().database().into();
395                session.add_notice(AdapterNotice::UnknownSessionDatabase(db));
396            }
397        }
398
399        // Users stub their toe on their default cluster not existing, so we provide a notice to
400        // help guide them on what do to.
401        let cluster_var = session
402            .vars()
403            .inspect(CLUSTER.name())
404            .expect("cluster should exist");
405        if session.vars().current_object_missing_warnings()
406            && catalog.resolve_cluster(Some(&cluster_active)).is_err()
407        {
408            let cluster_notice = 'notice: {
409                if cluster_var.inspect_session_value().is_some() {
410                    break 'notice Some(AdapterNotice::DefaultClusterDoesNotExist {
411                        name: cluster_active,
412                        kind: "session",
413                        suggested_action: "Pick an extant cluster with SET CLUSTER = name. Run SHOW CLUSTERS to see available clusters.".into(),
414                    });
415                }
416
417                let role_default = catalog.get_role(catalog.active_role_id());
418                let role_cluster = match role_default.vars().get(CLUSTER.name()) {
419                    Some(OwnedVarInput::Flat(name)) => Some(name),
420                    None => None,
421                    // This is unexpected!
422                    Some(v @ OwnedVarInput::SqlSet(_)) => {
423                        tracing::warn!(?v, "SqlSet found for cluster Role Default");
424                        break 'notice None;
425                    }
426                };
427
428                let alter_role = "with `ALTER ROLE <role> SET cluster TO <cluster>;`";
429                match role_cluster {
430                    // If there is no default, suggest a Role default.
431                    None => Some(AdapterNotice::DefaultClusterDoesNotExist {
432                        name: cluster_active,
433                        kind: "system",
434                        suggested_action: format!(
435                            "Set a default cluster for the current role {alter_role}."
436                        ),
437                    }),
438                    // If the default does not exist, suggest to change it.
439                    Some(_) => Some(AdapterNotice::DefaultClusterDoesNotExist {
440                        name: cluster_active,
441                        kind: "role",
442                        suggested_action: format!(
443                            "Change the default cluster for the current role {alter_role}."
444                        ),
445                    }),
446                }
447            };
448
449            if let Some(notice) = cluster_notice {
450                session.add_notice(notice);
451            }
452        }
453
454        client.enable_frontend_peek_sequencing = ENABLE_FRONTEND_PEEK_SEQUENCING
455            .require(catalog.system_vars())
456            .is_ok();
457
458        Ok(client)
459    }
460
461    /// Cancels the query currently running on the specified connection.
462    pub fn cancel_request(&self, conn_id: ConnectionIdType, secret_key: u32) {
463        self.send(Command::CancelRequest {
464            conn_id,
465            secret_key,
466        });
467    }
468
469    /// Executes a single SQL statement that returns rows as the
470    /// `mz_support` user.
471    pub async fn support_execute_one(
472        &self,
473        sql: &str,
474    ) -> Result<Pin<Box<dyn Stream<Item = PeekResponseUnary> + Send>>, anyhow::Error> {
475        // Connect to the coordinator.
476        let conn_id = self.new_conn_id()?;
477        let session = self.new_session(
478            SessionConfig {
479                conn_id,
480                uuid: Uuid::new_v4(),
481                user: SUPPORT_USER.name.clone(),
482                client_ip: None,
483                external_metadata_rx: None,
484                helm_chart_version: None,
485                authenticator_kind: AuthenticatorKind::None,
486                groups: None,
487            },
488            Authenticated,
489        );
490        let mut session_client = self.startup(session).await?;
491
492        // Parse the SQL statement.
493        let stmts = mz_sql::parse::parse(sql)?;
494        if stmts.len() != 1 {
495            bail!("must supply exactly one query");
496        }
497        let StatementParseResult { ast: stmt, sql } = stmts.into_element();
498
499        const EMPTY_PORTAL: &str = "";
500        session_client.start_transaction(Some(1))?;
501        session_client
502            .declare(EMPTY_PORTAL.into(), stmt, sql.to_string())
503            .await?;
504
505        let execute_result = session_client
506            .execute(EMPTY_PORTAL.into(), futures::future::pending(), None)
507            .await?;
508        match execute_result {
509            (ExecuteResponse::SendingRowsStreaming { mut rows, .. }, _) => {
510                // We have to only drop the session client _after_ we read the
511                // result. Otherwise the peek will get cancelled right when we
512                // drop the session client. So we wrap it up in an extra stream
513                // like this, which owns the client and can return it.
514                let owning_response_stream = async_stream::stream! {
515                    while let Some(rows) = rows.next().await {
516                        yield rows;
517                    }
518                    drop(session_client);
519                };
520                Ok(Box::pin(owning_response_stream))
521            }
522            r => bail!("unsupported response type: {r:?}"),
523        }
524    }
525
526    /// Returns the metrics associated with the adapter layer.
527    pub fn metrics(&self) -> &Metrics {
528        &self.metrics
529    }
530
531    /// The current time according to the [`Client`].
532    pub fn now(&self) -> DateTime<Utc> {
533        to_datetime((self.now)())
534    }
535
536    /// Get a metadata and a channel that can be used to append to a webhook source.
537    pub async fn get_webhook_appender(
538        &self,
539        database: String,
540        schema: String,
541        name: String,
542    ) -> Result<AppendWebhookResponse, AppendWebhookError> {
543        let (tx, rx) = oneshot::channel();
544
545        // Send our request.
546        self.send(Command::GetWebhook {
547            database,
548            schema,
549            name,
550            tx,
551        });
552
553        // Using our one shot channel to get the result, returning an error if the sender dropped.
554        let response = rx
555            .await
556            .map_err(|_| anyhow::anyhow!("failed to receive webhook response"))?;
557
558        response
559    }
560
561    /// Gets the current value of all system variables.
562    pub async fn get_system_vars(&self) -> SystemVars {
563        let (tx, rx) = oneshot::channel();
564        self.send(Command::GetSystemVars { tx });
565        rx.await.expect("coordinator unexpectedly gone")
566    }
567
568    /// Returns a snapshot of the catalog.
569    ///
570    /// Does a Coordinator round-trip. Session-bound callers should
571    /// prefer [`SessionClient::catalog_snapshot`], which serves from the
572    /// session's snapshot cache.
573    pub async fn catalog_snapshot_expensive(&self) -> Arc<Catalog> {
574        let (tx, rx) = oneshot::channel();
575        self.send(Command::CatalogSnapshot { tx });
576        let CatalogSnapshot { catalog } = rx.await.expect("coordinator unexpectedly gone");
577        catalog
578    }
579
580    /// Reconciles the coordinator's scoped feature-flag working copy towards
581    /// `overrides`. Used by the system-parameter sync loop from continuous
582    /// LaunchDarkly evaluation.
583    ///
584    /// `prune_scope` bounds which objects' rows the reconcile may remove (the
585    /// objects `overrides` was evaluated for). The sync loop passes the live
586    /// objects from its snapshot; `None` is a full replace, used by the
587    /// disabled-feature clear path. See
588    /// [`crate::catalog::Op::UpdateScopedSystemParameters`].
589    pub async fn update_scoped_system_parameters(
590        &self,
591        overrides: ScopedParameters,
592        prune_scope: Option<ScopedParametersScope>,
593    ) {
594        let (tx, rx) = oneshot::channel();
595        self.send(Command::UpdateScopedSystemParameters {
596            overrides,
597            prune_scope,
598            tx,
599        });
600        let _ = rx.await;
601    }
602
603    /// Installs (or replaces) the shared system-parameter frontend on the
604    /// coordinator, letting the create-cluster / create-replica paths resolve a
605    /// new object's scoped overrides synchronously. Sent by the sync loop each
606    /// time it (re)initializes the frontend. Fire-and-forget.
607    pub fn install_scoped_system_parameter_frontend(&self, frontend: Arc<SystemParameterFrontend>) {
608        self.send(Command::InstallScopedSystemParameterFrontend { frontend });
609    }
610
611    #[instrument(level = "debug")]
612    pub(crate) fn send(&self, cmd: Command) {
613        self.inner_cmd_tx
614            .send((OpenTelemetryContext::obtain(), cmd))
615            .expect("coordinator unexpectedly gone");
616    }
617}
618
619/// A coordinator client that is bound to a connection.
620///
621/// See also [`Client`].
622pub struct SessionClient {
623    // Invariant: inner may only be `None` after the session has been terminated.
624    // Once the session is terminated, no communication to the Coordinator
625    // should be attempted.
626    inner: Option<Client>,
627    // Invariant: session may only be `None` during a method call. Every public
628    // method must ensure that `Session` is `Some` before it returns.
629    session: Option<Session>,
630    timeouts: Timeout,
631    segment_client: Option<mz_segment::Client>,
632    environment_id: EnvironmentId,
633    /// Client for frontend peek sequencing; populated at connection startup.
634    peek_client: PeekClient,
635    /// Whether frontend peek sequencing is enabled; initialized at connection startup.
636    // TODO(peek-seq): Currently, this is initialized only at session startup. We'll be able to
637    // check the actual feature flag value at every peek (without a Coordinator call) once we'll
638    // always have a catalog snapshot at hand.
639    pub enable_frontend_peek_sequencing: bool,
640}
641
642impl SessionClient {
643    /// Parses a SQL expression, reporting failures as a telemetry event if
644    /// possible.
645    pub fn parse<'a>(
646        &self,
647        sql: &'a str,
648    ) -> Result<Result<Vec<StatementParseResult<'a>>, ParserStatementError>, String> {
649        match mz_sql::parse::parse_with_limit(sql) {
650            Ok(Err(e)) => {
651                self.track_statement_parse_failure(&e);
652                Ok(Err(e))
653            }
654            r => r,
655        }
656    }
657
658    fn track_statement_parse_failure(&self, parse_error: &ParserStatementError) {
659        let session = self.session.as_ref().expect("session invariant violated");
660        let Some(user_id) = session.user().external_metadata.as_ref().map(|m| m.user_id) else {
661            return;
662        };
663        let Some(segment_client) = &self.segment_client else {
664            return;
665        };
666        let Some(statement_kind) = parse_error.statement else {
667            return;
668        };
669        let Some((action, object_type)) = telemetry::analyze_audited_statement(statement_kind)
670        else {
671            return;
672        };
673        let event_type = StatementFailureType::ParseFailure;
674        let event_name = format!(
675            "{} {} {}",
676            object_type.as_title_case(),
677            action.as_title_case(),
678            event_type.as_title_case(),
679        );
680        segment_client.environment_track(
681            &self.environment_id,
682            event_name,
683            json!({
684                "statement_kind": statement_kind,
685                "error": &parse_error.error,
686            }),
687            EventDetails {
688                user_id: Some(user_id),
689                application_name: Some(session.application_name()),
690                ..Default::default()
691            },
692        );
693    }
694
695    // Verify and return the named prepared statement. We need to verify each use
696    // to make sure the prepared statement is still safe to use.
697    pub async fn get_prepared_statement(
698        &mut self,
699        name: &str,
700    ) -> Result<&PreparedStatement, AdapterError> {
701        let catalog = self.catalog_snapshot("get_prepared_statement").await;
702        Coordinator::verify_prepared_statement(&catalog, self.session(), name)?;
703        Ok(self
704            .session()
705            .get_prepared_statement_unverified(name)
706            .expect("must exist"))
707    }
708
709    /// Saves the parsed statement as a prepared statement.
710    ///
711    /// The prepared statement is saved in the connection's [`crate::session::Session`]
712    /// under the specified name.
713    pub async fn prepare(
714        &mut self,
715        name: String,
716        stmt: Option<Statement<Raw>>,
717        sql: String,
718        param_types: Vec<Option<SqlScalarType>>,
719    ) -> Result<(), AdapterError> {
720        let catalog = self.catalog_snapshot("prepare").await;
721
722        // Note: This failpoint is used to simulate a request outliving the external connection
723        // that made it.
724        let mut async_pause = false;
725        (|| {
726            fail::fail_point!("async_prepare", |val| {
727                async_pause = val.map_or(false, |val| val.parse().unwrap_or(false))
728            });
729        })();
730        if async_pause {
731            tokio::time::sleep(Duration::from_secs(1)).await;
732        };
733
734        let desc = Coordinator::describe(&catalog, self.session(), stmt.clone(), param_types)?;
735        let now = self.now();
736        let state_revision = StateRevision {
737            catalog_revision: catalog.transient_revision(),
738            session_state_revision: self.session().state_revision(),
739        };
740        self.session()
741            .set_prepared_statement(name, stmt, sql, desc, state_revision, now);
742        Ok(())
743    }
744
745    /// Binds a statement to a portal.
746    #[mz_ore::instrument(level = "debug")]
747    pub async fn declare(
748        &mut self,
749        name: String,
750        stmt: Statement<Raw>,
751        sql: String,
752    ) -> Result<(), AdapterError> {
753        let catalog = self.catalog_snapshot("declare").await;
754        let param_types = vec![];
755        let desc =
756            Coordinator::describe(&catalog, self.session(), Some(stmt.clone()), param_types)?;
757        let params = vec![];
758        let result_formats = vec![mz_pgwire_common::Format::Text; desc.arity()];
759        let now = self.now();
760        let logging = self.session().mint_logging(sql, Some(&stmt), now);
761        let state_revision = StateRevision {
762            catalog_revision: catalog.transient_revision(),
763            session_state_revision: self.session().state_revision(),
764        };
765        self.session().set_portal(
766            name,
767            desc,
768            Some(stmt),
769            logging,
770            params,
771            result_formats,
772            state_revision,
773        )?;
774        Ok(())
775    }
776
777    /// Executes a previously-bound portal.
778    ///
779    /// Note: the provided `cancel_future` must be cancel-safe as it's polled in a `select!` loop.
780    ///
781    /// `outer_ctx_extra` is Some when we are executing as part of an outer statement, e.g., a FETCH
782    /// triggering the execution of the underlying query.
783    #[mz_ore::instrument(level = "debug")]
784    pub async fn execute(
785        &mut self,
786        portal_name: String,
787        cancel_future: impl Future<Output = std::io::Error> + Send,
788        outer_ctx_extra: Option<ExecuteContextGuard>,
789    ) -> Result<(ExecuteResponse, Instant), AdapterError> {
790        let execute_started = Instant::now();
791
792        let mut outer_ctx_extra = outer_ctx_extra;
793
794        // Unroll SQL `EXECUTE <prepared> (...)` so the inner statement
795        // flows through `try_frontend_peek` below, rather than being
796        // re-dispatched via `Command::Execute` from the coordinator's
797        // `Plan::Execute` handler. Without this, a prepared statement
798        // would route differently from the same statement issued
799        // directly.
800        //
801        // On a successful unroll, `unroll_sql_execute` also begins
802        // EXECUTE-level statement logging on the outer portal, so that
803        // `mz_statement_execution_history` records `EXECUTE foo (...)`
804        // rather than the inner SQL, and installs the resulting
805        // `ExecuteContextGuard` into `outer_ctx_extra`.
806        let portal_name = self
807            .unroll_sql_execute(portal_name, &mut outer_ctx_extra)
808            .await?;
809
810        // Attempt peek sequencing in the session task.
811        // If unsupported, fall back to the Coordinator path.
812        // TODO(peek-seq): wire up cancel_future
813        let peek_result = self
814            .try_frontend_peek(&portal_name, &mut outer_ctx_extra)
815            .await?;
816        if let Some(resp) = peek_result {
817            debug!("frontend peek succeeded");
818            // Frontend peek handled the execution and retired outer_ctx_extra if it existed.
819            // No additional work needed here.
820            return Ok((resp, execute_started));
821        } else {
822            debug!("frontend peek did not happen, falling back to `Command::Execute`");
823            // If we bailed out, outer_ctx_extra is still present (if it was originally).
824            // `Command::Execute` will handle it.
825            // (This is not true if we bailed out _after_ the frontend peek sequencing has already
826            // begun its own statement logging. That case would be a bug.)
827        }
828
829        let response = self
830            .send_with_cancel(
831                |tx, session| Command::Execute {
832                    portal_name,
833                    session,
834                    tx,
835                    outer_ctx_extra,
836                },
837                cancel_future,
838            )
839            .await?;
840        Ok((response, execute_started))
841    }
842
843    /// If the named portal binds a SQL `EXECUTE <prepared>`, resolve the
844    /// prepared statement, install a fresh portal for the inner statement
845    /// (carrying the EXECUTE's actual parameter values), and return that
846    /// portal's name so the caller can run `try_frontend_peek` against it.
847    ///
848    /// Only ever unrolls one level: the parser rejects
849    /// `PREPARE foo AS EXECUTE bar` (matching Postgres), so the inner
850    /// statement is guaranteed not to be another `EXECUTE`. A failsafe below
851    /// surfaces an internal error if that invariant is ever violated.
852    ///
853    /// When the portal does not bind an `EXECUTE` — the common case —
854    /// returns the original portal name, costing only a portal lookup.
855    async fn unroll_sql_execute(
856        &mut self,
857        portal_name: String,
858        outer_ctx_extra: &mut Option<ExecuteContextGuard>,
859    ) -> Result<String, AdapterError> {
860        let (stmt, params, outer_logging, outer_lifecycle_timestamps) = {
861            let session = self.session.as_ref().expect("SessionClient invariant");
862            let portal = match session.get_portal_unverified(&portal_name) {
863                Some(p) => p,
864                // No portal: let `try_frontend_peek` surface the
865                // standard "missing portal" error.
866                None => return Ok(portal_name),
867            };
868            match &portal.stmt {
869                Some(stmt) => (
870                    Arc::clone(stmt),
871                    portal.parameters.clone(),
872                    Arc::clone(&portal.logging),
873                    portal.lifecycle_timestamps.clone(),
874                ),
875                None => return Ok(portal_name),
876            }
877        };
878
879        // Only EXECUTE statements need unrolling. Bail out before taking a
880        // catalog snapshot in the (overwhelmingly common) non-EXECUTE case.
881        if !matches!(&*stmt, Statement::Execute(_)) {
882            return Ok(portal_name);
883        }
884
885        let catalog = self.catalog_snapshot("unroll_sql_execute").await;
886
887        // Validate the outer EXECUTE portal against the (possibly newer)
888        // catalog: ensures the recorded portal description still matches
889        // what describing the EXECUTE would produce now.
890        {
891            let session = self.session.as_mut().expect("SessionClient invariant");
892            Coordinator::verify_portal(&catalog, session, &portal_name)?;
893        }
894
895        // Bump query_total for the outer EXECUTE itself. The inner
896        // statement gets its own increment inside `try_frontend_peek_inner`
897        // (or, on bailout, in the coordinator's `handle_execute`).
898        {
899            let session = self.session.as_ref().expect("SessionClient invariant");
900            session
901                .metrics()
902                .query_total(&[
903                    metrics::session_type_label_value(session.user()),
904                    metrics::statement_type_label_value(&stmt),
905                ])
906                .inc();
907        }
908
909        // Begin EXECUTE-level statement logging up front, so that planning
910        // errors below produce an `Errored` end-event in
911        // `mz_statement_execution_history` rather than no entry at all.
912        //
913        // We pass the *outer* portal's `logging` and pgwire-bound `params`
914        // so the recorded entry shows the user-visible `EXECUTE foo (...)`,
915        // not the inner SQL. The id (if any) moves into `outer_ctx_extra`
916        // below for `try_frontend_peek` to retire; on planning error we
917        // explicitly emit an `Errored` end-event below.
918        let began_outer_logging = outer_ctx_extra.is_none();
919        let logging_id: Option<crate::statement_logging::StatementLoggingId> =
920            if began_outer_logging {
921                let session = self.session.as_mut().expect("SessionClient invariant");
922                let result = self
923                    .peek_client
924                    .statement_logging_frontend
925                    .begin_statement_execution(
926                        session,
927                        &params,
928                        &outer_logging,
929                        catalog.system_config(),
930                        outer_lifecycle_timestamps,
931                    );
932                if let Some((id, began_execution, mseh_update, prepared_statement)) = result {
933                    self.peek_client.log_began_execution(
934                        began_execution,
935                        mseh_update,
936                        prepared_statement,
937                    );
938                    Some(id)
939                } else {
940                    None
941                }
942            } else {
943                None
944            };
945
946        let new_portal_name = match self.install_inner_portal_for_execute(&catalog, &stmt, &params)
947        {
948            Ok(name) => name,
949            Err(err) => {
950                if let Some(id) = logging_id {
951                    self.peek_client.log_ended_execution(
952                        id,
953                        StatementEndedExecutionReason::Errored {
954                            error: err.to_string(),
955                        },
956                    );
957                }
958                return Err(err);
959            }
960        };
961
962        // Hand off to `outer_ctx_extra` whenever we entered the begin path
963        // for the outer EXECUTE — even if `begin_statement_execution`
964        // returned `None` (sampling decided not to sample, or logging is
965        // disabled for the user). This mirrors the original coord path,
966        // which always installs a guard via
967        // `ExecuteContextGuard::new(maybe_uuid, ...)`. Without this, the
968        // inner portal would be treated as a fresh statement by
969        // `try_frontend_peek` (or the fallback `Command::Execute` path)
970        // and re-account its bytes against
971        // `mz_statement_logging_unsampled_bytes`, double-counting the
972        // inner SQL.
973        if began_outer_logging {
974            // Soft invariant: `try_frontend_peek` takes ownership of
975            // `outer_ctx_extra` immediately, so this guard's `Drop` is
976            // unreachable on the normal flow and the dummy channel is
977            // never used. If a panic does fire `Drop` between here and
978            // that takeover, the `Aborted` end-event is silently lost
979            // — an acceptable trade given the panic implies the
980            // connection is going down anyway.
981            let (dummy_tx, _dummy_rx) = mpsc::unbounded_channel();
982            *outer_ctx_extra = Some(ExecuteContextGuard::new(logging_id, dummy_tx));
983        }
984
985        Ok(new_portal_name)
986    }
987
988    /// Helper for [`Self::unroll_sql_execute`]: plans the outer
989    /// `Statement::Execute`, verifies the referenced prepared statement, and
990    /// installs a fresh portal carrying the inner statement plus the
991    /// EXECUTE's bound parameter values. Returns the new portal's name.
992    ///
993    /// Split out so [`Self::unroll_sql_execute`] can wrap the fallible work
994    /// in a single error-handling site that emits an `Errored` end-event
995    /// for the EXECUTE-level statement-logging entry.
996    fn install_inner_portal_for_execute(
997        &mut self,
998        catalog: &Arc<Catalog>,
999        stmt: &Arc<Statement<Raw>>,
1000        params: &mz_sql::plan::Params,
1001    ) -> Result<String, AdapterError> {
1002        use mz_sql::plan::Plan;
1003
1004        let execute_plan = {
1005            let session = self.session.as_mut().expect("SessionClient invariant");
1006            let conn_catalog = catalog.for_session(session);
1007            let (resolved_stmt, resolved_ids) =
1008                mz_sql::names::resolve(&conn_catalog, (**stmt).clone())?;
1009            let pcx = session.pcx();
1010            let (plan, _sql_impl_ids) = mz_sql::plan::plan(
1011                Some(pcx),
1012                &conn_catalog,
1013                resolved_stmt,
1014                params,
1015                &resolved_ids,
1016            )?;
1017            match plan {
1018                Plan::Execute(plan) => plan,
1019                other => {
1020                    // Planning a `Statement::Execute` must yield
1021                    // `Plan::Execute`. If it doesn't, the planner
1022                    // contract is broken.
1023                    return Err(AdapterError::Internal(format!(
1024                        "planning Statement::Execute yielded unexpected plan: {:?}",
1025                        mz_sql::plan::PlanKind::from(&other),
1026                    )));
1027                }
1028            }
1029        };
1030
1031        // Verify and install the inner portal. Mirrors
1032        // `Coordinator::sequence_execute`. The new portal carries the inner
1033        // prepared statement's `logging`, but `try_frontend_peek` will see
1034        // `outer_ctx_extra=Some(...)` and inherit the EXECUTE-level logging
1035        // instead of starting fresh from this portal.
1036        let session = self.session.as_mut().expect("SessionClient invariant");
1037        Coordinator::verify_prepared_statement(catalog, session, &execute_plan.name)?;
1038        let ps = session
1039            .get_prepared_statement_unverified(&execute_plan.name)
1040            .expect("verified above");
1041        let inner_stmt = ps.stmt().cloned();
1042        let inner_desc = ps.desc().clone();
1043        let state_revision = ps.state_revision;
1044        let inner_logging = Arc::clone(ps.logging());
1045
1046        // Failsafe: `PREPARE foo AS EXECUTE bar` is rejected by the parser,
1047        // so the resolved inner statement must not be another `EXECUTE`. If
1048        // that ever changes, we'd silently skip frontend sequencing for the
1049        // deeper EXECUTEs — surface it as an internal error instead.
1050        if let Some(inner) = inner_stmt.as_ref() {
1051            if matches!(inner, Statement::Execute(_)) {
1052                return Err(AdapterError::Internal(format!(
1053                    "nested EXECUTE: prepared statement {} resolves to another EXECUTE; \
1054                     parser should reject `PREPARE ... AS EXECUTE ...`",
1055                    execute_plan.name.quoted(),
1056                )));
1057            }
1058        }
1059
1060        session.create_new_portal(
1061            inner_stmt,
1062            inner_logging,
1063            inner_desc,
1064            execute_plan.params,
1065            Vec::new(),
1066            state_revision,
1067        )
1068    }
1069
1070    fn now(&self) -> EpochMillis {
1071        (self.inner().now)()
1072    }
1073
1074    fn now_datetime(&self) -> DateTime<Utc> {
1075        to_datetime(self.now())
1076    }
1077
1078    /// Starts a transaction based on implicit:
1079    /// - `None`: InTransaction
1080    /// - `Some(1)`: Started
1081    /// - `Some(n > 1)`: InTransactionImplicit
1082    /// - `Some(0)`: no change
1083    pub fn start_transaction(&mut self, implicit: Option<usize>) -> Result<(), AdapterError> {
1084        let now = self.now_datetime();
1085        let session = self.session.as_mut().expect("session invariant violated");
1086        let result = match implicit {
1087            None => session.start_transaction(now, None, None),
1088            Some(stmts) => {
1089                session.start_transaction_implicit(now, stmts);
1090                Ok(())
1091            }
1092        };
1093        result
1094    }
1095
1096    /// Ends a transaction. Even if an error is returned, guarantees that the transaction in the
1097    /// session and Coordinator has cleared its state.
1098    #[instrument(level = "debug")]
1099    pub async fn end_transaction(
1100        &mut self,
1101        action: EndTransactionAction,
1102    ) -> Result<ExecuteResponse, AdapterError> {
1103        let res = self
1104            .send(|tx, session| Command::Commit {
1105                action,
1106                session,
1107                tx,
1108            })
1109            .await;
1110        // Commit isn't guaranteed to set the session's state to anything specific, so clear it
1111        // here. It's safe to ignore the returned `TransactionStatus` because that doesn't contain
1112        // any data that the Coordinator must act on for correctness.
1113        let _ = self.session().clear_transaction();
1114        res
1115    }
1116
1117    /// Fails a transaction.
1118    pub fn fail_transaction(&mut self) {
1119        let session = self.session.take().expect("session invariant violated");
1120        let session = session.fail_transaction();
1121        self.session = Some(session);
1122    }
1123
1124    /// Fetches the catalog, served from the session-side snapshot cache when
1125    /// the catalog is unchanged since the cached snapshot was taken. See
1126    /// [`PeekClient::catalog_snapshot`].
1127    #[instrument(level = "debug")]
1128    pub async fn catalog_snapshot(&mut self, context: &str) -> Arc<Catalog> {
1129        self.peek_client.catalog_snapshot(context).await
1130    }
1131
1132    /// Reports whether `enable_statement_arrival_logging` is on.
1133    pub async fn statement_arrival_logging_enabled(&mut self) -> bool {
1134        let catalog = self.catalog_snapshot("statement_arrival_logging").await;
1135        catalog.system_config().enable_statement_arrival_logging()
1136    }
1137
1138    /// Dumps the catalog to a JSON string.
1139    ///
1140    /// No authorization is performed, so access to this function must be limited to internal
1141    /// servers or superusers.
1142    pub async fn dump_catalog(&mut self) -> Result<CatalogDump, AdapterError> {
1143        let catalog = self.catalog_snapshot("dump_catalog").await;
1144        catalog.dump().map_err(AdapterError::from)
1145    }
1146
1147    /// Checks the catalog for internal consistency, returning a JSON object describing the
1148    /// inconsistencies, if there are any.
1149    ///
1150    /// No authorization is performed, so access to this function must be limited to internal
1151    /// servers or superusers.
1152    pub async fn check_catalog(&mut self) -> Result<(), serde_json::Value> {
1153        let catalog = self.catalog_snapshot("check_catalog").await;
1154        catalog.check_consistency()
1155    }
1156
1157    /// Checks the coordinator for internal consistency, returning a JSON object describing the
1158    /// inconsistencies, if there are any. This is a superset of checks that check_catalog performs,
1159    ///
1160    /// No authorization is performed, so access to this function must be limited to internal
1161    /// servers or superusers.
1162    pub async fn check_coordinator(&self) -> Result<(), serde_json::Value> {
1163        self.send_without_session(|tx| Command::CheckConsistency { tx })
1164            .await
1165            .map_err(|inconsistencies| {
1166                serde_json::to_value(inconsistencies).unwrap_or_else(|_| {
1167                    serde_json::Value::String("failed to serialize inconsistencies".to_string())
1168                })
1169            })
1170    }
1171
1172    pub async fn dump_coordinator_state(&self) -> Result<serde_json::Value, anyhow::Error> {
1173        self.send_without_session(|tx| Command::Dump { tx }).await
1174    }
1175
1176    /// Tells the coordinator a statement has finished execution, in the cases
1177    /// where we have no other reason to communicate with the coordinator.
1178    pub fn retire_execute(
1179        &self,
1180        guard: ExecuteContextGuard,
1181        reason: StatementEndedExecutionReason,
1182    ) {
1183        if !guard.is_trivial() {
1184            let data = guard.defuse();
1185            let cmd = Command::RetireExecute { data, reason };
1186            self.inner().send(cmd);
1187        }
1188    }
1189
1190    /// Sets up a streaming COPY FROM STDIN operation.
1191    ///
1192    /// Sends a command to the coordinator to create a background batch
1193    /// builder task. Returns a [`CopyFromStdinWriter`] that pgwire uses
1194    /// to stream decoded rows.
1195    pub async fn start_copy_from_stdin(
1196        &mut self,
1197        target_id: CatalogItemId,
1198        target_name: String,
1199        columns: Vec<ColumnIndex>,
1200        row_desc: mz_repr::RelationDesc,
1201        params: mz_pgcopy::CopyFormatParams<'static>,
1202    ) -> Result<CopyFromStdinWriter, AdapterError> {
1203        self.send(|tx, session| Command::StartCopyFromStdin {
1204            target_id,
1205            target_name,
1206            columns,
1207            row_desc,
1208            params,
1209            session,
1210            tx,
1211        })
1212        .await
1213    }
1214
1215    /// Commits staged COPY FROM STDIN batches to a table.
1216    ///
1217    /// Adds the pre-built persist batches to the session's transaction
1218    /// operations. The actual commit happens when the transaction ends.
1219    pub fn stage_copy_from_stdin_batches(
1220        &mut self,
1221        target_id: CatalogItemId,
1222        batches: Vec<mz_persist_client::batch::ProtoBatch>,
1223    ) -> Result<(), AdapterError> {
1224        use crate::session::{TransactionOps, WriteOp};
1225        use mz_storage_client::client::TableData;
1226
1227        self.session()
1228            .add_transaction_ops(TransactionOps::Writes(vec![WriteOp {
1229                id: target_id,
1230                rows: TableData::Batches(batches.into()),
1231            }]))?;
1232        Ok(())
1233    }
1234
1235    /// Gets the current value of all system variables.
1236    pub async fn get_system_vars(&self) -> SystemVars {
1237        self.inner().get_system_vars().await
1238    }
1239
1240    /// Updates the specified system variables to the specified values.
1241    pub async fn set_system_vars(
1242        &mut self,
1243        vars: BTreeMap<String, String>,
1244    ) -> Result<(), AdapterError> {
1245        let conn_id = self.session().conn_id().clone();
1246        self.send_without_session(|tx| Command::SetSystemVars { vars, conn_id, tx })
1247            .await
1248    }
1249
1250    /// Injects audit events into the catalog via the coordinator.
1251    ///
1252    /// No authorization is performed, so access to this function must be limited to internal
1253    /// servers or superusers.
1254    pub async fn inject_audit_events(
1255        &mut self,
1256        events: Vec<crate::catalog::InjectedAuditEvent>,
1257    ) -> Result<(), AdapterError> {
1258        let conn_id = self.session().conn_id().clone();
1259        self.send_without_session(|tx| Command::InjectAuditEvents {
1260            events,
1261            conn_id,
1262            tx,
1263        })
1264        .await
1265    }
1266
1267    /// Terminates the client session.
1268    pub async fn terminate(&mut self) {
1269        let conn_id = self.session().conn_id().clone();
1270        let res = self
1271            .send_without_session(|tx| Command::Terminate {
1272                conn_id,
1273                tx: Some(tx),
1274            })
1275            .await;
1276        if let Err(e) = res {
1277            // Nothing we can do to handle a failed terminate so we just log and ignore it.
1278            error!("Unable to terminate session: {e:?}");
1279        }
1280        // Prevent any communication with Coordinator after session is terminated.
1281        self.inner = None;
1282    }
1283
1284    /// Returns a mutable reference to the session bound to this client.
1285    pub fn session(&mut self) -> &mut Session {
1286        self.session.as_mut().expect("session invariant violated")
1287    }
1288
1289    /// Returns a reference to the inner client.
1290    pub fn inner(&self) -> &Client {
1291        self.inner.as_ref().expect("inner invariant violated")
1292    }
1293
1294    async fn send_without_session<T, F>(&self, f: F) -> T
1295    where
1296        F: FnOnce(oneshot::Sender<T>) -> Command,
1297    {
1298        let (tx, rx) = oneshot::channel();
1299        self.inner().send(f(tx));
1300        rx.await.expect("sender dropped")
1301    }
1302
1303    #[instrument(level = "debug")]
1304    async fn send<T, F>(&mut self, f: F) -> Result<T, AdapterError>
1305    where
1306        F: FnOnce(oneshot::Sender<Response<T>>, Session) -> Command,
1307    {
1308        self.send_with_cancel(f, futures::future::pending()).await
1309    }
1310
1311    /// Send a [`Command`] to the Coordinator, with the ability to cancel the command.
1312    ///
1313    /// Note: the provided `cancel_future` must be cancel-safe as it's polled in a `select!` loop.
1314    #[instrument(level = "debug")]
1315    async fn send_with_cancel<T, F>(
1316        &mut self,
1317        f: F,
1318        cancel_future: impl Future<Output = std::io::Error> + Send,
1319    ) -> Result<T, AdapterError>
1320    where
1321        F: FnOnce(oneshot::Sender<Response<T>>, Session) -> Command,
1322    {
1323        let session = self.session.take().expect("session invariant violated");
1324        let mut typ = None;
1325        let application_name = session.application_name();
1326        let name_hint = ApplicationNameHint::from_str(application_name);
1327        let conn_id = session.conn_id().clone();
1328        let (tx, rx) = oneshot::channel();
1329
1330        // Destructure self so we can hold a mutable reference to the inner client and session at
1331        // the same time.
1332        let Self {
1333            inner: inner_client,
1334            session: client_session,
1335            ..
1336        } = self;
1337
1338        // TODO(parkmycar): Leaking this invariant here doesn't feel great, but calling
1339        // `self.client()` doesn't work because then Rust takes a borrow on the entirity of self.
1340        let inner_client = inner_client.as_ref().expect("inner invariant violated");
1341
1342        // ~~SPOOKY ZONE~~
1343        //
1344        // This guard prevents a race where a `Session` is returned on `rx` but never placed
1345        // back in `self` because the Future returned by this function is concurrently dropped
1346        // with the Coordinator sending a response.
1347        let mut guarded_rx = rx.with_guard(|response: Response<_>| {
1348            *client_session = Some(response.session);
1349        });
1350
1351        inner_client.send({
1352            let cmd = f(tx, session);
1353            // Measure the success and error rate of certain commands:
1354            // - declare reports success of SQL statement planning
1355            // - execute reports success of dataflow execution
1356            match cmd {
1357                Command::Execute { .. } => typ = Some("execute"),
1358                Command::GetWebhook { .. } => typ = Some("webhook"),
1359                Command::StartCopyFromStdin { .. }
1360                | Command::Startup { .. }
1361                | Command::AuthenticatePassword { .. }
1362                | Command::AuthenticateGetSASLChallenge { .. }
1363                | Command::AuthenticateVerifySASLProof { .. }
1364                | Command::CheckRoleCanLogin { .. }
1365                | Command::CatalogSnapshot { .. }
1366                | Command::Commit { .. }
1367                | Command::CancelRequest { .. }
1368                | Command::PrivilegedCancelRequest { .. }
1369                | Command::GetSystemVars { .. }
1370                | Command::SetSystemVars { .. }
1371                | Command::UpdateScopedSystemParameters { .. }
1372                | Command::InstallScopedSystemParameterFrontend { .. }
1373                | Command::Terminate { .. }
1374                | Command::RetireExecute { .. }
1375                | Command::CheckConsistency { .. }
1376                | Command::Dump { .. }
1377                | Command::GetComputeInstanceClient { .. }
1378                | Command::GetOracle { .. }
1379                | Command::DetermineRealTimeRecentTimestamp { .. }
1380                | Command::GetTransactionReadHoldsBundle { .. }
1381                | Command::StoreTransactionReadHolds { .. }
1382                | Command::ExecuteSlowPathPeek { .. }
1383                | Command::ExecuteSubscribe { .. }
1384                | Command::CopyToPreflight { .. }
1385                | Command::ExecuteCopyTo { .. }
1386                | Command::ExecuteSideEffectingFunc { .. }
1387                | Command::LookupConnection { .. }
1388                | Command::RegisterFrontendPeek { .. }
1389                | Command::UnregisterFrontendPeek { .. }
1390                | Command::ExplainTimestamp { .. }
1391                | Command::FrontendStatementLogging(..)
1392                | Command::InjectAuditEvents { .. } => {}
1393            };
1394            cmd
1395        });
1396
1397        let mut cancel_future = pin::pin!(cancel_future);
1398        let mut cancelled = false;
1399        loop {
1400            tokio::select! {
1401                res = &mut guarded_rx => {
1402                    // We received a result, so drop our guard to drop our borrows.
1403                    drop(guarded_rx);
1404
1405                    let res = res.expect("sender dropped");
1406                    let status = res.result.is_ok().then_some("success").unwrap_or("error");
1407                    if let Err(err) = res.result.as_ref() {
1408                        if name_hint.should_trace_errors() {
1409                            tracing::warn!(?err, ?name_hint, "adapter response error");
1410                        }
1411                    }
1412
1413                    if let Some(typ) = typ {
1414                        inner_client
1415                            .metrics
1416                            .commands
1417                            .with_label_values(&[typ, status, name_hint.as_str()])
1418                            .inc();
1419                    }
1420                    *client_session = Some(res.session);
1421                    return res.result;
1422                },
1423                _err = &mut cancel_future, if !cancelled => {
1424                    cancelled = true;
1425                    inner_client.send(Command::PrivilegedCancelRequest {
1426                        conn_id: conn_id.clone(),
1427                    });
1428                }
1429            };
1430        }
1431    }
1432
1433    pub fn add_idle_in_transaction_session_timeout(&mut self) {
1434        let session = self.session();
1435        let timeout_dur = session.vars().idle_in_transaction_session_timeout();
1436        if !timeout_dur.is_zero() {
1437            let timeout_dur = timeout_dur.clone();
1438            if let Some(txn) = session.transaction().inner() {
1439                let txn_id = txn.id.clone();
1440                let timeout = TimeoutType::IdleInTransactionSession(txn_id);
1441                self.timeouts.add_timeout(timeout, timeout_dur);
1442            }
1443        }
1444    }
1445
1446    pub fn remove_idle_in_transaction_session_timeout(&mut self) {
1447        let session = self.session();
1448        if let Some(txn) = session.transaction().inner() {
1449            let txn_id = txn.id.clone();
1450            self.timeouts
1451                .remove_timeout(&TimeoutType::IdleInTransactionSession(txn_id));
1452        }
1453    }
1454
1455    /// # Cancel safety
1456    ///
1457    /// This method is cancel safe. If `recv` is used as the event in a
1458    /// `tokio::select!` statement and some other branch
1459    /// completes first, it is guaranteed that no messages were received on this
1460    /// channel.
1461    pub async fn recv_timeout(&mut self) -> Option<TimeoutType> {
1462        self.timeouts.recv().await
1463    }
1464
1465    /// Attempt to sequence a peek from the session task.
1466    ///
1467    /// Returns `Ok(Some(response))` if we handled the peek, or `Ok(None)` to fall back to the
1468    /// Coordinator's sequencing. If it returns an error, it should be returned to the user.
1469    ///
1470    /// `outer_ctx_extra` is Some when we are executing as part of an outer statement, e.g., a FETCH
1471    /// triggering the execution of the underlying query.
1472    pub(crate) async fn try_frontend_peek(
1473        &mut self,
1474        portal_name: &str,
1475        outer_ctx_extra: &mut Option<ExecuteContextGuard>,
1476    ) -> Result<Option<ExecuteResponse>, AdapterError> {
1477        if self.enable_frontend_peek_sequencing {
1478            let session = self.session.as_mut().expect("SessionClient invariant");
1479            self.peek_client
1480                .try_frontend_peek(portal_name, session, outer_ctx_extra)
1481                .await
1482        } else {
1483            Ok(None)
1484        }
1485    }
1486}
1487
1488impl Drop for SessionClient {
1489    fn drop(&mut self) {
1490        // We may not have a session if this client was dropped while awaiting
1491        // a response. In this case, it is the coordinator's responsibility to
1492        // terminate the session.
1493        if let Some(session) = self.session.take() {
1494            // We may not have a connection to the Coordinator if the session was
1495            // prematurely terminated, for example due to a timeout.
1496            if let Some(inner) = &self.inner {
1497                inner.send(Command::Terminate {
1498                    conn_id: session.conn_id().clone(),
1499                    tx: None,
1500                })
1501            }
1502        }
1503    }
1504}
1505
1506/// Renders SQL for statement arrival logging: parsed and displayed with its
1507/// literals redacted, which is the same redaction the statement log applies.
1508/// When the text does not parse or exceeds the statement batch size limit, a
1509/// placeholder with the byte length is returned. Raw text is never returned,
1510/// so a statement that crashes the parser is not captured, an accepted
1511/// limitation.
1512pub fn redact_sql_for_logging(sql: &str) -> String {
1513    match mz_sql_parser::parser::parse_statements_with_limit(sql) {
1514        Ok(Ok(stmts)) => stmts
1515            .into_iter()
1516            .map(|stmt| stmt.ast.to_ast_string_redacted())
1517            .join("; "),
1518        Ok(Err(_)) => format!("<unparseable ({} bytes)>", sql.len()),
1519        Err(_) => format!("<too large ({} bytes)>", sql.len()),
1520    }
1521}
1522
1523#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
1524pub enum TimeoutType {
1525    IdleInTransactionSession(TransactionId),
1526}
1527
1528impl Display for TimeoutType {
1529    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1530        match self {
1531            TimeoutType::IdleInTransactionSession(txn_id) => {
1532                writeln!(f, "Idle in transaction session for transaction '{txn_id}'")
1533            }
1534        }
1535    }
1536}
1537
1538impl From<TimeoutType> for AdapterError {
1539    fn from(timeout: TimeoutType) -> Self {
1540        match timeout {
1541            TimeoutType::IdleInTransactionSession(_) => {
1542                AdapterError::IdleInTransactionSessionTimeout
1543            }
1544        }
1545    }
1546}
1547
1548struct Timeout {
1549    tx: mpsc::UnboundedSender<TimeoutType>,
1550    rx: mpsc::UnboundedReceiver<TimeoutType>,
1551    active_timeouts: BTreeMap<TimeoutType, AbortOnDropHandle<()>>,
1552}
1553
1554impl Timeout {
1555    fn new() -> Self {
1556        let (tx, rx) = mpsc::unbounded_channel();
1557        Timeout {
1558            tx,
1559            rx,
1560            active_timeouts: BTreeMap::new(),
1561        }
1562    }
1563
1564    /// # Cancel safety
1565    ///
1566    /// This method is cancel safe. If `recv` is used as the event in a
1567    /// `tokio::select!` statement and some other branch
1568    /// completes first, it is guaranteed that no messages were received on this
1569    /// channel.
1570    ///
1571    /// <https://docs.rs/tokio/latest/tokio/sync/mpsc/struct.UnboundedReceiver.html#cancel-safety>
1572    async fn recv(&mut self) -> Option<TimeoutType> {
1573        self.rx.recv().await
1574    }
1575
1576    fn add_timeout(&mut self, timeout: TimeoutType, duration: Duration) {
1577        let tx = self.tx.clone();
1578        let timeout_key = timeout.clone();
1579        let handle = mz_ore::task::spawn(|| format!("{timeout_key}"), async move {
1580            tokio::time::sleep(duration).await;
1581            let _ = tx.send(timeout);
1582        })
1583        .abort_on_drop();
1584        self.active_timeouts.insert(timeout_key, handle);
1585    }
1586
1587    fn remove_timeout(&mut self, timeout: &TimeoutType) {
1588        self.active_timeouts.remove(timeout);
1589
1590        // Remove the timeout from the rx queue if it exists.
1591        let mut timeouts = Vec::new();
1592        while let Ok(pending_timeout) = self.rx.try_recv() {
1593            if timeout != &pending_timeout {
1594                timeouts.push(pending_timeout);
1595            }
1596        }
1597        for pending_timeout in timeouts {
1598            self.tx.send(pending_timeout).expect("rx is in this struct");
1599        }
1600    }
1601}
1602
1603/// A wrapper around a Stream of PeekResponseUnary that records when it sees the
1604/// first row data in the given histogram. It also keeps track of whether we have already observed
1605/// the end of the underlying stream.
1606#[derive(Derivative)]
1607#[derivative(Debug)]
1608pub struct RecordFirstRowStream {
1609    /// The underlying stream of rows.
1610    #[derivative(Debug = "ignore")]
1611    pub rows: Box<dyn Stream<Item = PeekResponseUnary> + Unpin + Send + Sync>,
1612    /// The Instant when execution started.
1613    pub execute_started: Instant,
1614    /// The histogram where the time since `execute_started` will be recorded when we see the first
1615    /// row.
1616    pub time_to_first_row_seconds: Histogram,
1617    /// Whether we've seen any rows.
1618    pub saw_rows: bool,
1619    /// The Instant when we saw the first row.
1620    pub recorded_first_row_instant: Option<Instant>,
1621    /// Whether we have already observed the end of the underlying stream.
1622    pub no_more_rows: bool,
1623    /// Whether the first-to-last-byte metric has already been recorded for this stream.
1624    pub metric_recorded: bool,
1625}
1626
1627impl RecordFirstRowStream {
1628    /// Create a new [`RecordFirstRowStream`]
1629    pub fn new(
1630        rows: Box<dyn Stream<Item = PeekResponseUnary> + Unpin + Send + Sync>,
1631        execute_started: Instant,
1632        client: &SessionClient,
1633        instance_id: Option<ComputeInstanceId>,
1634        strategy: Option<StatementExecutionStrategy>,
1635    ) -> Self {
1636        let histogram = Self::histogram(client, instance_id, strategy);
1637        Self {
1638            rows,
1639            execute_started,
1640            time_to_first_row_seconds: histogram,
1641            saw_rows: false,
1642            recorded_first_row_instant: None,
1643            no_more_rows: false,
1644            metric_recorded: false,
1645        }
1646    }
1647
1648    fn histogram(
1649        client: &SessionClient,
1650        instance_id: Option<ComputeInstanceId>,
1651        strategy: Option<StatementExecutionStrategy>,
1652    ) -> Histogram {
1653        let session = client.session.as_ref().expect("session invariant");
1654        let isolation_level = *session.vars().transaction_isolation();
1655        let name_hint = ApplicationNameHint::from_str(session.application_name());
1656        let instance = match instance_id {
1657            Some(i) => Cow::Owned(i.to_string()),
1658            None => Cow::Borrowed("none"),
1659        };
1660        let strategy = match strategy {
1661            Some(s) => s.name(),
1662            None => "none",
1663        };
1664
1665        client
1666            .inner()
1667            .metrics()
1668            .time_to_first_row_seconds
1669            .with_label_values(&[
1670                instance.as_ref(),
1671                isolation_level.as_variant_str(),
1672                strategy,
1673                name_hint.as_str(),
1674            ])
1675    }
1676
1677    /// If you want to match [`RecordFirstRowStream`]'s logic but don't need
1678    /// a UnboundedReceiver, you can tell it when to record an observation.
1679    pub fn record(
1680        execute_started: Instant,
1681        client: &SessionClient,
1682        instance_id: Option<ComputeInstanceId>,
1683        strategy: Option<StatementExecutionStrategy>,
1684    ) {
1685        Self::histogram(client, instance_id, strategy)
1686            .observe(execute_started.elapsed().as_secs_f64());
1687    }
1688
1689    pub async fn recv(&mut self) -> Option<PeekResponseUnary> {
1690        let msg = self.rows.next().await;
1691        if !self.saw_rows && matches!(msg, Some(PeekResponseUnary::Rows(_))) {
1692            self.saw_rows = true;
1693            self.time_to_first_row_seconds
1694                .observe(self.execute_started.elapsed().as_secs_f64());
1695            self.recorded_first_row_instant = Some(Instant::now());
1696        }
1697        if msg.is_none() {
1698            self.no_more_rows = true;
1699        }
1700        msg
1701    }
1702}