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::{FutureExt, 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_expr::UnmaterializableFunc;
29use mz_expr::{CollectionPlan, RowSetFinishing};
30use mz_ore::channel::OneshotReceiverExt;
31use mz_ore::collections::CollectionExt;
32use mz_ore::id_gen::{IdAllocator, IdAllocatorInnerBitSet, MAX_ORG_ID, org_id_conn_bits};
33use mz_ore::instrument;
34use mz_ore::now::{EpochMillis, NowFn, to_datetime};
35use mz_ore::str::StrExt;
36use mz_ore::task::AbortOnDropHandle;
37use mz_ore::thread::JoinOnDropHandle;
38use mz_ore::tracing::OpenTelemetryContext;
39use mz_repr::user::InternalUserMetadata;
40use mz_repr::{CatalogItemId, ColumnIndex, SqlScalarType};
41use mz_sql::ast::ConstantVisitor;
42use mz_sql::ast::{Raw, Statement};
43use mz_sql::catalog::{EnvironmentId, SessionCatalog};
44use mz_sql::plan::{MutationKind, Plan, ReadThenWritePlan};
45use mz_sql::session::hint::ApplicationNameHint;
46use mz_sql::session::metadata::SessionMetadata;
47use mz_sql::session::user::SUPPORT_USER;
48use mz_sql::session::vars::{
49    CLUSTER, ENABLE_FRONTEND_PEEK_SEQUENCING, OwnedVarInput, SystemVars, Var,
50};
51use mz_sql_parser::ast::display::AstDisplay;
52use mz_sql_parser::ast::{InsertStatement, StatementKind};
53use mz_sql_parser::parser::{ParserStatementError, StatementParseResult};
54use prometheus::Histogram;
55use serde_json::json;
56use tokio::sync::{mpsc, oneshot};
57use tracing::{debug, error};
58use uuid::Uuid;
59
60use crate::catalog::Catalog;
61use crate::command::{
62    CatalogDump, CatalogSnapshot, Command, CopyFromStdinWriter, ExecuteResponse, Response,
63    SASLChallengeResponse, SASLVerifyProofResponse, SuperuserAttribute,
64};
65use crate::config::{ScopedParameters, ScopedParametersScope, SystemParameterFrontend};
66use crate::coord::{Coordinator, ExecuteContextGuard};
67use crate::error::AdapterError;
68use crate::frontend_read_then_write::{
69    FrontendWriteAttemptState, FrontendWriteCancellation, contains_mz_now,
70    validate_selection_dependencies,
71};
72use crate::metrics::Metrics;
73use crate::optimize::dataflows::{EvalTime, ExprPrepOneShot};
74use crate::optimize::{self, Optimize, OptimizerError};
75use crate::peek_client::{ExecutionLogging, TakeOver};
76use crate::session::{
77    EndTransactionAction, PreparedStatement, Session, SessionConfig, StateRevision, TransactionId,
78    TransactionStatus,
79};
80use crate::statement_logging::{StatementEndedExecutionReason, StatementExecutionStrategy};
81use crate::telemetry::{self, EventDetails, SegmentClientExt, StatementFailureType};
82use crate::webhook::AppendWebhookResponse;
83use crate::{AdapterNotice, AppendWebhookError, PeekClient, PeekResponseUnary, StartupResponse};
84
85/// A handle to a running coordinator.
86///
87/// The coordinator runs on its own thread. Dropping the handle will wait for
88/// the coordinator's thread to exit, which will only occur after all
89/// outstanding [`Client`]s for the coordinator have dropped.
90pub struct Handle {
91    pub(crate) session_id: Uuid,
92    pub(crate) start_instant: Instant,
93    pub(crate) _thread: JoinOnDropHandle<()>,
94}
95
96impl Handle {
97    /// Returns the session ID associated with this coordinator.
98    ///
99    /// The session ID is generated on coordinator boot. It lasts for the
100    /// lifetime of the coordinator. Restarting the coordinator will result
101    /// in a new session ID.
102    pub fn session_id(&self) -> Uuid {
103        self.session_id
104    }
105
106    /// Returns the instant at which the coordinator booted.
107    pub fn start_instant(&self) -> Instant {
108        self.start_instant
109    }
110}
111
112/// A coordinator client.
113///
114/// A coordinator client is a simple handle to a communication channel with the
115/// coordinator. It can be cheaply cloned.
116///
117/// Clients keep the coordinator alive. The coordinator will not exit until all
118/// outstanding clients have dropped.
119#[derive(Debug, Clone)]
120pub struct Client {
121    build_info: &'static BuildInfo,
122    inner_cmd_tx: mpsc::UnboundedSender<(OpenTelemetryContext, Command)>,
123    id_alloc: IdAllocator<IdAllocatorInnerBitSet>,
124    now: NowFn,
125    metrics: Metrics,
126    environment_id: EnvironmentId,
127    segment_client: Option<mz_segment::Client>,
128}
129
130impl Client {
131    pub(crate) fn new(
132        build_info: &'static BuildInfo,
133        cmd_tx: mpsc::UnboundedSender<(OpenTelemetryContext, Command)>,
134        metrics: Metrics,
135        now: NowFn,
136        environment_id: EnvironmentId,
137        segment_client: Option<mz_segment::Client>,
138    ) -> Client {
139        // Connection ids are 32 bits and have 3 parts.
140        // 1. MSB bit is always 0 because these are interpreted as an i32, and it is possible some
141        //    driver will not handle a negative id since postgres has never produced one because it
142        //    uses process ids.
143        // 2. Next 12 bits are the lower 12 bits of the org id. This allows balancerd to route
144        //    incoming cancel messages to a subset of the environments.
145        // 3. Last 19 bits are random.
146        let env_lower = org_id_conn_bits(&environment_id.organization_id());
147        Client {
148            build_info,
149            inner_cmd_tx: cmd_tx,
150            id_alloc: IdAllocator::new(1, MAX_ORG_ID, env_lower),
151            now,
152            metrics,
153            environment_id,
154            segment_client,
155        }
156    }
157
158    /// Allocates a client for an incoming connection.
159    pub fn new_conn_id(&self) -> Result<ConnectionId, AdapterError> {
160        self.id_alloc.alloc().ok_or(AdapterError::IdExhaustionError)
161    }
162
163    /// Creates a new session associated with this client for the given user.
164    ///
165    /// It is the caller's responsibility to have authenticated the user.
166    /// We pass in an Authenticated marker as a guardrail to ensure the
167    /// user has authenticated with an authenticator before creating a session.
168    pub fn new_session(&self, config: SessionConfig, _authenticated: Authenticated) -> Session {
169        // We use the system clock to determine when a session connected to Materialize. This is not
170        // intended to be 100% accurate and correct, so we don't burden the timestamp oracle with
171        // generating a more correct timestamp.
172        Session::new(self.build_info, config, self.metrics().session_metrics())
173    }
174
175    /// Used by [mz_auth::AuthenticatorKind::Password]
176    /// to verify the provided user's password against the
177    /// stored credentials in the catalog.
178    pub async fn authenticate(
179        &self,
180        user: &String,
181        password: &Password,
182    ) -> Result<Authenticated, AdapterError> {
183        let (tx, rx) = oneshot::channel();
184        self.send(Command::AuthenticatePassword {
185            role_name: user.to_string(),
186            password: Some(password.clone()),
187            tx,
188        });
189        rx.await.expect("sender dropped")?;
190        Ok(Authenticated)
191    }
192
193    /// Used by [mz_auth::AuthenticatorKind::Sasl] for SASL-SCRAM authentication.
194    /// This is used prior to [Client::verify_sasl_proof].
195    pub async fn generate_sasl_challenge(
196        &self,
197        user: &String,
198        client_nonce: &String,
199    ) -> Result<SASLChallengeResponse, AdapterError> {
200        let (tx, rx) = oneshot::channel();
201        self.send(Command::AuthenticateGetSASLChallenge {
202            role_name: user.to_string(),
203            nonce: client_nonce.to_string(),
204            tx,
205        });
206        let response = rx.await.expect("sender dropped")?;
207        Ok(response)
208    }
209
210    /// Used by [mz_auth::AuthenticatorKind::Sasl] for SASL-SCRAM authentication.
211    /// This is used after [Client::generate_sasl_challenge].
212    pub async fn verify_sasl_proof(
213        &self,
214        user: &String,
215        proof: &String,
216        nonce: &String,
217        mock_hash: &String,
218    ) -> Result<(SASLVerifyProofResponse, Authenticated), AdapterError> {
219        let (tx, rx) = oneshot::channel();
220        self.send(Command::AuthenticateVerifySASLProof {
221            role_name: user.to_string(),
222            proof: proof.to_string(),
223            auth_message: nonce.to_string(),
224            mock_hash: mock_hash.to_string(),
225            tx,
226        });
227        let response = rx.await.expect("sender dropped")?;
228        Ok((response, Authenticated))
229    }
230
231    /// Checks if a role exists and has the `LOGIN` attribute.
232    pub async fn role_can_login(&self, role_name: &str) -> Result<(), AdapterError> {
233        let (tx, rx) = oneshot::channel();
234        self.send(Command::CheckRoleCanLogin {
235            role_name: role_name.to_string(),
236            tx,
237        });
238        rx.await.expect("sender dropped")
239    }
240
241    /// Upgrades this client to a session client.
242    ///
243    /// A session is a connection that has successfully negotiated parameters,
244    /// like the user. Most coordinator operations are available only after
245    /// upgrading a connection to a session.
246    ///
247    /// Returns a new client that is bound to the session and a response
248    /// containing various details about the startup.
249    #[mz_ore::instrument(level = "debug")]
250    pub async fn startup(&self, session: Session) -> Result<SessionClient, AdapterError> {
251        let user = session.user().clone();
252        let conn_id = session.conn_id().clone();
253        let secret_key = session.secret_key();
254        let uuid = session.uuid();
255        let client_ip = session.client_ip();
256        let application_name = session.application_name().into();
257        let notice_tx = session.retain_notice_transmitter();
258
259        let (tx, rx) = oneshot::channel();
260
261        // ~~SPOOKY ZONE~~
262        //
263        // This guard prevents a race where the startup command finishes, but the Future returned
264        // by this function is concurrently dropped, so we never create a `SessionClient` and thus
265        // never cleanup the initialized Session.
266        //
267        // NOTE: Terminate must only be sent for a successful startup. On a failed startup the
268        // Coordinator never registered the connection, so a Terminate for it would trip the
269        // Coordinator's "unknown connection" assertion. There is also nothing that needs
270        // cleaning up, see the invariant on `Coordinator::handle_startup_inner`.
271        let rx = rx.with_guard(|resp: Result<StartupResponse, _>| {
272            if resp.is_ok() {
273                self.send(Command::Terminate {
274                    conn_id: conn_id.clone(),
275                    tx: None,
276                });
277            }
278        });
279
280        self.send(Command::Startup {
281            tx,
282            user,
283            conn_id: conn_id.clone(),
284            secret_key,
285            uuid,
286            client_ip: client_ip.copied(),
287            application_name,
288            notice_tx,
289        });
290
291        // When startup fails, no need to call terminate (handle_startup does this). Delay creating
292        // the client until after startup to sidestep the panic in its `Drop` implementation.
293        let response = rx.await.expect("sender dropped")?;
294
295        // Create the client as soon as startup succeeds (before any await points) so its `Drop` can
296        // handle termination.
297        // Build the PeekClient with controller handles returned from startup.
298        let StartupResponse {
299            role_id,
300            write_notify,
301            session_defaults,
302            catalog,
303            storage_collections,
304            transient_id_gen,
305            optimizer_metrics,
306            persist_client,
307            statement_logging_frontend,
308            superuser_attribute,
309            occ_write_semaphore,
310            frontend_read_then_write_enabled,
311            group_commit_notifier,
312            read_only,
313        } = response;
314
315        let peek_client = PeekClient::new(
316            self.clone(),
317            &catalog,
318            storage_collections,
319            transient_id_gen,
320            optimizer_metrics,
321            persist_client,
322            statement_logging_frontend,
323            occ_write_semaphore,
324            frontend_read_then_write_enabled,
325            group_commit_notifier,
326            read_only,
327        );
328
329        let mut client = SessionClient {
330            inner: Some(self.clone()),
331            session: Some(session),
332            timeouts: Timeout::new(),
333            environment_id: self.environment_id.clone(),
334            segment_client: self.segment_client.clone(),
335            peek_client,
336            enable_frontend_peek_sequencing: false, // initialized below, once we have a ConnCatalog
337        };
338
339        let session = client.session();
340
341        // Apply the superuser attribute to the session's user if
342        // it exists.
343        if let SuperuserAttribute(Some(superuser)) = superuser_attribute {
344            session.apply_internal_user_metadata(InternalUserMetadata { superuser });
345        }
346
347        session.initialize_role_metadata(role_id);
348        let vars_mut = session.vars_mut();
349        for (name, val) in session_defaults {
350            if let Err(err) = vars_mut.set_default(&name, val.borrow()) {
351                // Note: erroring here is unexpected, but we don't want to panic if somehow our
352                // assumptions are wrong.
353                tracing::error!("failed to set peristed default, {err:?}");
354            }
355        }
356        session
357            .vars_mut()
358            .end_transaction(EndTransactionAction::Commit);
359
360        // Stash the future that notifies us of builtin table writes completing, we'll block on
361        // this future before allowing queries from this session against relevant relations.
362        //
363        // Note: We stash the future as opposed to waiting on it here to prevent blocking session
364        // creation on builtin table updates. This improves the latency for session creation and
365        // reduces scheduling load on any dataflows that read from these builtin relations, since
366        // it allows updates to be batched.
367        session.set_builtin_table_updates(write_notify);
368
369        let catalog = catalog.for_session(session);
370
371        let cluster_active = session.vars().cluster().to_string();
372        if session.vars().welcome_message() {
373            let cluster_info = if catalog.resolve_cluster(Some(&cluster_active)).is_err() {
374                format!("{cluster_active} (does not exist)")
375            } else {
376                cluster_active.to_string()
377            };
378
379            // Emit a welcome message, optimized for readability by humans using
380            // interactive tools. If you change the message, make sure that it
381            // formats nicely in both `psql` and the console's SQL shell.
382            session.add_notice(AdapterNotice::Welcome(format!(
383                "connected to Materialize v{}
384  Environment ID: {}
385  Region: {}
386  User: {}
387  Cluster: {}
388  Database: {}
389  {}
390  Session UUID: {}
391
392Issue a SQL query to get started. Need help?
393  View documentation: https://materialize.com/s/docs
394  Join our Slack community: https://materialize.com/s/chat
395    ",
396                session.vars().build_info().semver_version(),
397                self.environment_id,
398                self.environment_id.region(),
399                session.vars().user().name,
400                cluster_info,
401                session.vars().database(),
402                match session.vars().search_path() {
403                    [schema] => format!("Schema: {}", schema),
404                    schemas => format!(
405                        "Search path: {}",
406                        schemas.iter().map(|id| id.to_string()).join(", ")
407                    ),
408                },
409                session.uuid(),
410            )));
411        }
412
413        if session.vars().current_object_missing_warnings() {
414            if catalog.active_database().is_none() {
415                let db = session.vars().database().into();
416                session.add_notice(AdapterNotice::UnknownSessionDatabase(db));
417            }
418        }
419
420        // Users stub their toe on their default cluster not existing, so we provide a notice to
421        // help guide them on what do to.
422        let cluster_var = session
423            .vars()
424            .inspect(CLUSTER.name())
425            .expect("cluster should exist");
426        if session.vars().current_object_missing_warnings()
427            && catalog.resolve_cluster(Some(&cluster_active)).is_err()
428        {
429            let cluster_notice = 'notice: {
430                if cluster_var.inspect_session_value().is_some() {
431                    break 'notice Some(AdapterNotice::DefaultClusterDoesNotExist {
432                        name: cluster_active,
433                        kind: "session",
434                        suggested_action: "Pick an extant cluster with SET CLUSTER = name. Run SHOW CLUSTERS to see available clusters.".into(),
435                    });
436                }
437
438                let role_default = catalog.get_role(catalog.active_role_id());
439                let role_cluster = match role_default.vars().get(CLUSTER.name()) {
440                    Some(OwnedVarInput::Flat(name)) => Some(name),
441                    None => None,
442                    // This is unexpected!
443                    Some(v @ OwnedVarInput::SqlSet(_)) => {
444                        tracing::warn!(?v, "SqlSet found for cluster Role Default");
445                        break 'notice None;
446                    }
447                };
448
449                let alter_role = "with `ALTER ROLE <role> SET cluster TO <cluster>;`";
450                match role_cluster {
451                    // If there is no default, suggest a Role default.
452                    None => Some(AdapterNotice::DefaultClusterDoesNotExist {
453                        name: cluster_active,
454                        kind: "system",
455                        suggested_action: format!(
456                            "Set a default cluster for the current role {alter_role}."
457                        ),
458                    }),
459                    // If the default does not exist, suggest to change it.
460                    Some(_) => Some(AdapterNotice::DefaultClusterDoesNotExist {
461                        name: cluster_active,
462                        kind: "role",
463                        suggested_action: format!(
464                            "Change the default cluster for the current role {alter_role}."
465                        ),
466                    }),
467                }
468            };
469
470            if let Some(notice) = cluster_notice {
471                session.add_notice(notice);
472            }
473        }
474
475        client.enable_frontend_peek_sequencing = ENABLE_FRONTEND_PEEK_SEQUENCING
476            .require(catalog.system_vars())
477            .is_ok();
478
479        Ok(client)
480    }
481
482    /// Cancels the query currently running on the specified connection.
483    pub fn cancel_request(&self, conn_id: ConnectionIdType, secret_key: u32) {
484        self.send(Command::CancelRequest {
485            conn_id,
486            secret_key,
487        });
488    }
489
490    /// Executes a single SQL statement that returns rows as the
491    /// `mz_support` user.
492    pub async fn support_execute_one(
493        &self,
494        sql: &str,
495    ) -> Result<Pin<Box<dyn Stream<Item = PeekResponseUnary> + Send>>, anyhow::Error> {
496        // Connect to the coordinator.
497        let conn_id = self.new_conn_id()?;
498        let session = self.new_session(
499            SessionConfig {
500                conn_id,
501                uuid: Uuid::new_v4(),
502                user: SUPPORT_USER.name.clone(),
503                client_ip: None,
504                external_metadata_rx: None,
505                helm_chart_version: None,
506                authenticator_kind: AuthenticatorKind::None,
507                groups: None,
508            },
509            Authenticated,
510        );
511        let mut session_client = self.startup(session).await?;
512
513        // Parse the SQL statement.
514        let stmts = mz_sql::parse::parse(sql)?;
515        if stmts.len() != 1 {
516            bail!("must supply exactly one query");
517        }
518        let StatementParseResult { ast: stmt, sql } = stmts.into_element();
519
520        const EMPTY_PORTAL: &str = "";
521        session_client.start_transaction(Some(1))?;
522        session_client
523            .declare(EMPTY_PORTAL.into(), stmt, sql.to_string())
524            .await?;
525
526        let execute_result = session_client
527            .execute(EMPTY_PORTAL.into(), futures::future::pending(), None)
528            .await?;
529        match execute_result {
530            (ExecuteResponse::SendingRowsStreaming { mut rows, .. }, _) => {
531                // We have to only drop the session client _after_ we read the
532                // result. Otherwise the peek will get cancelled right when we
533                // drop the session client. So we wrap it up in an extra stream
534                // like this, which owns the client and can return it.
535                let owning_response_stream = async_stream::stream! {
536                    while let Some(rows) = rows.next().await {
537                        yield rows;
538                    }
539                    drop(session_client);
540                };
541                Ok(Box::pin(owning_response_stream))
542            }
543            r => bail!("unsupported response type: {r:?}"),
544        }
545    }
546
547    /// Returns the metrics associated with the adapter layer.
548    pub fn metrics(&self) -> &Metrics {
549        &self.metrics
550    }
551
552    /// The current time according to the [`Client`].
553    pub fn now(&self) -> DateTime<Utc> {
554        to_datetime((self.now)())
555    }
556
557    /// Get a metadata and a channel that can be used to append to a webhook source.
558    pub async fn get_webhook_appender(
559        &self,
560        database: String,
561        schema: String,
562        name: String,
563    ) -> Result<AppendWebhookResponse, AppendWebhookError> {
564        let (tx, rx) = oneshot::channel();
565
566        // Send our request.
567        self.send(Command::GetWebhook {
568            database,
569            schema,
570            name,
571            tx,
572        });
573
574        // Using our one shot channel to get the result, returning an error if the sender dropped.
575        let response = rx
576            .await
577            .map_err(|_| anyhow::anyhow!("failed to receive webhook response"))?;
578
579        response
580    }
581
582    /// Gets the current value of all system variables.
583    pub async fn get_system_vars(&self) -> SystemVars {
584        let (tx, rx) = oneshot::channel();
585        self.send(Command::GetSystemVars { tx });
586        rx.await.expect("coordinator unexpectedly gone")
587    }
588
589    /// Returns a snapshot of the catalog.
590    ///
591    /// Does a Coordinator round-trip. Session-bound callers should
592    /// prefer [`SessionClient::catalog_snapshot`], which serves from the
593    /// session's snapshot cache.
594    pub async fn catalog_snapshot_expensive(&self) -> Arc<Catalog> {
595        let (tx, rx) = oneshot::channel();
596        self.send(Command::CatalogSnapshot { tx });
597        let CatalogSnapshot { catalog } = rx.await.expect("coordinator unexpectedly gone");
598        catalog
599    }
600
601    /// Reconciles the coordinator's scoped feature-flag working copy towards
602    /// `overrides`. Used by the system-parameter sync loop from continuous
603    /// LaunchDarkly evaluation.
604    ///
605    /// `prune_scope` bounds which objects' rows the reconcile may remove (the
606    /// objects `overrides` was evaluated for). The sync loop passes the live
607    /// objects from its snapshot. See
608    /// [`crate::catalog::Op::UpdateScopedSystemParameters`].
609    pub async fn update_scoped_system_parameters(
610        &self,
611        overrides: ScopedParameters,
612        prune_scope: ScopedParametersScope,
613    ) {
614        let (tx, rx) = oneshot::channel();
615        self.send(Command::UpdateScopedSystemParameters {
616            overrides,
617            prune_scope,
618            tx,
619        });
620        let _ = rx.await;
621    }
622
623    /// Installs (or replaces) the shared system-parameter frontend on the
624    /// coordinator, letting the create-cluster / create-replica paths resolve a
625    /// new object's scoped overrides synchronously. Sent by the sync loop each
626    /// time it (re)initializes the frontend. Fire-and-forget.
627    pub fn install_scoped_system_parameter_frontend(&self, frontend: Arc<SystemParameterFrontend>) {
628        self.send(Command::InstallScopedSystemParameterFrontend { frontend });
629    }
630
631    #[instrument(level = "debug")]
632    pub(crate) fn try_send(&self, cmd: Command) -> bool {
633        self.inner_cmd_tx
634            .send((OpenTelemetryContext::obtain(), cmd))
635            .is_ok()
636    }
637
638    #[instrument(level = "debug")]
639    pub(crate) fn send(&self, cmd: Command) {
640        assert!(self.try_send(cmd), "coordinator unexpectedly gone");
641    }
642}
643
644/// A coordinator client that is bound to a connection.
645///
646/// See also [`Client`].
647pub struct SessionClient {
648    // Invariant: inner may only be `None` after the session has been terminated.
649    // Once the session is terminated, no communication to the Coordinator
650    // should be attempted.
651    inner: Option<Client>,
652    // Invariant: session may only be `None` during a method call. Every public
653    // method must ensure that `Session` is `Some` before it returns.
654    session: Option<Session>,
655    timeouts: Timeout,
656    segment_client: Option<mz_segment::Client>,
657    environment_id: EnvironmentId,
658    /// Client for frontend peek sequencing; populated at connection startup.
659    peek_client: PeekClient,
660    /// Whether frontend peek sequencing is enabled; initialized at connection startup.
661    // TODO(peek-seq): Currently, this is initialized only at session startup. We'll be able to
662    // check the actual feature flag value at every peek (without a Coordinator call) once we'll
663    // always have a catalog snapshot at hand.
664    pub enable_frontend_peek_sequencing: bool,
665}
666
667impl SessionClient {
668    /// Parses a SQL expression, reporting failures as a telemetry event if
669    /// possible.
670    pub fn parse<'a>(
671        &self,
672        sql: &'a str,
673    ) -> Result<Result<Vec<StatementParseResult<'a>>, ParserStatementError>, String> {
674        match mz_sql::parse::parse_with_limit(sql) {
675            Ok(Err(e)) => {
676                self.track_statement_parse_failure(&e);
677                Ok(Err(e))
678            }
679            r => r,
680        }
681    }
682
683    fn track_statement_parse_failure(&self, parse_error: &ParserStatementError) {
684        let session = self.session.as_ref().expect("session invariant violated");
685        let Some(user_id) = session.user().external_metadata.as_ref().map(|m| m.user_id) else {
686            return;
687        };
688        let Some(segment_client) = &self.segment_client else {
689            return;
690        };
691        let Some(statement_kind) = parse_error.statement else {
692            return;
693        };
694        let Some((action, object_type)) = telemetry::analyze_audited_statement(statement_kind)
695        else {
696            return;
697        };
698        let event_type = StatementFailureType::ParseFailure;
699        let event_name = format!(
700            "{} {} {}",
701            object_type.as_title_case(),
702            action.as_title_case(),
703            event_type.as_title_case(),
704        );
705        segment_client.environment_track(
706            &self.environment_id,
707            event_name,
708            json!({
709                "statement_kind": statement_kind,
710                "error": &parse_error.error,
711            }),
712            EventDetails {
713                user_id: Some(user_id),
714                application_name: Some(session.application_name()),
715                ..Default::default()
716            },
717        );
718    }
719
720    // Verify and return the named prepared statement. We need to verify each use
721    // to make sure the prepared statement is still safe to use.
722    pub async fn get_prepared_statement(
723        &mut self,
724        name: &str,
725    ) -> Result<&PreparedStatement, AdapterError> {
726        let catalog = self.catalog_snapshot("get_prepared_statement").await;
727        Coordinator::verify_prepared_statement(&catalog, self.session(), name)?;
728        Ok(self
729            .session()
730            .get_prepared_statement_unverified(name)
731            .expect("must exist"))
732    }
733
734    /// Saves the parsed statement as a prepared statement.
735    ///
736    /// The prepared statement is saved in the connection's [`crate::session::Session`]
737    /// under the specified name.
738    pub async fn prepare(
739        &mut self,
740        name: String,
741        stmt: Option<Statement<Raw>>,
742        sql: String,
743        param_types: Vec<Option<SqlScalarType>>,
744    ) -> Result<(), AdapterError> {
745        let catalog = self.catalog_snapshot("prepare").await;
746
747        // Note: This failpoint is used to simulate a request outliving the external connection
748        // that made it.
749        let mut async_pause = false;
750        (|| {
751            fail::fail_point!("async_prepare", |val| {
752                async_pause = val.map_or(false, |val| val.parse().unwrap_or(false))
753            });
754        })();
755        if async_pause {
756            tokio::time::sleep(Duration::from_secs(1)).await;
757        };
758
759        let desc = Coordinator::describe(&catalog, self.session(), stmt.clone(), param_types)?;
760        let now = self.now();
761        let state_revision = StateRevision {
762            catalog_revision: catalog.transient_revision(),
763            session_state_revision: self.session().state_revision(),
764        };
765        self.session()
766            .set_prepared_statement(name, stmt, sql, desc, state_revision, now);
767        Ok(())
768    }
769
770    /// Binds a statement to a portal.
771    #[mz_ore::instrument(level = "debug")]
772    pub async fn declare(
773        &mut self,
774        name: String,
775        stmt: Statement<Raw>,
776        sql: String,
777    ) -> Result<(), AdapterError> {
778        let catalog = self.catalog_snapshot("declare").await;
779        let param_types = vec![];
780        let desc =
781            Coordinator::describe(&catalog, self.session(), Some(stmt.clone()), param_types)?;
782        let params = vec![];
783        let result_formats = vec![mz_pgwire_common::Format::Text; desc.arity()];
784        let now = self.now();
785        let logging = self.session().mint_logging(sql, Some(&stmt), now);
786        let state_revision = StateRevision {
787            catalog_revision: catalog.transient_revision(),
788            session_state_revision: self.session().state_revision(),
789        };
790        self.session().set_portal(
791            name,
792            desc,
793            Some(stmt),
794            logging,
795            params,
796            result_formats,
797            state_revision,
798        )?;
799        Ok(())
800    }
801
802    /// Executes a previously-bound portal.
803    ///
804    /// Note: the provided `cancel_future` must be cancel-safe as it's polled in a `select!` loop.
805    ///
806    /// `outer_ctx_extra` is Some when we are executing as part of an outer statement, e.g., a FETCH
807    /// triggering the execution of the underlying query.
808    #[mz_ore::instrument(level = "debug")]
809    pub async fn execute(
810        &mut self,
811        portal_name: String,
812        cancel_future: impl Future<Output = std::io::Error> + Send,
813        outer_ctx_extra: Option<ExecuteContextGuard>,
814    ) -> Result<(ExecuteResponse, Instant), AdapterError> {
815        let execute_started = Instant::now();
816        let cancel_future = cancel_future.map(|_| ()).shared();
817
818        // Owning the end-of-execution obligation in this frame is what lets
819        // cancellation report an error: the inner future can be dropped without
820        // the obligation going with it. See `ExecutionLogging`.
821        let mut logging = ExecutionLogging::adopt(outer_ctx_extra, &self.peek_client);
822
823        let result = self
824            .execute_attempts(portal_name, &mut logging, cancel_future)
825            .await;
826
827        logging.retire(&result);
828
829        result.map(|response| (response, execute_started))
830    }
831
832    /// Runs the execution paths in order of preference: frontend peek
833    /// sequencing, frontend read-then-write sequencing, and finally the
834    /// coordinator via `Command::Execute`.
835    async fn execute_attempts(
836        &mut self,
837        portal_name: String,
838        logging: &mut ExecutionLogging,
839        cancel_future: impl Future<Output = ()> + Send + Clone,
840    ) -> Result<ExecuteResponse, AdapterError> {
841        // Unroll SQL `EXECUTE <prepared> (...)` so the inner statement
842        // flows through `try_frontend_peek` /
843        // `try_frontend_read_then_write` below, rather than being
844        // re-dispatched via `Command::Execute` from the coordinator's
845        // `Plan::Execute` handler. Without this, a prepared statement
846        // would route differently from the same statement issued
847        // directly.
848        //
849        // On a successful unroll, `unroll_sql_execute` also takes over
850        // statement logging on the outer portal, so that
851        // `mz_statement_execution_history` records `EXECUTE foo (...)`
852        // rather than the inner SQL.
853        let portal_name = self.unroll_sql_execute(portal_name, logging).await?;
854
855        // Attempt peek sequencing in the session task.
856        // If unsupported, fall back to the Coordinator path.
857        // TODO(peek-seq): wire up cancel_future
858        let peek_result = self.try_frontend_peek(&portal_name, logging).await?;
859        if let Some(resp) = peek_result {
860            debug!("frontend peek succeeded");
861            return Ok(resp);
862        }
863        debug!("frontend peek did not happen, trying frontend read-then-write");
864
865        // Attempt read-then-write sequencing in the session task.
866        let rtw_result = self
867            .try_frontend_read_then_write_with_cancel(&portal_name, logging, cancel_future.clone())
868            .await?;
869        if let Some(resp) = rtw_result {
870            debug!("frontend read-then-write succeeded");
871            return Ok(resp);
872        }
873        debug!("frontend read-then-write did not happen, falling back to `Command::Execute`");
874
875        // No frontend path took the statement over, so the coordinator retires
876        // whatever entry we hold, or begins its own if we hold none.
877        let outer_ctx_extra = logging.release();
878        self.send_with_cancel(
879            |tx, session| Command::Execute {
880                portal_name,
881                session,
882                tx,
883                outer_ctx_extra,
884            },
885            cancel_future,
886        )
887        .await
888    }
889
890    /// If the named portal binds a SQL `EXECUTE <prepared>`, resolve the
891    /// prepared statement, install a fresh portal for the inner statement
892    /// (carrying the EXECUTE's actual parameter values), and return that
893    /// portal's name so the caller can run `try_frontend_peek` /
894    /// `try_frontend_read_then_write` against it.
895    ///
896    /// Only ever unrolls one level: the parser rejects
897    /// `PREPARE foo AS EXECUTE bar` (matching Postgres), so the inner
898    /// statement is guaranteed not to be another `EXECUTE`. A failsafe below
899    /// surfaces an internal error if that invariant is ever violated.
900    ///
901    /// When the portal does not bind an `EXECUTE`, the common case, returns the
902    /// original portal name, costing only a portal lookup.
903    async fn unroll_sql_execute(
904        &mut self,
905        portal_name: String,
906        logging: &mut ExecutionLogging,
907    ) -> Result<String, AdapterError> {
908        let (stmt, params, outer_logging, outer_lifecycle_timestamps) = {
909            let session = self.session.as_ref().expect("SessionClient invariant");
910            let portal = match session.get_portal_unverified(&portal_name) {
911                Some(p) => p,
912                // No portal: let `try_frontend_peek` /
913                // `try_frontend_read_then_write` surface the
914                // standard "missing portal" error.
915                None => return Ok(portal_name),
916            };
917            match &portal.stmt {
918                Some(stmt) => (
919                    Arc::clone(stmt),
920                    portal.parameters.clone(),
921                    Arc::clone(&portal.logging),
922                    portal.lifecycle_timestamps.clone(),
923                ),
924                None => return Ok(portal_name),
925            }
926        };
927
928        // Only EXECUTE statements need unrolling. Bail out before taking a
929        // catalog snapshot in the (overwhelmingly common) non-EXECUTE case.
930        if !matches!(&*stmt, Statement::Execute(_)) {
931            return Ok(portal_name);
932        }
933
934        let catalog = self.catalog_snapshot("unroll_sql_execute").await;
935
936        // Validate the outer EXECUTE portal against the (possibly newer)
937        // catalog: ensures the recorded portal description still matches
938        // what describing the EXECUTE would produce now.
939        {
940            let session = self.session.as_mut().expect("SessionClient invariant");
941            Coordinator::verify_portal(&catalog, session, &portal_name)?;
942        }
943
944        // Take over the EXECUTE itself, so that a planning error below produces
945        // an errored end-event in `mz_statement_execution_history` rather than
946        // no entry at all. We pass the *outer* portal's `logging` and
947        // pgwire-bound `params` so the recorded entry shows the user-visible
948        // `EXECUTE foo (...)`, not the inner SQL. The inner statement gets its
949        // own `query_total` increment when a frontend path takes it over, or,
950        // on bailout, in the coordinator's `handle_execute`.
951        logging.take_over(
952            &self.peek_client,
953            self.session.as_mut().expect("SessionClient invariant"),
954            Some(&stmt),
955            &params,
956            &outer_logging,
957            &catalog,
958            outer_lifecycle_timestamps,
959            TakeOver::UnrolledExecute,
960        );
961
962        self.install_inner_portal_for_execute(&catalog, &stmt, &params)
963    }
964
965    /// Helper for [`Self::unroll_sql_execute`]: plans the outer
966    /// `Statement::Execute`, verifies the referenced prepared statement, and
967    /// installs a fresh portal carrying the inner statement plus the
968    /// EXECUTE's bound parameter values. Returns the new portal's name.
969    fn install_inner_portal_for_execute(
970        &mut self,
971        catalog: &Arc<Catalog>,
972        stmt: &Arc<Statement<Raw>>,
973        params: &mz_sql::plan::Params,
974    ) -> Result<String, AdapterError> {
975        use mz_sql::plan::Plan;
976
977        let execute_plan = {
978            let session = self.session.as_mut().expect("SessionClient invariant");
979            let conn_catalog = catalog.for_session(session);
980            let (resolved_stmt, resolved_ids) =
981                mz_sql::names::resolve(&conn_catalog, (**stmt).clone())?;
982            let pcx = session.pcx();
983            let (plan, _sql_impl_ids) = mz_sql::plan::plan(
984                Some(pcx),
985                &conn_catalog,
986                resolved_stmt,
987                params,
988                &resolved_ids,
989            )?;
990            match plan {
991                Plan::Execute(plan) => plan,
992                other => {
993                    // Planning a `Statement::Execute` must yield
994                    // `Plan::Execute`. If it doesn't, the planner
995                    // contract is broken.
996                    return Err(AdapterError::Internal(format!(
997                        "planning Statement::Execute yielded unexpected plan: {:?}",
998                        mz_sql::plan::PlanKind::from(&other),
999                    )));
1000                }
1001            }
1002        };
1003
1004        // Verify and install the inner portal. The new portal carries the
1005        // inner prepared statement's `logging`, but the logging slot already
1006        // holds the EXECUTE-level entry, so a frontend path taking the inner
1007        // statement over inherits that entry instead of starting fresh from
1008        // this portal.
1009        let session = self.session.as_mut().expect("SessionClient invariant");
1010        Coordinator::verify_prepared_statement(catalog, session, &execute_plan.name)?;
1011        let ps = session
1012            .get_prepared_statement_unverified(&execute_plan.name)
1013            .expect("verified above");
1014        let inner_stmt = ps.stmt().cloned();
1015        let inner_desc = ps.desc().clone();
1016        let state_revision = ps.state_revision;
1017        let inner_logging = Arc::clone(ps.logging());
1018
1019        // Failsafe: `PREPARE foo AS EXECUTE bar` is rejected by the parser,
1020        // so the resolved inner statement must not be another `EXECUTE`. If
1021        // that ever changes, we'd silently skip frontend sequencing for the
1022        // deeper EXECUTEs. Surface it as an internal error instead.
1023        if let Some(inner) = inner_stmt.as_ref() {
1024            if matches!(inner, Statement::Execute(_)) {
1025                return Err(AdapterError::Internal(format!(
1026                    "nested EXECUTE: prepared statement {} resolves to another EXECUTE; \
1027                     parser should reject `PREPARE ... AS EXECUTE ...`",
1028                    execute_plan.name.quoted(),
1029                )));
1030            }
1031        }
1032
1033        session.create_new_portal(
1034            inner_stmt,
1035            inner_logging,
1036            inner_desc,
1037            execute_plan.params,
1038            Vec::new(),
1039            state_revision,
1040        )
1041    }
1042
1043    fn now(&self) -> EpochMillis {
1044        (self.inner().now)()
1045    }
1046
1047    fn now_datetime(&self) -> DateTime<Utc> {
1048        to_datetime(self.now())
1049    }
1050
1051    /// Starts a transaction based on implicit:
1052    /// - `None`: InTransaction
1053    /// - `Some(1)`: Started
1054    /// - `Some(n > 1)`: InTransactionImplicit
1055    /// - `Some(0)`: no change
1056    pub fn start_transaction(&mut self, implicit: Option<usize>) -> Result<(), AdapterError> {
1057        let now = self.now_datetime();
1058        let session = self.session.as_mut().expect("session invariant violated");
1059        let result = match implicit {
1060            None => session.start_transaction(now, None, None),
1061            Some(stmts) => {
1062                session.start_transaction_implicit(now, stmts);
1063                Ok(())
1064            }
1065        };
1066        result
1067    }
1068
1069    /// Ends a transaction. Even if an error is returned, guarantees that the transaction in the
1070    /// session and Coordinator has cleared its state.
1071    #[instrument(level = "debug")]
1072    pub async fn end_transaction(
1073        &mut self,
1074        action: EndTransactionAction,
1075    ) -> Result<ExecuteResponse, AdapterError> {
1076        let res = self
1077            .send(|tx, session| Command::Commit {
1078                action,
1079                session,
1080                tx,
1081            })
1082            .await;
1083        // Commit isn't guaranteed to set the session's state to anything specific, so clear it
1084        // here. It's safe to ignore the returned `TransactionStatus` because that doesn't contain
1085        // any data that the Coordinator must act on for correctness.
1086        let _ = self.session().clear_transaction();
1087        res
1088    }
1089
1090    /// Fails a transaction.
1091    pub fn fail_transaction(&mut self) {
1092        let session = self.session.take().expect("session invariant violated");
1093        let session = session.fail_transaction();
1094        self.session = Some(session);
1095    }
1096
1097    /// Fetches the catalog, served from the session-side snapshot cache when
1098    /// the catalog is unchanged since the cached snapshot was taken. See
1099    /// [`PeekClient::catalog_snapshot`].
1100    #[instrument(level = "debug")]
1101    pub async fn catalog_snapshot(&mut self, context: &str) -> Arc<Catalog> {
1102        self.peek_client.catalog_snapshot(context).await
1103    }
1104
1105    /// Reports whether `enable_statement_arrival_logging` is on.
1106    pub async fn statement_arrival_logging_enabled(&mut self) -> bool {
1107        let catalog = self.catalog_snapshot("statement_arrival_logging").await;
1108        catalog.system_config().enable_statement_arrival_logging()
1109    }
1110
1111    /// Reports whether `enable_extended_protocol_implicit_transaction` is on.
1112    pub async fn extended_protocol_implicit_transaction_enabled(&mut self) -> bool {
1113        let catalog = self
1114            .catalog_snapshot("extended_protocol_implicit_transaction")
1115            .await;
1116        catalog
1117            .system_config()
1118            .enable_extended_protocol_implicit_transaction()
1119    }
1120
1121    /// Dumps the catalog to a JSON string.
1122    ///
1123    /// No authorization is performed, so access to this function must be limited to internal
1124    /// servers or superusers.
1125    pub async fn dump_catalog(&mut self) -> Result<CatalogDump, AdapterError> {
1126        let catalog = self.catalog_snapshot("dump_catalog").await;
1127        catalog.dump().map_err(AdapterError::from)
1128    }
1129
1130    /// Checks the catalog for internal consistency, returning a JSON object describing the
1131    /// inconsistencies, if there are any.
1132    ///
1133    /// No authorization is performed, so access to this function must be limited to internal
1134    /// servers or superusers.
1135    pub async fn check_catalog(&mut self) -> Result<(), serde_json::Value> {
1136        let catalog = self.catalog_snapshot("check_catalog").await;
1137        catalog.check_consistency()
1138    }
1139
1140    /// Checks the coordinator for internal consistency, returning a JSON object describing the
1141    /// inconsistencies, if there are any. This is a superset of checks that check_catalog performs,
1142    ///
1143    /// No authorization is performed, so access to this function must be limited to internal
1144    /// servers or superusers.
1145    pub async fn check_coordinator(&self) -> Result<(), serde_json::Value> {
1146        self.send_without_session(|tx| Command::CheckConsistency { tx })
1147            .await
1148            .map_err(|inconsistencies| {
1149                serde_json::to_value(inconsistencies).unwrap_or_else(|_| {
1150                    serde_json::Value::String("failed to serialize inconsistencies".to_string())
1151                })
1152            })
1153    }
1154
1155    pub async fn dump_coordinator_state(&self) -> Result<serde_json::Value, anyhow::Error> {
1156        self.send_without_session(|tx| Command::Dump { tx }).await
1157    }
1158
1159    /// Tells the coordinator a statement has finished execution, in the cases
1160    /// where we have no other reason to communicate with the coordinator.
1161    pub fn retire_execute(
1162        &self,
1163        guard: ExecuteContextGuard,
1164        reason: StatementEndedExecutionReason,
1165    ) {
1166        if !guard.is_trivial() {
1167            let data = guard.defuse();
1168            let cmd = Command::RetireExecute { data, reason };
1169            self.inner().send(cmd);
1170        }
1171    }
1172
1173    /// Sets up a streaming COPY FROM STDIN operation.
1174    ///
1175    /// Sends a command to the coordinator to create a background batch
1176    /// builder task. Returns a [`CopyFromStdinWriter`] that pgwire uses
1177    /// to stream decoded rows.
1178    pub async fn start_copy_from_stdin(
1179        &mut self,
1180        target_id: CatalogItemId,
1181        target_name: String,
1182        columns: Vec<ColumnIndex>,
1183        row_desc: mz_repr::RelationDesc,
1184        params: mz_pgcopy::CopyFormatParams<'static>,
1185    ) -> Result<CopyFromStdinWriter, AdapterError> {
1186        self.send(|tx, session| Command::StartCopyFromStdin {
1187            target_id,
1188            target_name,
1189            columns,
1190            row_desc,
1191            params,
1192            session,
1193            tx,
1194        })
1195        .await
1196    }
1197
1198    /// Commits staged COPY FROM STDIN batches to a table.
1199    ///
1200    /// Adds the pre-built persist batches to the session's transaction
1201    /// operations. The actual commit happens when the transaction ends.
1202    pub fn stage_copy_from_stdin_batches(
1203        &mut self,
1204        target_id: CatalogItemId,
1205        batches: Vec<mz_persist_client::batch::ProtoBatch>,
1206    ) -> Result<(), AdapterError> {
1207        use crate::session::{TransactionOps, WriteOp};
1208        use mz_storage_client::client::TableData;
1209
1210        self.session()
1211            .add_transaction_ops(TransactionOps::Writes(vec![WriteOp {
1212                id: target_id,
1213                rows: TableData::Batches(batches.into()),
1214            }]))?;
1215        Ok(())
1216    }
1217
1218    /// Gets the current value of all system variables.
1219    pub async fn get_system_vars(&self) -> SystemVars {
1220        self.inner().get_system_vars().await
1221    }
1222
1223    /// Updates the specified system variables to the specified values.
1224    pub async fn set_system_vars(
1225        &mut self,
1226        vars: BTreeMap<String, String>,
1227    ) -> Result<(), AdapterError> {
1228        let conn_id = self.session().conn_id().clone();
1229        self.send_without_session(|tx| Command::SetSystemVars { vars, conn_id, tx })
1230            .await
1231    }
1232
1233    /// Injects audit events into the catalog via the coordinator.
1234    ///
1235    /// No authorization is performed, so access to this function must be limited to internal
1236    /// servers or superusers.
1237    pub async fn inject_audit_events(
1238        &mut self,
1239        events: Vec<crate::catalog::InjectedAuditEvent>,
1240    ) -> Result<(), AdapterError> {
1241        let conn_id = self.session().conn_id().clone();
1242        self.send_without_session(|tx| Command::InjectAuditEvents {
1243            events,
1244            conn_id,
1245            tx,
1246        })
1247        .await
1248    }
1249
1250    /// Terminates the client session.
1251    pub async fn terminate(&mut self) {
1252        let conn_id = self.session().conn_id().clone();
1253        let res = self
1254            .send_without_session(|tx| Command::Terminate {
1255                conn_id,
1256                tx: Some(tx),
1257            })
1258            .await;
1259        if let Err(e) = res {
1260            // Nothing we can do to handle a failed terminate so we just log and ignore it.
1261            error!("Unable to terminate session: {e:?}");
1262        }
1263        // Prevent any communication with Coordinator after session is terminated.
1264        self.inner = None;
1265    }
1266
1267    /// Returns a mutable reference to the session bound to this client.
1268    pub fn session(&mut self) -> &mut Session {
1269        self.session.as_mut().expect("session invariant violated")
1270    }
1271
1272    /// Returns a reference to the inner client.
1273    pub fn inner(&self) -> &Client {
1274        self.inner.as_ref().expect("inner invariant violated")
1275    }
1276
1277    async fn send_without_session<T, F>(&self, f: F) -> T
1278    where
1279        F: FnOnce(oneshot::Sender<T>) -> Command,
1280    {
1281        let (tx, rx) = oneshot::channel();
1282        self.inner().send(f(tx));
1283        rx.await.expect("sender dropped")
1284    }
1285
1286    #[instrument(level = "debug")]
1287    async fn send<T, F>(&mut self, f: F) -> Result<T, AdapterError>
1288    where
1289        F: FnOnce(oneshot::Sender<Response<T>>, Session) -> Command,
1290    {
1291        self.send_with_cancel(f, futures::future::pending()).await
1292    }
1293
1294    /// Send a [`Command`] to the Coordinator, with the ability to cancel the command.
1295    ///
1296    /// Note: the provided `cancel_future` must be cancel-safe as it's polled in a `select!` loop.
1297    #[instrument(level = "debug")]
1298    async fn send_with_cancel<T, F>(
1299        &mut self,
1300        f: F,
1301        cancel_future: impl Future<Output = ()> + Send,
1302    ) -> Result<T, AdapterError>
1303    where
1304        F: FnOnce(oneshot::Sender<Response<T>>, Session) -> Command,
1305    {
1306        let session = self.session.take().expect("session invariant violated");
1307        let mut typ = None;
1308        let application_name = session.application_name();
1309        let name_hint = ApplicationNameHint::from_str(application_name);
1310        let conn_id = session.conn_id().clone();
1311        let (tx, rx) = oneshot::channel();
1312
1313        // Destructure self so we can hold a mutable reference to the inner client and session at
1314        // the same time.
1315        let Self {
1316            inner: inner_client,
1317            session: client_session,
1318            ..
1319        } = self;
1320
1321        // TODO(parkmycar): Leaking this invariant here doesn't feel great, but calling
1322        // `self.client()` doesn't work because then Rust takes a borrow on the entirity of self.
1323        let inner_client = inner_client.as_ref().expect("inner invariant violated");
1324
1325        // ~~SPOOKY ZONE~~
1326        //
1327        // This guard prevents a race where a `Session` is returned on `rx` but never placed
1328        // back in `self` because the Future returned by this function is concurrently dropped
1329        // with the Coordinator sending a response.
1330        let mut guarded_rx = rx.with_guard(|response: Response<_>| {
1331            *client_session = Some(response.session);
1332        });
1333
1334        inner_client.send({
1335            let cmd = f(tx, session);
1336            // Measure the success and error rate of certain commands:
1337            // - declare reports success of SQL statement planning
1338            // - execute reports success of dataflow execution
1339            match cmd {
1340                Command::Execute { .. } => typ = Some("execute"),
1341                Command::GetWebhook { .. } => typ = Some("webhook"),
1342                Command::StartCopyFromStdin { .. }
1343                | Command::Startup { .. }
1344                | Command::AuthenticatePassword { .. }
1345                | Command::AuthenticateGetSASLChallenge { .. }
1346                | Command::AuthenticateVerifySASLProof { .. }
1347                | Command::CheckRoleCanLogin { .. }
1348                | Command::CatalogSnapshot { .. }
1349                | Command::Commit { .. }
1350                | Command::CancelRequest { .. }
1351                | Command::PrivilegedCancelRequest { .. }
1352                | Command::GetSystemVars { .. }
1353                | Command::SetSystemVars { .. }
1354                | Command::UpdateScopedSystemParameters { .. }
1355                | Command::InstallScopedSystemParameterFrontend { .. }
1356                | Command::Terminate { .. }
1357                | Command::RetireExecute { .. }
1358                | Command::CheckConsistency { .. }
1359                | Command::Dump { .. }
1360                | Command::GetComputeInstanceClient { .. }
1361                | Command::GetOracle { .. }
1362                | Command::DetermineRealTimeRecentTimestamp { .. }
1363                | Command::GetTransactionReadHoldsBundle { .. }
1364                | Command::StoreTransactionReadHolds { .. }
1365                | Command::ExecuteSlowPathPeek { .. }
1366                | Command::ExecuteSubscribe { .. }
1367                | Command::CopyToPreflight { .. }
1368                | Command::ExecuteCopyTo { .. }
1369                | Command::ExecuteSideEffectingFunc { .. }
1370                | Command::LookupConnection { .. }
1371                | Command::RegisterFrontendPeek { .. }
1372                | Command::UnregisterFrontendPeek { .. }
1373                | Command::ExplainTimestamp { .. }
1374                | Command::FrontendStatementLogging(..)
1375                | Command::InjectAuditEvents { .. }
1376                | Command::RegisterConnectionCancelWatch { .. }
1377                | Command::CreateInternalSubscribe { .. }
1378                | Command::AttemptWrite { .. }
1379                | Command::DropInternalSubscribe { .. } => {}
1380            };
1381            cmd
1382        });
1383
1384        let mut cancel_future = pin::pin!(cancel_future);
1385        let mut cancelled = false;
1386        loop {
1387            tokio::select! {
1388                res = &mut guarded_rx => {
1389                    // We received a result, so drop our guard to drop our borrows.
1390                    drop(guarded_rx);
1391
1392                    let res = res.expect("sender dropped");
1393                    let status = res.result.is_ok().then_some("success").unwrap_or("error");
1394                    if let Err(err) = res.result.as_ref() {
1395                        if name_hint.should_trace_errors() {
1396                            tracing::warn!(?err, ?name_hint, "adapter response error");
1397                        }
1398                    }
1399
1400                    if let Some(typ) = typ {
1401                        inner_client
1402                            .metrics
1403                            .commands
1404                            .with_label_values(&[typ, status, name_hint.as_str()])
1405                            .inc();
1406                    }
1407                    *client_session = Some(res.session);
1408                    return res.result;
1409                },
1410                _ = &mut cancel_future, if !cancelled => {
1411                    cancelled = true;
1412                    inner_client.send(Command::PrivilegedCancelRequest {
1413                        conn_id: conn_id.clone(),
1414                    });
1415                }
1416            };
1417        }
1418    }
1419
1420    pub fn add_idle_in_transaction_session_timeout(&mut self) {
1421        let session = self.session();
1422        let timeout_dur = session.vars().idle_in_transaction_session_timeout();
1423        if !timeout_dur.is_zero() {
1424            let timeout_dur = timeout_dur.clone();
1425            if let Some(txn) = session.transaction().inner() {
1426                let txn_id = txn.id.clone();
1427                let timeout = TimeoutType::IdleInTransactionSession(txn_id);
1428                self.timeouts.add_timeout(timeout, timeout_dur);
1429            }
1430        }
1431    }
1432
1433    pub fn remove_idle_in_transaction_session_timeout(&mut self) {
1434        let session = self.session();
1435        if let Some(txn) = session.transaction().inner() {
1436            let txn_id = txn.id.clone();
1437            self.timeouts
1438                .remove_timeout(&TimeoutType::IdleInTransactionSession(txn_id));
1439        }
1440    }
1441
1442    /// # Cancel safety
1443    ///
1444    /// This method is cancel safe. If `recv` is used as the event in a
1445    /// `tokio::select!` statement and some other branch
1446    /// completes first, it is guaranteed that no messages were received on this
1447    /// channel.
1448    pub async fn recv_timeout(&mut self) -> Option<TimeoutType> {
1449        self.timeouts.recv().await
1450    }
1451
1452    /// Attempt to sequence a peek from the session task.
1453    ///
1454    /// Returns `Ok(Some(response))` if we handled the peek, or `Ok(None)` to fall back to the
1455    /// Coordinator's sequencing. If it returns an error, it should be returned to the user.
1456    pub(crate) async fn try_frontend_peek(
1457        &mut self,
1458        portal_name: &str,
1459        logging: &mut ExecutionLogging,
1460    ) -> Result<Option<ExecuteResponse>, AdapterError> {
1461        if self.enable_frontend_peek_sequencing {
1462            let session = self.session.as_mut().expect("SessionClient invariant");
1463            self.peek_client
1464                .try_frontend_peek(portal_name, session, logging)
1465                .await
1466        } else {
1467            Ok(None)
1468        }
1469    }
1470
1471    /// Whether the frontend read-then-write path could take this portal over.
1472    ///
1473    /// The gate is deliberately cheap, a flag read and a portal lookup, because
1474    /// every statement that reaches `execute_attempts` without being handled by
1475    /// the peek path is tested against it. Everything expensive, including the
1476    /// coordinator round-trip that registers the connection cancel watch, sits
1477    /// behind it.
1478    fn frontend_read_then_write_applies(&self, portal_name: &str) -> bool {
1479        if !self.peek_client.frontend_read_then_write_enabled {
1480            return false;
1481        }
1482        let session = self.session.as_ref().expect("SessionClient invariant");
1483        match session.get_portal_unverified(portal_name) {
1484            Some(portal) => portal
1485                .stmt
1486                .as_deref()
1487                .is_some_and(is_read_then_write_statement),
1488            None => false,
1489        }
1490    }
1491
1492    /// Runs frontend read-then-write while reacting to both local/session
1493    /// cancellation and coordinator-issued connection cancellation.
1494    ///
1495    /// Returns `Ok(None)` when the statement is not eligible for this path and
1496    /// the caller must fall back to the coordinator, either because
1497    /// `try_frontend_read_then_write` declined it or because this wrapper did.
1498    ///
1499    /// Cancellation and statement timeout are never reported for a write that
1500    /// may have committed. Once a write has been submitted we await its
1501    /// definitive result instead of returning the cancellation.
1502    async fn try_frontend_read_then_write_with_cancel(
1503        &mut self,
1504        portal_name: &str,
1505        logging: &mut ExecutionLogging,
1506        cancel_future: impl Future<Output = ()> + Send,
1507    ) -> Result<Option<ExecuteResponse>, AdapterError> {
1508        // Bail out before the cancel-watch registration below, which is a
1509        // synchronous round-trip through the coordinator's command loop. A
1510        // statement this path will not take over must not pay for it, and must
1511        // not add queueing latency for other sessions either.
1512        if !self.frontend_read_then_write_applies(portal_name) {
1513            return Ok(None);
1514        }
1515
1516        let conn_id = self.session().conn_id().clone();
1517        let statement_timeout = *self.session().vars().statement_timeout();
1518        let inner_client = self.inner().clone();
1519        let attempt_state = Arc::new(FrontendWriteAttemptState::new());
1520
1521        let mut cancel_future = pin::pin!(cancel_future);
1522        let statement_timeout = async move {
1523            if statement_timeout.is_zero() {
1524                futures::future::pending::<()>().await;
1525            } else {
1526                tokio::time::sleep(statement_timeout).await;
1527            }
1528        };
1529        tokio::pin!(statement_timeout);
1530
1531        // Registering installs a fresh channel, so this cannot observe a
1532        // cancellation aimed at an earlier statement. The entry it leaves behind
1533        // is replaced by the next registration and removed when a statement
1534        // reaches the coordinator or the connection's state is cleared, so there
1535        // is nothing to unregister here.
1536        let mut connection_cancel_rx = {
1537            let register =
1538                self.peek_client
1539                    .call_coordinator(|tx| Command::RegisterConnectionCancelWatch {
1540                        conn_id: conn_id.clone(),
1541                        tx,
1542                    });
1543            tokio::pin!(register);
1544            tokio::select! {
1545                rx = &mut register => rx,
1546                _ = &mut cancel_future => {
1547                    inner_client.try_send(Command::PrivilegedCancelRequest {
1548                        conn_id: conn_id.clone(),
1549                    });
1550                    return Err(AdapterError::Canceled);
1551                }
1552                _ = &mut statement_timeout => {
1553                    inner_client.try_send(Command::PrivilegedCancelRequest {
1554                        conn_id: conn_id.clone(),
1555                    });
1556                    return Err(AdapterError::StatementTimeout);
1557                }
1558            }
1559        };
1560        if *connection_cancel_rx.borrow() {
1561            return Err(AdapterError::Canceled);
1562        }
1563        let connection_cancel = async move {
1564            if connection_cancel_rx.wait_for(|v| *v).await.is_err() {
1565                futures::future::pending::<()>().await;
1566            }
1567        };
1568        tokio::pin!(connection_cancel);
1569
1570        let frontend_read_then_write =
1571            self.try_frontend_read_then_write(portal_name, logging, Arc::clone(&attempt_state));
1572        tokio::pin!(frontend_read_then_write);
1573
1574        let requested = tokio::select! {
1575            response = &mut frontend_read_then_write => return response,
1576            _ = &mut cancel_future => FrontendWriteCancellation::Canceled,
1577            _ = &mut connection_cancel => FrontendWriteCancellation::Canceled,
1578            _ = &mut statement_timeout => FrontendWriteCancellation::StatementTimeout,
1579        };
1580
1581        attempt_state.request(requested);
1582        inner_client.try_send(Command::PrivilegedCancelRequest {
1583            conn_id: conn_id.clone(),
1584        });
1585
1586        if !attempt_state.write_submitted() {
1587            return Err(requested.into());
1588        }
1589
1590        // A submitted write can already be durable. Await its definitive result
1591        // rather than reporting cancellation or timeout incorrectly.
1592        frontend_read_then_write.await
1593    }
1594
1595    /// Attempt to sequence a read-then-write (DELETE/UPDATE/INSERT INTO ..
1596    /// SELECT .. FROM) from the session task.
1597    ///
1598    /// Returns `Ok(Some(response))` if we handled the operation, or `Ok(None)`
1599    /// to fall back to the Coordinator's sequencing. If it returns an error, it
1600    /// should be returned to the user.
1601    async fn try_frontend_read_then_write(
1602        &mut self,
1603        portal_name: &str,
1604        logging: &mut ExecutionLogging,
1605        attempt_state: Arc<FrontendWriteAttemptState>,
1606    ) -> Result<Option<ExecuteResponse>, AdapterError> {
1607        // Re-checked here rather than relying on the caller's gate. See the
1608        // module-level docs on `frontend_read_then_write` for why the flag is
1609        // fixed for the lifetime of the process.
1610        if !self.peek_client.frontend_read_then_write_enabled {
1611            return Ok(None);
1612        }
1613
1614        let catalog = self.catalog_snapshot("try_frontend_read_then_write").await;
1615
1616        let stmt = {
1617            let session = self.session.as_ref().expect("SessionClient invariant");
1618            let portal = match session.get_portal_unverified(portal_name) {
1619                Some(portal) => portal,
1620                None => return Ok(None), // Portal doesn't exist, fall back
1621            };
1622            portal.stmt.clone()
1623        };
1624
1625        let stmt = match stmt {
1626            Some(stmt) if is_read_then_write_statement(&stmt) => stmt,
1627            Some(_stmt) => {
1628                return Ok(None);
1629            }
1630            None => {
1631                return Ok(None);
1632            }
1633        };
1634
1635        // Verify and plan against one catalog snapshot. Pairing a stale plan
1636        // with a newer target generation could direct a write incorrectly.
1637        // A failed verification is not logged, mirroring the coordinator: the
1638        // portal is what statement logging draws its record from.
1639        Coordinator::verify_portal(
1640            &catalog,
1641            self.session.as_mut().expect("SessionClient invariant"),
1642            portal_name,
1643        )?;
1644
1645        let (params, logging_info, lifecycle_timestamps) = {
1646            let portal = self
1647                .session
1648                .as_ref()
1649                .expect("SessionClient invariant")
1650                .get_portal_unverified(portal_name)
1651                .expect("verified above");
1652            (
1653                portal.parameters.clone(),
1654                Arc::clone(&portal.logging),
1655                portal.lifecycle_timestamps.clone(),
1656            )
1657        };
1658
1659        // Past this point the coordinator never sees this statement, so every
1660        // exit has to produce an outcome for it. The remaining `Ok(None)`
1661        // bailouts are all above.
1662        logging.take_over(
1663            &self.peek_client,
1664            self.session.as_mut().expect("SessionClient invariant"),
1665            Some(&stmt),
1666            &params,
1667            &logging_info,
1668            &catalog,
1669            lifecycle_timestamps,
1670            TakeOver::StatementToRun,
1671        );
1672
1673        // Mirror the coordinator's transaction-state gate in `handle_execute`:
1674        // in a multi-statement transaction (an implicit batch or an explicit
1675        // block), the only DML allowed is an AST-constant INSERT without
1676        // RETURNING, which joins the transaction's write ops and commits at
1677        // transaction end. All other DML is prohibited because writes on this
1678        // path commit immediately and cannot be rolled back at transaction
1679        // end. `Failed` transactions pass through, pgwire only admits
1680        // COMMIT/ROLLBACK in that state.
1681        //
1682        // An AST-constant source can still plan to a read, so the check on the
1683        // planned selection further down narrows this.
1684        {
1685            let session = self.session.as_ref().expect("SessionClient invariant");
1686            // `Started` does not mean "single statement" on its own. An
1687            // extended-protocol pipeline keeps the transaction `Started` across
1688            // statements until `Sync`, so once it holds write ops this
1689            // statement runs alongside them and belongs with the
1690            // multi-statement cases. Committing here would commit against a
1691            // snapshot that lacks those ops, reordering this statement before
1692            // writes that a later pipeline error would roll back.
1693            let contains_ops = session.transaction().contains_ops();
1694            match session.transaction() {
1695                TransactionStatus::Default | TransactionStatus::Failed(_) => {}
1696                TransactionStatus::Started(_) if !contains_ops => {}
1697                TransactionStatus::Started(_)
1698                | TransactionStatus::InTransactionImplicit(_)
1699                | TransactionStatus::InTransaction(_) => {
1700                    let constant_insert = matches!(
1701                        &*stmt,
1702                        Statement::Insert(InsertStatement {
1703                            source, returning, ..
1704                        }) if returning.is_empty() && ConstantVisitor::insert_source(source)
1705                    );
1706                    if !constant_insert {
1707                        return Err(prohibited_in_transaction(&stmt));
1708                    }
1709                }
1710            }
1711        }
1712
1713        let (plan, target_cluster, resolved_ids, sql_impl_ids) = {
1714            let session = self.session.as_mut().expect("SessionClient invariant");
1715            let conn_catalog = catalog.for_session(session);
1716            let (stmt, resolved_ids) = mz_sql::names::resolve(&conn_catalog, (*stmt).clone())?;
1717            let pcx = session.pcx();
1718            let (plan, sql_impl_ids) =
1719                mz_sql::plan::plan(Some(pcx), &conn_catalog, stmt, &params, &resolved_ids)?;
1720
1721            let target_cluster = match session.transaction().cluster() {
1722                Some(cluster_id) => crate::coord::TargetCluster::Transaction(cluster_id),
1723                None => crate::coord::catalog_serving::auto_run_on_catalog_server(
1724                    &conn_catalog,
1725                    session,
1726                    &plan,
1727                ),
1728            };
1729
1730            (plan, target_cluster, resolved_ids, sql_impl_ids)
1731        };
1732
1733        // Reject mutations in read-only mode (e.g. during 0dt upgrades). Placed
1734        // where the coordinator has it, in `sequence_plan`: after planning, so a
1735        // statement that does not plan reports the planning error, and before
1736        // the cluster and RBAC checks below, which the coordinator also reports
1737        // second. Every sub-path from here on writes (constant INSERT and the
1738        // OCC INSERT/UPDATE/DELETE), so one check covers them all.
1739        if self.peek_client.read_only {
1740            return Err(AdapterError::ReadOnly);
1741        }
1742
1743        // Cluster restrictions and RBAC, mirroring the coordinator's checks
1744        // in sequencer.rs. Resolution may fail if the target cluster doesn't
1745        // exist. That gets reported later (with the correct error) by
1746        // `validate_read_then_write`. For the purposes of these checks we
1747        // treat it as "no cluster known", consistent with the coordinator.
1748        let (target_cluster_id, target_cluster_name) = {
1749            let session = self.session.as_ref().expect("SessionClient invariant");
1750            match catalog.resolve_target_cluster(target_cluster.clone(), session) {
1751                Ok(cluster) => (Some(cluster.id), Some(cluster.name.clone())),
1752                Err(_) => (None, None),
1753            }
1754        };
1755
1756        // Record the cluster before the checks below can fail, so that their
1757        // error rows carry it, as the coordinator's do.
1758        if let (Some(logging_id), Some(cluster_id), Some(cluster_name)) =
1759            (logging.id(), target_cluster_id, target_cluster_name.clone())
1760        {
1761            self.peek_client
1762                .log_set_cluster(logging_id, cluster_id, cluster_name);
1763        }
1764
1765        {
1766            let session = self.session.as_ref().expect("SessionClient invariant");
1767            let conn_catalog = catalog.for_session(session);
1768            if let Some(cluster_name) = &target_cluster_name {
1769                crate::coord::catalog_serving::check_cluster_restrictions(
1770                    cluster_name,
1771                    &conn_catalog,
1772                    &plan,
1773                )?;
1774            }
1775            if let Err(e) = mz_sql::rbac::check_plan(
1776                &conn_catalog,
1777                None,
1778                session,
1779                &plan,
1780                target_cluster_id,
1781                &resolved_ids,
1782                &sql_impl_ids,
1783            ) {
1784                return Err(e.into());
1785            }
1786        }
1787
1788        // Wait for any in-flight startup builtin-table appends that this plan
1789        // depends on. Mirrors the frontend_peek and coordinator sequencer
1790        // paths, and is a no-op for plans that don't depend on builtin tables.
1791        {
1792            let session = self.session.as_mut().expect("SessionClient invariant");
1793            if let Some((_, wait_future)) =
1794                crate::coord::appends::waiting_on_startup_appends(&catalog, session, &plan)
1795            {
1796                wait_future.await;
1797            }
1798        }
1799
1800        // The coordinator's per-plan checks, in the order it applies them:
1801        // `sequence_insert` rejects a transaction that cannot take a write
1802        // before it rejects the isolation level, and both it and
1803        // `sequence_read_then_write` reject bounded staleness before dispatching
1804        // on the plan. So both checks sit here, above the constant-INSERT
1805        // dispatch as well as the read-then-write path.
1806        //
1807        // `allows_writes` is only defined inside a transaction, which is also
1808        // the only place it can be false: outside one the session task opens a
1809        // fresh transaction with no ops. Autocommit statements therefore rely on
1810        // the check in `PeekClient::frontend_read_then_write` instead.
1811        {
1812            let session = self.session.as_ref().expect("SessionClient invariant");
1813            if session.transaction().is_in_multi_statement_transaction()
1814                && !session.transaction().allows_writes()
1815            {
1816                return Err(AdapterError::ReadOnlyTransaction);
1817            }
1818            if session
1819                .vars()
1820                .transaction_isolation()
1821                .is_bounded_staleness()
1822            {
1823                return Err(AdapterError::BoundedStalenessReadOnly);
1824            }
1825        }
1826
1827        // Handle ReadThenWrite plans or Insert plans.
1828        let rtw_plan = match plan {
1829            Plan::ReadThenWrite(rtw_plan) => rtw_plan,
1830            Plan::Insert(insert_plan) => {
1831                // A constant INSERT without RETURNING is a blind write, handled
1832                // here through the coordinator's `insert_constant` helper, which
1833                // buffers the rows as session write ops.
1834                //
1835                // Deciding that needs HIR lowered to MIR, because a VALUES list
1836                // is planned as a `Wrap` call at the HIR level.
1837                //
1838                // Only take that path when the HIR names no persisted
1839                // collections (no `Get` nodes on tables or MVs). The MIR
1840                // optimizer can fold an MV reference into a literal when the
1841                // MV's plan happens to be constant, but "plan is constant" is
1842                // NOT the same as "content is visible at the current
1843                // oracle_ts". A `REFRESH AT year 30000` MV has a constant plan
1844                // but no durable content until the refresh fires. Folding it
1845                // and blind-writing the literal would skip timestamp selection
1846                // and linearization, producing data that was never observable.
1847                // Preserving the HIR-level `Get` nodes routes the INSERT through
1848                // the RTW path, where timestamp selection handles REFRESH and
1849                // other time-dependent reads correctly.
1850                let has_read_deps = !insert_plan.values.depends_on().is_empty();
1851
1852                if !has_read_deps {
1853                    let optimized_mir = if insert_plan.values.as_const().is_some() {
1854                        // Already constant at HIR level - just lower without optimization
1855                        let expr = insert_plan
1856                            .values
1857                            .clone()
1858                            .lower(catalog.system_config(), None)?;
1859                        mz_expr::OptimizedMirRelationExpr(expr)
1860                    } else {
1861                        // Need to optimize to check if it becomes constant.
1862                        // Use one-shot expression prep so unmaterializable
1863                        // functions like current_user() are resolved before we
1864                        // decide whether this can use the blind-write path.
1865                        let optimizer_config =
1866                            optimize::OptimizerConfig::from(catalog.system_config());
1867                        let session = self.session.as_ref().expect("SessionClient invariant");
1868                        let prep = ExprPrepOneShot {
1869                            logical_time: EvalTime::NotAvailable,
1870                            session,
1871                            catalog_state: catalog.state(),
1872                        };
1873                        let mut optimizer =
1874                            optimize::view::Optimizer::new_with_prep(optimizer_config, None, prep);
1875                        match optimizer.optimize(insert_plan.values.clone()) {
1876                            Ok(expr) => expr,
1877                            Err(OptimizerError::UncallableFunction {
1878                                func: UnmaterializableFunc::MzNow,
1879                                ..
1880                            }) => {
1881                                // Preserve the established user-facing `mz_now()`
1882                                // error by falling back to the RTW validator.
1883                                let expr = insert_plan
1884                                    .values
1885                                    .clone()
1886                                    .lower(catalog.system_config(), None)?;
1887                                mz_expr::OptimizedMirRelationExpr(expr)
1888                            }
1889                            Err(e) => return Err(e.into()),
1890                        }
1891                    };
1892
1893                    let inner_mir = optimized_mir.into_inner();
1894                    if inner_mir.as_const().is_some() && insert_plan.returning.is_empty() {
1895                        let session = self.session.as_mut().expect("SessionClient invariant");
1896                        let result = Coordinator::insert_constant(
1897                            &catalog,
1898                            session,
1899                            insert_plan.id,
1900                            inner_mir,
1901                        );
1902
1903                        return Ok(Some(result?));
1904                    }
1905                }
1906
1907                let desc_arity = match catalog.try_get_entry(&insert_plan.id) {
1908                    Some(table) => {
1909                        let desc = table
1910                            .relation_desc_latest()
1911                            .ok_or_else(|| AdapterError::Internal("table has no desc".into()))?;
1912                        desc.arity()
1913                    }
1914                    None => {
1915                        return Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
1916                            kind: mz_catalog::memory::error::ErrorKind::Sql(
1917                                mz_sql::catalog::CatalogError::UnknownItem(
1918                                    insert_plan.id.to_string(),
1919                                ),
1920                            ),
1921                        }));
1922                    }
1923                };
1924
1925                let finishing = RowSetFinishing {
1926                    order_by: vec![],
1927                    limit: None,
1928                    offset: 0,
1929                    project: (0..desc_arity).collect(),
1930                };
1931
1932                ReadThenWritePlan {
1933                    id: insert_plan.id,
1934                    selection: insert_plan.values,
1935                    finishing,
1936                    assignments: BTreeMap::new(),
1937                    kind: MutationKind::Insert,
1938                    returning: insert_plan.returning,
1939                }
1940            }
1941            _ => {
1942                return Err(AdapterError::Internal(
1943                    "unexpected plan type for mutation".into(),
1944                ));
1945            }
1946        };
1947
1948        // The syntactic predicate for "reads persisted state", see the module
1949        // docs on `frontend_read_then_write`. Inside a transaction, only a write
1950        // that reads nothing can run on this path.
1951        //
1952        // The AST gate above is not enough to establish this. It admits
1953        // INSERTs whose source is constant in the AST, and such a statement can
1954        // still plan to a selection with `Get` nodes, because SQL-implemented
1955        // builtins (`pg_get_viewdef`, `text` to `reg*` casts, ...) read system
1956        // relations. So decide on the planned selection, and do it before we
1957        // execute a dataflow for a statement we would then refuse.
1958        {
1959            let session = self.session.as_ref().expect("SessionClient invariant");
1960            let in_transaction = session
1961                .transaction()
1962                .may_share_transaction_with_other_statements();
1963            let depends_on = rtw_plan.selection.depends_on();
1964            if in_transaction && !depends_on.is_empty() {
1965                // Report the reasons that hold wherever the statement runs
1966                // before the one that holds only here. A statement carrying
1967                // `mz_now`, or reading a system table, never works anywhere.
1968                // Answering with the transaction state names the one condition
1969                // the caller could remove, which tells them to retry outside a
1970                // transaction and get the same refusal again.
1971                if contains_mz_now(&rtw_plan) {
1972                    return Err(AdapterError::Unsupported(
1973                        "calls to mz_now in write statements",
1974                    ));
1975                }
1976                validate_selection_dependencies(&catalog, &depends_on)?;
1977                return Err(prohibited_in_transaction(&stmt));
1978            }
1979        }
1980
1981        let session = self.session.as_mut().expect("SessionClient invariant");
1982        self.peek_client
1983            .frontend_read_then_write(
1984                session,
1985                rtw_plan,
1986                target_cluster,
1987                &catalog,
1988                logging.id(),
1989                attempt_state,
1990            )
1991            .await
1992            .map(Some)
1993    }
1994}
1995
1996/// Whether a statement is one the frontend read-then-write path sequences.
1997///
1998/// These are the statement kinds that plan to a `ReadThenWrite` or an `Insert`.
1999/// Note that not every one of them ends up on the OCC path: an `INSERT` whose
2000/// source folds to a constant is dispatched as a blind write instead.
2001fn is_read_then_write_statement(stmt: &Statement<Raw>) -> bool {
2002    matches!(
2003        stmt,
2004        Statement::Delete(_) | Statement::Update(_) | Statement::Insert(_)
2005    )
2006}
2007
2008/// Builds the error for DML that cannot run in a transaction block, mirroring
2009/// the coordinator's redaction in `handle_execute`: statements that can carry
2010/// sensitive literals are redacted because the error message is persisted in
2011/// `mz_statement_execution_history`.
2012fn prohibited_in_transaction(stmt: &Statement<Raw>) -> AdapterError {
2013    let op = if StatementKind::from(stmt).is_sensitive() {
2014        stmt.to_ast_string_redacted()
2015    } else {
2016        stmt.to_string()
2017    };
2018    AdapterError::OperationProhibitsTransaction(op)
2019}
2020
2021impl Drop for SessionClient {
2022    fn drop(&mut self) {
2023        // We may not have a session if this client was dropped while awaiting
2024        // a response. In this case, it is the coordinator's responsibility to
2025        // terminate the session.
2026        if let Some(session) = self.session.take() {
2027            // We may not have a connection to the Coordinator if the session was
2028            // prematurely terminated, for example due to a timeout.
2029            if let Some(inner) = &self.inner {
2030                inner.send(Command::Terminate {
2031                    conn_id: session.conn_id().clone(),
2032                    tx: None,
2033                })
2034            }
2035        }
2036    }
2037}
2038
2039/// Renders SQL for statement arrival logging: parsed and displayed with its
2040/// literals redacted, which is the same redaction the statement log applies.
2041/// When the text does not parse or exceeds the statement batch size limit, a
2042/// placeholder with the byte length is returned. Raw text is never returned,
2043/// so a statement that crashes the parser is not captured, an accepted
2044/// limitation.
2045pub fn redact_sql_for_logging(sql: &str) -> String {
2046    match mz_sql_parser::parser::parse_statements_with_limit(sql) {
2047        Ok(Ok(stmts)) => stmts
2048            .into_iter()
2049            .map(|stmt| stmt.ast.to_ast_string_redacted())
2050            .join("; "),
2051        Ok(Err(_)) => format!("<unparseable ({} bytes)>", sql.len()),
2052        Err(_) => format!("<too large ({} bytes)>", sql.len()),
2053    }
2054}
2055
2056#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
2057pub enum TimeoutType {
2058    IdleInTransactionSession(TransactionId),
2059}
2060
2061impl Display for TimeoutType {
2062    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2063        match self {
2064            TimeoutType::IdleInTransactionSession(txn_id) => {
2065                writeln!(f, "Idle in transaction session for transaction '{txn_id}'")
2066            }
2067        }
2068    }
2069}
2070
2071impl From<TimeoutType> for AdapterError {
2072    fn from(timeout: TimeoutType) -> Self {
2073        match timeout {
2074            TimeoutType::IdleInTransactionSession(_) => {
2075                AdapterError::IdleInTransactionSessionTimeout
2076            }
2077        }
2078    }
2079}
2080
2081struct Timeout {
2082    tx: mpsc::UnboundedSender<TimeoutType>,
2083    rx: mpsc::UnboundedReceiver<TimeoutType>,
2084    active_timeouts: BTreeMap<TimeoutType, AbortOnDropHandle<()>>,
2085}
2086
2087impl Timeout {
2088    fn new() -> Self {
2089        let (tx, rx) = mpsc::unbounded_channel();
2090        Timeout {
2091            tx,
2092            rx,
2093            active_timeouts: BTreeMap::new(),
2094        }
2095    }
2096
2097    /// # Cancel safety
2098    ///
2099    /// This method is cancel safe. If `recv` is used as the event in a
2100    /// `tokio::select!` statement and some other branch
2101    /// completes first, it is guaranteed that no messages were received on this
2102    /// channel.
2103    ///
2104    /// <https://docs.rs/tokio/latest/tokio/sync/mpsc/struct.UnboundedReceiver.html#cancel-safety>
2105    async fn recv(&mut self) -> Option<TimeoutType> {
2106        self.rx.recv().await
2107    }
2108
2109    fn add_timeout(&mut self, timeout: TimeoutType, duration: Duration) {
2110        let tx = self.tx.clone();
2111        let timeout_key = timeout.clone();
2112        let handle = mz_ore::task::spawn(|| format!("{timeout_key}"), async move {
2113            tokio::time::sleep(duration).await;
2114            let _ = tx.send(timeout);
2115        })
2116        .abort_on_drop();
2117        self.active_timeouts.insert(timeout_key, handle);
2118    }
2119
2120    fn remove_timeout(&mut self, timeout: &TimeoutType) {
2121        self.active_timeouts.remove(timeout);
2122
2123        // Remove the timeout from the rx queue if it exists.
2124        let mut timeouts = Vec::new();
2125        while let Ok(pending_timeout) = self.rx.try_recv() {
2126            if timeout != &pending_timeout {
2127                timeouts.push(pending_timeout);
2128            }
2129        }
2130        for pending_timeout in timeouts {
2131            self.tx.send(pending_timeout).expect("rx is in this struct");
2132        }
2133    }
2134}
2135
2136/// A wrapper around a Stream of PeekResponseUnary that records when it sees the
2137/// first row data in the given histogram. It also keeps track of whether we have already observed
2138/// the end of the underlying stream.
2139#[derive(Derivative)]
2140#[derivative(Debug)]
2141pub struct RecordFirstRowStream {
2142    /// The underlying stream of rows.
2143    #[derivative(Debug = "ignore")]
2144    pub rows: Box<dyn Stream<Item = PeekResponseUnary> + Unpin + Send + Sync>,
2145    /// The Instant when execution started.
2146    pub execute_started: Instant,
2147    /// The histogram where the time since `execute_started` will be recorded when we see the first
2148    /// row.
2149    pub time_to_first_row_seconds: Histogram,
2150    /// Whether we've seen any rows.
2151    pub saw_rows: bool,
2152    /// The Instant when we saw the first row.
2153    pub recorded_first_row_instant: Option<Instant>,
2154    /// Whether we have already observed the end of the underlying stream.
2155    pub no_more_rows: bool,
2156    /// Whether the first-to-last-byte metric has already been recorded for this stream.
2157    pub metric_recorded: bool,
2158}
2159
2160impl RecordFirstRowStream {
2161    /// Create a new [`RecordFirstRowStream`]
2162    pub fn new(
2163        rows: Box<dyn Stream<Item = PeekResponseUnary> + Unpin + Send + Sync>,
2164        execute_started: Instant,
2165        client: &SessionClient,
2166        instance_id: Option<ComputeInstanceId>,
2167        strategy: Option<StatementExecutionStrategy>,
2168    ) -> Self {
2169        let histogram = Self::histogram(client, instance_id, strategy);
2170        Self {
2171            rows,
2172            execute_started,
2173            time_to_first_row_seconds: histogram,
2174            saw_rows: false,
2175            recorded_first_row_instant: None,
2176            no_more_rows: false,
2177            metric_recorded: false,
2178        }
2179    }
2180
2181    fn histogram(
2182        client: &SessionClient,
2183        instance_id: Option<ComputeInstanceId>,
2184        strategy: Option<StatementExecutionStrategy>,
2185    ) -> Histogram {
2186        let session = client.session.as_ref().expect("session invariant");
2187        let isolation_level = *session.vars().transaction_isolation();
2188        let name_hint = ApplicationNameHint::from_str(session.application_name());
2189        let instance = match instance_id {
2190            Some(i) => Cow::Owned(i.to_string()),
2191            None => Cow::Borrowed("none"),
2192        };
2193        let strategy = match strategy {
2194            Some(s) => s.name(),
2195            None => "none",
2196        };
2197
2198        client
2199            .inner()
2200            .metrics()
2201            .time_to_first_row_seconds
2202            .with_label_values(&[
2203                instance.as_ref(),
2204                isolation_level.as_variant_str(),
2205                strategy,
2206                name_hint.as_str(),
2207            ])
2208    }
2209
2210    /// If you want to match [`RecordFirstRowStream`]'s logic but don't need
2211    /// a UnboundedReceiver, you can tell it when to record an observation.
2212    pub fn record(
2213        execute_started: Instant,
2214        client: &SessionClient,
2215        instance_id: Option<ComputeInstanceId>,
2216        strategy: Option<StatementExecutionStrategy>,
2217    ) {
2218        Self::histogram(client, instance_id, strategy)
2219            .observe(execute_started.elapsed().as_secs_f64());
2220    }
2221
2222    pub async fn recv(&mut self) -> Option<PeekResponseUnary> {
2223        let msg = self.rows.next().await;
2224        if !self.saw_rows && matches!(msg, Some(PeekResponseUnary::Rows(_))) {
2225            self.saw_rows = true;
2226            self.time_to_first_row_seconds
2227                .observe(self.execute_started.elapsed().as_secs_f64());
2228            self.recorded_first_row_instant = Some(Instant::now());
2229        }
2230        if msg.is_none() {
2231            self.no_more_rows = true;
2232        }
2233        msg
2234    }
2235}