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