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