Skip to main content

mz_adapter/
session.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Per-connection configuration parameters and state.
11
12#![warn(missing_docs)]
13
14use std::collections::BTreeMap;
15use std::collections::btree_map::Entry;
16use std::future::Future;
17use std::mem;
18use std::net::IpAddr;
19use std::sync::Arc;
20
21use chrono::{DateTime, Utc};
22use derivative::Derivative;
23use mz_adapter_types::connection::ConnectionId;
24use mz_auth::AuthenticatorKind;
25use mz_auth::user::ExternalUserMetadata;
26use mz_build_info::{BuildInfo, DUMMY_BUILD_INFO};
27use mz_controller_types::ClusterId;
28use mz_ore::metrics::{MetricsFutureExt, MetricsRegistry};
29use mz_ore::now::{EpochMillis, NowFn};
30use mz_pgwire_common::Format;
31use mz_repr::role_id::RoleId;
32use mz_repr::user::InternalUserMetadata;
33use mz_repr::{CatalogItemId, Datum, Row, RowIterator, SqlScalarType, Timestamp};
34use mz_sql::ast::{AstInfo, Raw, Statement, TransactionAccessMode};
35use mz_sql::plan::{Params, PlanContext, QueryWhen, StatementDesc};
36use mz_sql::session::metadata::SessionMetadata;
37use mz_sql::session::user::{
38    INTERNAL_USER_NAME_TO_DEFAULT_CLUSTER, RoleMetadata, SYSTEM_USER, User,
39};
40use mz_sql::session::vars::IsolationLevel;
41pub use mz_sql::session::vars::{
42    DEFAULT_DATABASE_NAME, EndTransactionAction, SERVER_MAJOR_VERSION, SERVER_MINOR_VERSION,
43    SERVER_PATCH_VERSION, SessionVars, Var,
44};
45use mz_sql_parser::ast::TransactionIsolationLevel;
46use mz_storage_client::client::TableData;
47use mz_storage_types::sources::Timeline;
48use qcell::{QCell, QCellOwner};
49use timely::progress::Timestamp as _;
50use tokio::sync::mpsc::{self, UnboundedSender};
51use tokio::sync::watch;
52use uuid::Uuid;
53
54use crate::AdapterNotice;
55use crate::ExecutionTime;
56use crate::catalog::CatalogState;
57use crate::client::RecordFirstRowStream;
58use crate::coord::ExplainContext;
59use crate::coord::appends::BuiltinTableAppendNotify;
60use crate::coord::in_memory_oracle::InMemoryTimestampOracle;
61use crate::coord::peek::PeekResponseUnary;
62use crate::coord::timestamp_selection::{TimestampContext, TimestampDetermination};
63use crate::error::AdapterError;
64use crate::metrics::{Metrics, SessionMetrics};
65use crate::notice::ExecutionTimeKind;
66use crate::statement_logging::PreparedStatementLoggingInfo;
67use mz_catalog::durable::Snapshot;
68
69const DUMMY_CONNECTION_ID: ConnectionId = ConnectionId::Static(0);
70
71/// A session holds per-connection state.
72#[derive(Derivative)]
73#[derivative(Debug)]
74pub struct Session {
75    conn_id: ConnectionId,
76    /// A globally unique identifier for the session. Not to be confused
77    /// with `conn_id`, which may be reused.
78    uuid: Uuid,
79    prepared_statements: BTreeMap<String, PreparedStatement>,
80    portals: BTreeMap<String, Portal>,
81    transaction: TransactionStatus,
82    pcx: Option<PlanContext>,
83    metrics: SessionMetrics,
84    #[derivative(Debug = "ignore")]
85    builtin_updates: Option<BuiltinTableAppendNotify>,
86
87    /// The role metadata of the current session.
88    ///
89    /// Invariant: role_metadata must be `Some` after the user has
90    /// successfully connected to and authenticated with Materialize.
91    ///
92    /// Prefer using this value over [`SessionConfig::user`].
93    //
94    // It would be better for this not to be an Option, but the
95    // `Session` is initialized before the user has connected to
96    // Materialize and is able to look up the `RoleMetadata`. The `Session`
97    // is also used to return an error when no role exists and
98    // therefore there is no valid `RoleMetadata`.
99    role_metadata: Option<RoleMetadata>,
100    client_ip: Option<IpAddr>,
101    vars: SessionVars,
102    notices_tx: mpsc::UnboundedSender<AdapterNotice>,
103    notices_rx: mpsc::UnboundedReceiver<AdapterNotice>,
104    next_transaction_id: TransactionId,
105    secret_key: u32,
106    external_metadata_rx: Option<watch::Receiver<ExternalUserMetadata>>,
107    // Token allowing us to access `Arc<QCell<StatementLogging>>`
108    // metadata. We want these to be reference-counted, because the same
109    // statement might be referenced from multiple portals simultaneously.
110    //
111    // However, they can't be `Rc<RefCell<StatementLogging>>`, because
112    // the `Session` is sent around to different threads.
113    //
114    // On the other hand, they don't need to be
115    // `Arc<Mutex<StatementLogging>>`, because they will always be
116    // accessed from the same thread that the `Session` is currently
117    // on. We express this by gating access with this token.
118    #[derivative(Debug = "ignore")]
119    qcell_owner: QCellOwner,
120    session_oracles: BTreeMap<Timeline, InMemoryTimestampOracle>,
121    /// Incremented when session state that is relevant to prepared statement planning changes.
122    /// Currently, only changes to `portals` are tracked. Changes to `prepared_statements` don't
123    /// need to be tracked, because prepared statements can't depend on other prepared statements.
124    /// TODO: We might want to track changes also to session variables.
125    /// (`Catalog::transient_revision` similarly tracks changes on the catalog side.)
126    state_revision: u64,
127}
128
129impl SessionMetadata for Session {
130    fn conn_id(&self) -> &ConnectionId {
131        &self.conn_id
132    }
133
134    fn client_ip(&self) -> Option<&IpAddr> {
135        self.client_ip.as_ref()
136    }
137
138    fn pcx(&self) -> &PlanContext {
139        &self
140            .transaction()
141            .inner()
142            .expect("no active transaction")
143            .pcx
144    }
145
146    fn role_metadata(&self) -> &RoleMetadata {
147        self.role_metadata
148            .as_ref()
149            .expect("role_metadata invariant violated")
150    }
151
152    fn vars(&self) -> &SessionVars {
153        &self.vars
154    }
155}
156
157/// Data structure suitable for passing to other threads that need access to some common Session
158/// properties.
159#[derive(Debug)]
160pub struct SessionMeta {
161    conn_id: ConnectionId,
162    client_ip: Option<IpAddr>,
163    pcx: PlanContext,
164    role_metadata: RoleMetadata,
165    vars: SessionVars,
166}
167
168impl SessionMetadata for SessionMeta {
169    fn vars(&self) -> &SessionVars {
170        &self.vars
171    }
172
173    fn conn_id(&self) -> &ConnectionId {
174        &self.conn_id
175    }
176
177    fn client_ip(&self) -> Option<&IpAddr> {
178        self.client_ip.as_ref()
179    }
180
181    fn pcx(&self) -> &PlanContext {
182        &self.pcx
183    }
184
185    fn role_metadata(&self) -> &RoleMetadata {
186        &self.role_metadata
187    }
188}
189
190/// Configures a new [`Session`].
191#[derive(Debug, Clone)]
192pub struct SessionConfig {
193    /// The connection ID for the session.
194    ///
195    /// May be reused after the session terminates.
196    pub conn_id: ConnectionId,
197    /// A universally unique identifier for the session, across all processes,
198    /// region, and all time.
199    ///
200    /// Must not be reused, even after the session terminates.
201    pub uuid: Uuid,
202    /// The peer address of the client
203    pub client_ip: Option<IpAddr>,
204    /// The name of the user associated with the session.
205    pub user: String,
206    /// An optional receiver that the session will periodically check for
207    /// updates to a user's external metadata.
208    pub external_metadata_rx: Option<watch::Receiver<ExternalUserMetadata>>,
209    /// Helm chart version
210    pub helm_chart_version: Option<String>,
211    /// The authenticator that authenticated this user, if any.
212    pub authenticator_kind: AuthenticatorKind,
213    /// Groups from JWT claims for OIDC group-to-role sync.
214    pub groups: Option<Vec<String>>,
215}
216
217impl Session {
218    /// Creates a new session for the specified connection ID.
219    pub(crate) fn new(
220        build_info: &'static BuildInfo,
221        config: SessionConfig,
222        metrics: SessionMetrics,
223    ) -> Session {
224        assert_ne!(config.conn_id, DUMMY_CONNECTION_ID);
225        Self::new_internal(build_info, config, metrics)
226    }
227
228    /// Returns a reference-less collection of data usable by other tasks that don't have ownership
229    /// of the Session.
230    pub fn meta(&self) -> SessionMeta {
231        SessionMeta {
232            conn_id: self.conn_id().clone(),
233            client_ip: self.client_ip().copied(),
234            pcx: self.pcx().clone(),
235            role_metadata: self.role_metadata().clone(),
236            vars: self.vars.clone(),
237        }
238
239        // TODO: soft_assert that these are the same as Session.
240    }
241
242    /// Creates new statement logging metadata for a one-off
243    /// statement.
244    // Normally, such logging information would be created as part of
245    // allocating a new prepared statement, and a refcounted handle
246    // would be copied from that prepared statement to portals during
247    // binding. However, we also support (via `Command::declare`)
248    // binding a statement directly to a portal without creating an
249    // intermediate prepared statement. Thus, for those cases, a
250    // mechanism for generating the logging metadata directly is needed.
251    pub(crate) fn mint_logging<A: AstInfo>(
252        &self,
253        raw_sql: String,
254        stmt: Option<&Statement<A>>,
255        now: EpochMillis,
256    ) -> Arc<QCell<PreparedStatementLoggingInfo>> {
257        Arc::new(QCell::new(
258            &self.qcell_owner,
259            PreparedStatementLoggingInfo::still_to_log(
260                raw_sql,
261                stmt,
262                now,
263                "".to_string(),
264                self.uuid,
265                false,
266            ),
267        ))
268    }
269
270    pub(crate) fn qcell_ro<'a, T2: 'a>(&'a self, cell: &'a Arc<QCell<T2>>) -> &'a T2 {
271        self.qcell_owner.ro(&*cell)
272    }
273
274    pub(crate) fn qcell_rw<'a, T2: 'a>(&'a mut self, cell: &'a Arc<QCell<T2>>) -> &'a mut T2 {
275        self.qcell_owner.rw(&*cell)
276    }
277
278    /// Returns a unique ID for the session.
279    /// Not to be confused with `connection_id`, which can be reused.
280    pub fn uuid(&self) -> Uuid {
281        self.uuid
282    }
283
284    /// Creates a new dummy session.
285    ///
286    /// Dummy sessions are intended for use when executing queries on behalf of
287    /// the system itself, rather than on behalf of a user.
288    pub fn dummy() -> Session {
289        let registry = MetricsRegistry::new();
290        let metrics = Metrics::register_into(&registry);
291        let metrics = metrics.session_metrics();
292        let mut dummy = Self::new_internal(
293            &DUMMY_BUILD_INFO,
294            SessionConfig {
295                conn_id: DUMMY_CONNECTION_ID,
296                uuid: Uuid::new_v4(),
297                user: SYSTEM_USER.name.clone(),
298                client_ip: None,
299                external_metadata_rx: None,
300                helm_chart_version: None,
301                authenticator_kind: AuthenticatorKind::None,
302                groups: None,
303            },
304            metrics,
305        );
306        dummy.initialize_role_metadata(RoleId::User(0));
307        dummy
308    }
309
310    fn new_internal(
311        build_info: &'static BuildInfo,
312        SessionConfig {
313            conn_id,
314            uuid,
315            user,
316            client_ip,
317            mut external_metadata_rx,
318            helm_chart_version,
319            authenticator_kind,
320            groups,
321        }: SessionConfig,
322        metrics: SessionMetrics,
323    ) -> Session {
324        let (notices_tx, notices_rx) = mpsc::unbounded_channel();
325        let default_cluster = INTERNAL_USER_NAME_TO_DEFAULT_CLUSTER.get(&user);
326        let user = User {
327            name: user,
328            internal_metadata: None,
329            external_metadata: external_metadata_rx
330                .as_mut()
331                .map(|rx| rx.borrow_and_update().clone()),
332            authenticator_kind: Some(authenticator_kind),
333            groups,
334        };
335        let mut vars = SessionVars::new_unchecked(build_info, user, helm_chart_version);
336        if let Some(default_cluster) = default_cluster {
337            vars.set_cluster(default_cluster.clone());
338        }
339        Session {
340            conn_id,
341            uuid,
342            transaction: TransactionStatus::Default,
343            pcx: None,
344            metrics,
345            builtin_updates: None,
346            prepared_statements: BTreeMap::new(),
347            portals: BTreeMap::new(),
348            role_metadata: None,
349            client_ip,
350            vars,
351            notices_tx,
352            notices_rx,
353            next_transaction_id: 0,
354            secret_key: rand::random(),
355            external_metadata_rx,
356            qcell_owner: QCellOwner::new(),
357            session_oracles: BTreeMap::new(),
358            state_revision: 0,
359        }
360    }
361
362    /// Returns the secret key associated with the session.
363    pub fn secret_key(&self) -> u32 {
364        self.secret_key
365    }
366
367    fn new_pcx(&self, mut wall_time: DateTime<Utc>) -> PlanContext {
368        if let Some(mock_time) = self.vars().unsafe_new_transaction_wall_time() {
369            wall_time = *mock_time;
370        }
371        PlanContext::new(wall_time)
372    }
373
374    /// Starts an explicit transaction, or changes an implicit to an explicit
375    /// transaction.
376    pub fn start_transaction(
377        &mut self,
378        wall_time: DateTime<Utc>,
379        access: Option<TransactionAccessMode>,
380        isolation_level: Option<TransactionIsolationLevel>,
381    ) -> Result<(), AdapterError> {
382        // Check that current transaction state is compatible with new `access`
383        if let Some(txn) = self.transaction.inner() {
384            // `READ WRITE` prohibited if:
385            // - Currently in `READ ONLY`
386            // - Already performed a query
387            let read_write_prohibited = match txn.ops {
388                TransactionOps::Peeks { .. } | TransactionOps::Subscribe => {
389                    txn.access == Some(TransactionAccessMode::ReadOnly)
390                }
391                TransactionOps::None
392                | TransactionOps::Writes(_)
393                | TransactionOps::SingleStatement { .. }
394                | TransactionOps::DDL { .. } => false,
395            };
396
397            if read_write_prohibited && access == Some(TransactionAccessMode::ReadWrite) {
398                return Err(AdapterError::ReadWriteUnavailable);
399            }
400        }
401
402        match std::mem::take(&mut self.transaction) {
403            TransactionStatus::Default => {
404                let id = self.next_transaction_id;
405                self.next_transaction_id = self.next_transaction_id.wrapping_add(1);
406                self.transaction = TransactionStatus::InTransaction(Transaction {
407                    completion_owner: TransactionCompletionOwner::Coordinator,
408                    pcx: self.new_pcx(wall_time),
409                    ops: TransactionOps::None,
410                    pending_ddl_inspections: Vec::new(),
411                    contains_explain: false,
412                    access,
413                    id,
414                });
415            }
416            TransactionStatus::Started(mut txn)
417            | TransactionStatus::InTransactionImplicit(mut txn)
418            | TransactionStatus::InTransaction(mut txn) => {
419                if access.is_some() {
420                    txn.access = access;
421                }
422                self.transaction = TransactionStatus::InTransaction(txn);
423            }
424            TransactionStatus::Failed(_) => unreachable!(),
425        };
426
427        if let Some(isolation_level) = isolation_level {
428            self.vars
429                .set_local_transaction_isolation(isolation_level.into());
430        }
431
432        Ok(())
433    }
434
435    /// Starts either a single statement or implicit transaction based on the
436    /// number of statements, but only if no transaction has been started already.
437    pub fn start_transaction_implicit(&mut self, wall_time: DateTime<Utc>, stmts: usize) {
438        if let TransactionStatus::Default = self.transaction {
439            let id = self.next_transaction_id;
440            self.next_transaction_id = self.next_transaction_id.wrapping_add(1);
441            let txn = Transaction {
442                completion_owner: TransactionCompletionOwner::Session,
443                pcx: self.new_pcx(wall_time),
444                ops: TransactionOps::None,
445                pending_ddl_inspections: Vec::new(),
446                contains_explain: false,
447                access: None,
448                id,
449            };
450            match stmts {
451                1 => self.transaction = TransactionStatus::Started(txn),
452                n if n > 1 => self.transaction = TransactionStatus::InTransactionImplicit(txn),
453                _ => {}
454            }
455        }
456    }
457
458    /// Starts a single statement transaction, but only if no transaction has been started already.
459    pub fn start_transaction_single_stmt(&mut self, wall_time: DateTime<Utc>) {
460        self.start_transaction_implicit(wall_time, 1);
461    }
462
463    /// Clears a transaction, setting its state to Default and destroying all
464    /// portals. Returned are:
465    /// - sinks that were started in this transaction and need to be dropped
466    /// - the cleared transaction so its operations can be handled
467    ///
468    /// The [Postgres protocol docs](https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY) specify:
469    /// > a named portal object lasts till the end of the current transaction
470    /// and
471    /// > An unnamed portal is destroyed at the end of the transaction
472    #[must_use]
473    pub fn clear_transaction(&mut self) -> TransactionStatus {
474        self.portals.clear();
475        self.pcx = None;
476        self.state_revision += 1;
477        mem::take(&mut self.transaction)
478    }
479
480    /// Transfers transaction completion to the coordinator. Call before handing it any
481    /// transaction-scoped state, including on paths that can fail after installing resources.
482    pub(crate) fn require_coordinator_completion(&mut self) {
483        if let Some(txn) = self.transaction.inner_mut() {
484            txn.completion_owner = TransactionCompletionOwner::Coordinator;
485        }
486    }
487
488    /// Returns the obligations for a session-owned read-only completion.
489    pub(crate) fn read_only_completion(
490        &self,
491        mut action: EndTransactionAction,
492    ) -> Option<ReadOnlyCompletion> {
493        let txn = self.transaction.inner()?;
494        if txn.completion_owner != TransactionCompletionOwner::Session {
495            return None;
496        }
497        let timestamp = match &txn.ops {
498            TransactionOps::None => None,
499            TransactionOps::Peeks {
500                determination,
501                requires_linearization,
502                ..
503            } => matches!(requires_linearization, RequireLinearization::Required)
504                .then(|| determination.timestamp_context.clone()),
505            TransactionOps::Writes(_)
506            | TransactionOps::Subscribe
507            | TransactionOps::SingleStatement { .. }
508            | TransactionOps::DDL { .. } => return None,
509        };
510        if matches!(self.transaction, TransactionStatus::Failed(_)) {
511            action = EndTransactionAction::Rollback;
512        }
513        Some(ReadOnlyCompletion {
514            action,
515            timestamp: if action == EndTransactionAction::Commit {
516                timestamp
517            } else {
518                None
519            },
520        })
521    }
522
523    /// Marks the current transaction as failed.
524    pub fn fail_transaction(mut self) -> Self {
525        match self.transaction {
526            TransactionStatus::Default => unreachable!(),
527            TransactionStatus::Started(txn)
528            | TransactionStatus::InTransactionImplicit(txn)
529            | TransactionStatus::InTransaction(txn) => {
530                self.transaction = TransactionStatus::Failed(txn);
531            }
532            TransactionStatus::Failed(_) => {}
533        };
534        self
535    }
536
537    /// Returns the current transaction status.
538    pub fn transaction(&self) -> &TransactionStatus {
539        &self.transaction
540    }
541
542    /// Returns the current transaction status.
543    pub fn transaction_mut(&mut self) -> &mut TransactionStatus {
544        &mut self.transaction
545    }
546
547    /// Returns the session's transaction code.
548    pub fn transaction_code(&self) -> TransactionCode {
549        self.transaction().into()
550    }
551
552    /// Retains a SHOW CREATE result for validation if this transaction commits DDL.
553    pub(crate) fn retain_catalog_result(
554        &mut self,
555        command: Arc<crate::catalog::command::CatalogResultCommand>,
556    ) {
557        self.retain_ddl_inspection(DdlInspection::CatalogResult(command));
558    }
559
560    fn retain_ddl_inspection(&mut self, inspection: DdlInspection) {
561        if !self.transaction().can_retain_ddl_inspections() {
562            return;
563        }
564        let txn = self
565            .transaction_mut()
566            .inner_mut()
567            .expect("active transaction");
568        match &mut txn.ops {
569            TransactionOps::DDL { commands, .. } => commands.push(inspection.into_command()),
570            _ => txn.pending_ddl_inspections.push(inspection),
571        }
572    }
573
574    /// Retains a successful statement description for validation at DDL commit.
575    pub(crate) fn retain_description(
576        &mut self,
577        statement: mz_sql::ast::Statement<mz_sql::ast::Raw>,
578        param_types: Vec<Option<mz_repr::SqlScalarType>>,
579        expected: mz_sql::plan::StatementDesc,
580    ) {
581        if !self.transaction().can_retain_ddl_inspections()
582            || (expected.relation_desc.is_none() && expected.param_types.is_empty())
583        {
584            return;
585        }
586        // DECLARE delegates description to its inner statement. EXECUTE and FETCH
587        // copy session-owned schemas without describing a catalog query.
588        let mut described = &statement;
589        while let mz_sql::ast::Statement::Declare(declare) = described {
590            described = &declare.stmt;
591        }
592        if matches!(
593            described,
594            mz_sql::ast::Statement::Execute(_) | mz_sql::ast::Statement::Fetch(_)
595        ) {
596            return;
597        }
598        let command = crate::catalog::command::DescriptionCommand::new(
599            statement,
600            param_types,
601            self.meta(),
602            expected,
603        );
604        self.retain_ddl_inspection(DdlInspection::Description(Arc::new(command)));
605    }
606
607    /// Adds operations, rejecting incompatible combinations of reads and writes.
608    pub fn add_transaction_ops(&mut self, add_ops: TransactionOps) -> Result<(), AdapterError> {
609        self.transaction.add_ops(add_ops)
610    }
611
612    /// Returns a channel on which to send notices to the session.
613    pub fn retain_notice_transmitter(&self) -> UnboundedSender<AdapterNotice> {
614        self.notices_tx.clone()
615    }
616
617    /// Adds a notice to the session.
618    pub fn add_notice(&self, notice: AdapterNotice) {
619        self.add_notices([notice])
620    }
621
622    /// Adds multiple notices to the session.
623    pub fn add_notices(&self, notices: impl IntoIterator<Item = AdapterNotice>) {
624        for notice in notices {
625            let _ = self.notices_tx.send(notice);
626        }
627    }
628
629    /// Awaits a possible notice.
630    ///
631    /// This method is cancel safe.
632    pub async fn recv_notice(&mut self) -> AdapterNotice {
633        // This method is cancel safe because recv is cancel safe.
634        loop {
635            let notice = self
636                .notices_rx
637                .recv()
638                .await
639                .expect("Session also holds a sender, so recv won't ever return None");
640            match self.notice_filter(notice) {
641                Some(notice) => return notice,
642                None => continue,
643            }
644        }
645    }
646
647    /// Returns a draining iterator over the notices attached to the session.
648    pub fn drain_notices(&mut self) -> Vec<AdapterNotice> {
649        let mut notices = Vec::new();
650        while let Ok(notice) = self.notices_rx.try_recv() {
651            if let Some(notice) = self.notice_filter(notice) {
652                notices.push(notice);
653            }
654        }
655        notices
656    }
657
658    /// Builds a timing notice if enabled and admitted by `client_min_messages`.
659    pub fn execution_time_notice(&self, time: ExecutionTime) -> Option<AdapterNotice> {
660        if !self.vars.emit_execution_time_notice() {
661            return None;
662        }
663        self.notice_filter(AdapterNotice::ExecutionTime(time))
664    }
665
666    /// Returns `time` with the kind `staged` if the current transaction holds work for its commit,
667    /// and `time` unchanged otherwise.
668    ///
669    /// Must be called before the statement settles, because settling can commit that work. A
670    /// statement that follows such work in the same transaction is marked too.
671    pub fn maybe_mark_staged(&self, time: ExecutionTime) -> ExecutionTime {
672        let staged = self
673            .transaction()
674            .inner()
675            .is_some_and(|txn| txn.ops.stages_work());
676        if staged {
677            ExecutionTime {
678                kind: ExecutionTimeKind::Staged,
679                ..time
680            }
681        } else {
682            time
683        }
684    }
685
686    /// Returns Some if the notice should be reported, otherwise None.
687    fn notice_filter(&self, notice: AdapterNotice) -> Option<AdapterNotice> {
688        // Filter out low threshold severity.
689        let minimum_client_severity = self.vars.client_min_messages();
690        let sev = notice.severity();
691        if !minimum_client_severity.should_output_to_client(&sev) {
692            return None;
693        }
694        // Filter out notices for other clusters.
695        if let AdapterNotice::ClusterReplicaStatusChanged { cluster, .. } = &notice {
696            if cluster != self.vars.cluster() {
697                return None;
698            }
699        }
700        Some(notice)
701    }
702
703    /// Sets the transaction ops to `TransactionOps::None`. Must only be used after
704    /// verifying that no transaction anomalies will occur if cleared.
705    pub fn clear_transaction_ops(&mut self) {
706        if let Some(txn) = self.transaction.inner_mut() {
707            txn.ops = TransactionOps::None;
708        }
709    }
710
711    /// Returns the transaction's read timestamp determination, if set.
712    ///
713    /// Returns `None` if there is no active transaction, or if the active
714    /// transaction is not a read transaction.
715    pub fn get_transaction_timestamp_determination(&self) -> Option<TimestampDetermination> {
716        match self.transaction.inner() {
717            Some(Transaction {
718                pcx: _,
719                ops: TransactionOps::Peeks { determination, .. },
720                access: _,
721                id: _,
722                pending_ddl_inspections: _,
723                contains_explain: _,
724                completion_owner: _,
725            }) => Some(determination.clone()),
726            _ => None,
727        }
728    }
729
730    /// Whether this session has a timestamp for a read transaction.
731    pub fn contains_read_timestamp(&self) -> bool {
732        matches!(
733            self.transaction.inner(),
734            Some(Transaction {
735                pcx: _,
736                ops: TransactionOps::Peeks {
737                    determination: TimestampDetermination {
738                        timestamp_context: TimestampContext::TimelineTimestamp { .. },
739                        ..
740                    },
741                    ..
742                },
743                access: _,
744                id: _,
745                pending_ddl_inspections: _,
746                contains_explain: _,
747                completion_owner: _,
748            })
749        )
750    }
751
752    /// Registers the prepared statement under `name`.
753    pub fn set_prepared_statement(
754        &mut self,
755        name: String,
756        stmt: Option<Statement<Raw>>,
757        raw_sql: String,
758        desc: StatementDesc,
759        state_revision: StateRevision,
760        now: EpochMillis,
761    ) {
762        let logging = PreparedStatementLoggingInfo::still_to_log(
763            raw_sql,
764            stmt.as_ref(),
765            now,
766            name.clone(),
767            self.uuid,
768            false,
769        );
770        let statement = PreparedStatement {
771            stmt,
772            desc,
773            state_revision,
774            logging: Arc::new(QCell::new(&self.qcell_owner, logging)),
775        };
776        self.prepared_statements.insert(name, statement);
777    }
778
779    /// Removes the prepared statement associated with `name`.
780    ///
781    /// Returns whether a statement previously existed.
782    pub fn remove_prepared_statement(&mut self, name: &str) -> bool {
783        self.prepared_statements.remove(name).is_some()
784    }
785
786    /// Removes all prepared statements.
787    pub fn remove_all_prepared_statements(&mut self) {
788        self.prepared_statements.clear();
789    }
790
791    /// Retrieves the prepared statement associated with `name`.
792    ///
793    /// This is unverified and could be incorrect if the underlying catalog has
794    /// changed.
795    pub fn get_prepared_statement_unverified(&self, name: &str) -> Option<&PreparedStatement> {
796        self.prepared_statements.get(name)
797    }
798
799    /// Retrieves the prepared statement associated with `name`.
800    ///
801    /// This is unverified and could be incorrect if the underlying catalog has
802    /// changed.
803    pub fn get_prepared_statement_mut_unverified(
804        &mut self,
805        name: &str,
806    ) -> Option<&mut PreparedStatement> {
807        self.prepared_statements.get_mut(name)
808    }
809
810    /// Returns the prepared statements for the session.
811    pub fn prepared_statements(&self) -> &BTreeMap<String, PreparedStatement> {
812        &self.prepared_statements
813    }
814
815    /// Returns the portals for the session.
816    pub fn portals(&self) -> &BTreeMap<String, Portal> {
817        &self.portals
818    }
819
820    /// Binds the specified portal to the specified prepared statement.
821    ///
822    /// If the prepared statement contains parameters, the values and types of
823    /// those parameters must be provided in `params`. It is the caller's
824    /// responsibility to ensure that the correct number of parameters is
825    /// provided.
826    ///
827    /// The `results_formats` parameter sets the desired format of the results,
828    /// and is stored on the portal.
829    pub fn set_portal(
830        &mut self,
831        portal_name: String,
832        desc: StatementDesc,
833        stmt: Option<Statement<Raw>>,
834        logging: Arc<QCell<PreparedStatementLoggingInfo>>,
835        params: Vec<(Datum, SqlScalarType)>,
836        result_formats: Vec<Format>,
837        state_revision: StateRevision,
838    ) -> Result<(), AdapterError> {
839        // The empty portal can be silently replaced.
840        if !portal_name.is_empty() && self.portals.contains_key(&portal_name) {
841            return Err(AdapterError::DuplicateCursor(portal_name));
842        }
843        self.state_revision += 1;
844        let param_types = desc.param_types.clone();
845        self.portals.insert(
846            portal_name,
847            Portal {
848                stmt: stmt.map(Arc::new),
849                desc,
850                state_revision,
851                parameters: Params {
852                    datums: Row::pack(params.iter().map(|(d, _t)| d)),
853                    execute_types: params.into_iter().map(|(_d, t)| t).collect(),
854                    expected_types: param_types,
855                },
856                result_formats,
857                state: PortalState::NotStarted,
858                logging,
859                lifecycle_timestamps: None,
860            },
861        );
862        Ok(())
863    }
864
865    /// Removes the specified portal.
866    ///
867    /// If there is no such portal, this method does nothing. Returns whether that portal existed.
868    pub fn remove_portal(&mut self, portal_name: &str) -> bool {
869        self.state_revision += 1;
870        self.portals.remove(portal_name).is_some()
871    }
872
873    /// Retrieves a reference to the specified portal.
874    ///
875    /// If there is no such portal, returns `None`.
876    pub fn get_portal_unverified(&self, portal_name: &str) -> Option<&Portal> {
877        self.portals.get(portal_name)
878    }
879
880    /// Retrieves a mutable reference to the specified portal.
881    ///
882    /// If there is no such portal, returns `None`.
883    ///
884    /// Note: When using the returned `PortalRefMut`, there is no need to increment
885    /// `Session::state_revision`, because the portal's meaning is not changed.
886    pub fn get_portal_unverified_mut(&mut self, portal_name: &str) -> Option<PortalRefMut<'_>> {
887        self.portals.get_mut(portal_name).map(|p| PortalRefMut {
888            stmt: &p.stmt,
889            desc: &p.desc,
890            state_revision: &mut p.state_revision,
891            parameters: &mut p.parameters,
892            result_formats: &mut p.result_formats,
893            logging: &mut p.logging,
894            state: &mut p.state,
895            lifecycle_timestamps: &mut p.lifecycle_timestamps,
896        })
897    }
898
899    /// Creates and installs a new portal.
900    pub fn create_new_portal(
901        &mut self,
902        stmt: Option<Statement<Raw>>,
903        logging: Arc<QCell<PreparedStatementLoggingInfo>>,
904        desc: StatementDesc,
905        parameters: Params,
906        result_formats: Vec<Format>,
907        state_revision: StateRevision,
908    ) -> Result<String, AdapterError> {
909        self.state_revision += 1;
910
911        // See: https://github.com/postgres/postgres/blob/84f5c2908dad81e8622b0406beea580e40bb03ac/src/backend/utils/mmgr/portalmem.c#L234
912        for i in 0usize.. {
913            let name = format!("<unnamed portal {}>", i);
914            match self.portals.entry(name.clone()) {
915                Entry::Occupied(_) => continue,
916                Entry::Vacant(entry) => {
917                    entry.insert(Portal {
918                        stmt: stmt.map(Arc::new),
919                        desc,
920                        state_revision,
921                        parameters,
922                        result_formats,
923                        state: PortalState::NotStarted,
924                        logging,
925                        lifecycle_timestamps: None,
926                    });
927                    return Ok(name);
928                }
929            }
930        }
931
932        coord_bail!("unable to create a new portal");
933    }
934
935    /// Resets the session to its initial state.
936    ///
937    /// Returns the parameters changed by [`SessionVars::reset_all`].
938    pub fn reset(&mut self) -> BTreeMap<&'static str, String> {
939        let _ = self.clear_transaction();
940        self.prepared_statements.clear();
941        self.vars.reset_all()
942    }
943
944    /// Returns the [application_name] that created this session.
945    ///
946    /// [application_name]: (https://www.postgresql.org/docs/current/runtime-config-logging.html#GUC-APPLICATION-NAME)
947    pub fn application_name(&self) -> &str {
948        self.vars.application_name()
949    }
950
951    /// Returns a reference to the variables in this session.
952    pub fn vars(&self) -> &SessionVars {
953        &self.vars
954    }
955
956    /// Returns a mutable reference to the variables in this session.
957    pub fn vars_mut(&mut self) -> &mut SessionVars {
958        &mut self.vars
959    }
960
961    /// Drains any external metadata updates and applies the changes from the latest update.
962    pub fn apply_external_metadata_updates(&mut self) {
963        // If no sender is registered then there isn't anything to do.
964        let Some(rx) = &mut self.external_metadata_rx else {
965            return;
966        };
967
968        // If the value hasn't changed then return.
969        if !rx.has_changed().unwrap_or(false) {
970            return;
971        }
972
973        // Update our metadata! Note the short critical section (just a clone) to avoid blocking
974        // the sending side of this watch channel.
975        let metadata = rx.borrow_and_update().clone();
976        self.vars.set_external_user_metadata(metadata);
977    }
978
979    /// Applies the internal user metadata to the session.
980    pub fn apply_internal_user_metadata(&mut self, metadata: InternalUserMetadata) {
981        self.vars.set_internal_user_metadata(metadata);
982    }
983
984    /// Initializes the session's role metadata.
985    pub fn initialize_role_metadata(&mut self, role_id: RoleId) {
986        self.role_metadata = Some(RoleMetadata::new(role_id));
987    }
988
989    /// Ensures that a timestamp oracle exists for `timeline` and returns a mutable reference to
990    /// the timestamp oracle.
991    pub fn ensure_timestamp_oracle(&mut self, timeline: Timeline) -> &mut InMemoryTimestampOracle {
992        self.session_oracles.entry(timeline).or_insert_with(|| {
993            InMemoryTimestampOracle::new(Timestamp::minimum(), NowFn::from(Timestamp::minimum))
994        })
995    }
996
997    /// Ensures that a timestamp oracle exists for reads and writes from/to a local input and
998    /// returns a mutable reference to the timestamp oracle.
999    pub fn ensure_local_timestamp_oracle(&mut self) -> &mut InMemoryTimestampOracle {
1000        self.ensure_timestamp_oracle(Timeline::EpochMilliseconds)
1001    }
1002
1003    /// Returns a reference to the timestamp oracle for `timeline`.
1004    pub fn get_timestamp_oracle(&self, timeline: &Timeline) -> Option<&InMemoryTimestampOracle> {
1005        self.session_oracles.get(timeline)
1006    }
1007
1008    /// If the current session is using the Strong Session Serializable isolation level advance the
1009    /// session local timestamp oracle to `write_ts`.
1010    pub fn apply_write(&mut self, timestamp: Timestamp) {
1011        if self.vars().transaction_isolation() == &IsolationLevel::StrongSessionSerializable {
1012            self.ensure_local_timestamp_oracle().apply_write(timestamp);
1013        }
1014    }
1015
1016    /// Returns the [`SessionMetrics`] instance associated with this [`Session`].
1017    pub fn metrics(&self) -> &SessionMetrics {
1018        &self.metrics
1019    }
1020
1021    /// Sets the `BuiltinTableAppendNotify` for this session.
1022    pub fn set_builtin_table_updates(&mut self, fut: BuiltinTableAppendNotify) {
1023        let prev = self.builtin_updates.replace(fut);
1024        mz_ore::soft_assert_or_log!(prev.is_none(), "replacing old builtin table notify");
1025    }
1026
1027    /// Takes the stashed `BuiltinTableAppendNotify`, if one exists, and returns a [`Future`] that
1028    /// waits for the writes to complete.
1029    pub fn clear_builtin_table_updates(&mut self) -> Option<impl Future<Output = ()> + 'static> {
1030        if let Some(fut) = self.builtin_updates.take() {
1031            // Record how long we blocked for, if we blocked at all.
1032            let histogram = self
1033                .metrics()
1034                .session_startup_table_writes_seconds()
1035                .clone();
1036            Some(async move {
1037                fut.wall_time().observe(histogram).await;
1038            })
1039        } else {
1040            None
1041        }
1042    }
1043
1044    /// Return the state_revision of the session, which can be used by dependent objects for knowing
1045    /// when to re-plan due to session state changes.
1046    pub fn state_revision(&self) -> u64 {
1047        self.state_revision
1048    }
1049}
1050
1051/// A prepared statement.
1052#[derive(Derivative, Clone)]
1053#[derivative(Debug)]
1054pub struct PreparedStatement {
1055    stmt: Option<Statement<Raw>>,
1056    desc: StatementDesc,
1057    /// The most recent state revision that has verified this statement.
1058    pub state_revision: StateRevision,
1059    #[derivative(Debug = "ignore")]
1060    logging: Arc<QCell<PreparedStatementLoggingInfo>>,
1061}
1062
1063impl PreparedStatement {
1064    /// Returns the AST associated with this prepared statement,
1065    /// if the prepared statement was not the empty query.
1066    pub fn stmt(&self) -> Option<&Statement<Raw>> {
1067        self.stmt.as_ref()
1068    }
1069
1070    /// Returns the description of the prepared statement.
1071    pub fn desc(&self) -> &StatementDesc {
1072        &self.desc
1073    }
1074
1075    /// Returns a handle to the metadata for statement logging.
1076    pub fn logging(&self) -> &Arc<QCell<PreparedStatementLoggingInfo>> {
1077        &self.logging
1078    }
1079}
1080
1081/// A portal represents the execution state of a running or runnable query.
1082#[derive(Derivative)]
1083#[derivative(Debug)]
1084pub struct Portal {
1085    /// The statement that is bound to this portal.
1086    pub stmt: Option<Arc<Statement<Raw>>>,
1087    /// The statement description.
1088    pub desc: StatementDesc,
1089    /// The most recent state revision that has verified this portal.
1090    pub state_revision: StateRevision,
1091    /// The bound values for the parameters in the prepared statement, if any.
1092    pub parameters: Params,
1093    /// The desired output format for each column in the result set.
1094    pub result_formats: Vec<Format>,
1095    /// A handle to metadata needed for statement logging.
1096    #[derivative(Debug = "ignore")]
1097    pub logging: Arc<QCell<PreparedStatementLoggingInfo>>,
1098    /// The execution state of the portal.
1099    #[derivative(Debug = "ignore")]
1100    pub state: PortalState,
1101    /// Statement lifecycle timestamps coming from `mz-pgwire`.
1102    pub lifecycle_timestamps: Option<LifecycleTimestamps>,
1103}
1104
1105/// A mutable reference to a portal, capturing its state and associated metadata. Importantly, it
1106/// does _not_ give _mutable_ access to `stmt` and `desc`, which means that you do not need to
1107/// increment `Session::state_revision` when modifying fields through a `PortalRefMut`, because the
1108/// portal's meaning is not changed.
1109pub struct PortalRefMut<'a> {
1110    /// The statement that is bound to this portal.
1111    pub stmt: &'a Option<Arc<Statement<Raw>>>,
1112    /// The statement description.
1113    pub desc: &'a StatementDesc,
1114    /// The most recent state revision that has verified this portal.
1115    pub state_revision: &'a mut StateRevision,
1116    /// The bound values for the parameters in the prepared statement, if any.
1117    pub parameters: &'a mut Params,
1118    /// The desired output format for each column in the result set.
1119    pub result_formats: &'a mut Vec<Format>,
1120    /// A handle to metadata needed for statement logging.
1121    pub logging: &'a mut Arc<QCell<PreparedStatementLoggingInfo>>,
1122    /// The execution state of the portal.
1123    pub state: &'a mut PortalState,
1124    /// Statement lifecycle timestamps coming from `mz-pgwire`.
1125    pub lifecycle_timestamps: &'a mut Option<LifecycleTimestamps>,
1126}
1127
1128/// Points to a revision of catalog state and session state. When the current revisions are not the
1129/// same as the revisions when a prepared statement or a portal was described, we need to check
1130/// whether the description is still valid.
1131#[derive(Debug, Clone, Copy, PartialEq)]
1132pub struct StateRevision {
1133    /// A revision of the catalog.
1134    pub catalog_revision: u64,
1135    /// A revision of the session state.
1136    pub session_state_revision: u64,
1137}
1138
1139/// Execution states of a portal.
1140pub enum PortalState {
1141    /// Portal not yet started.
1142    NotStarted,
1143    /// Portal is a rows-returning statement in progress with 0 or more rows
1144    /// remaining.
1145    InProgress(Option<InProgressRows>),
1146    /// Portal has completed and should not be re-executed. If the optional string
1147    /// is present, it is returned as a CommandComplete tag, otherwise an error
1148    /// is sent.
1149    Completed(Option<String>),
1150}
1151
1152/// State of an in-progress, rows-returning portal.
1153pub struct InProgressRows {
1154    /// The current batch of rows.
1155    pub current: Option<Box<dyn RowIterator + Send + Sync>>,
1156    /// A stream from which to fetch more row batches.
1157    pub remaining: RecordFirstRowStream,
1158}
1159
1160impl InProgressRows {
1161    /// Creates a new InProgressRows from a batch stream.
1162    pub fn new(remaining: RecordFirstRowStream) -> Self {
1163        Self {
1164            current: None,
1165            remaining,
1166        }
1167    }
1168
1169    /// Determines whether the underlying stream has ended and there are also no more rows
1170    /// stashed in `current`.
1171    pub fn no_more_rows(&self) -> bool {
1172        self.remaining.no_more_rows && self.current.is_none()
1173    }
1174}
1175
1176/// A stream of batched rows delivered to a client writer.
1177///
1178/// This is a boxed stream, not a bare channel receiver, so the coordinator can
1179/// insert an adapter between itself and the client writer. The SUBSCRIBE path
1180/// wraps the receiver in a byte-accounting `map` whose closure type cannot be
1181/// named, so it has to be boxed. Consumers box the receiver into a
1182/// `RecordFirstRowStream` regardless, so this adds no allocation.
1183pub type RowBatchStream = Box<dyn futures::Stream<Item = PeekResponseUnary> + Unpin + Send + Sync>;
1184
1185/// Part of statement lifecycle. These are timestamps that come from the Adapter frontend
1186/// (`mz-pgwire`) part of the lifecycle.
1187#[derive(Debug, Clone)]
1188pub struct LifecycleTimestamps {
1189    /// When the query was received. More specifically, when the tokio recv returned.
1190    /// For a Simple Query, this is for the whole query, for the Extended Query flow, this is only
1191    /// for `FrontendMessage::Execute`. (This means that this is after parsing for the
1192    /// Extended Query flow.)
1193    pub received: EpochMillis,
1194}
1195
1196impl LifecycleTimestamps {
1197    /// Creates a new `LifecycleTimestamps`.
1198    pub fn new(received: EpochMillis) -> Self {
1199        Self { received }
1200    }
1201}
1202
1203/// The transaction status of a session.
1204///
1205/// PostgreSQL's transaction states are in backend/access/transam/xact.c.
1206#[derive(Debug)]
1207pub enum TransactionStatus {
1208    /// Idle. Matches `TBLOCK_DEFAULT`.
1209    Default,
1210    /// Running a transaction started outside of any `BEGIN`. Matches
1211    /// `TBLOCK_STARTED`. In PostgreSQL, when using the extended query protocol, this
1212    /// may be upgraded into multi-statement implicit query (see [`Self::InTransactionImplicit`]).
1213    /// Additionally, some statements may trigger an eager commit of the implicit transaction,
1214    /// see: <https://git.postgresql.org/gitweb/?p=postgresql.git&a=commitdiff&h=f92944137>. In
1215    /// Materialize we never upgrade it. We eagerly commit it unless it can take on further
1216    /// statements of the same pipeline, see [`Self::may_span_pipeline`].
1217    Started(Transaction),
1218    /// Currently in a transaction issued from a `BEGIN`. Matches `TBLOCK_INPROGRESS`.
1219    InTransaction(Transaction),
1220    /// Currently in an implicit transaction started from a multi-statement query
1221    /// with more than 1 statements. Matches `TBLOCK_IMPLICIT_INPROGRESS`.
1222    InTransactionImplicit(Transaction),
1223    /// In a failed transaction. Matches `TBLOCK_ABORT`.
1224    Failed(Transaction),
1225}
1226
1227impl TransactionStatus {
1228    /// Extracts the inner transaction ops if not failed.
1229    pub fn into_ops(self) -> Option<TransactionOps> {
1230        match self {
1231            TransactionStatus::Default | TransactionStatus::Failed(_) => None,
1232            TransactionStatus::Started(txn)
1233            | TransactionStatus::InTransaction(txn)
1234            | TransactionStatus::InTransactionImplicit(txn) => Some(txn.ops),
1235        }
1236    }
1237
1238    /// Exposes the inner transaction.
1239    pub fn inner(&self) -> Option<&Transaction> {
1240        match self {
1241            TransactionStatus::Default => None,
1242            TransactionStatus::Started(txn)
1243            | TransactionStatus::InTransaction(txn)
1244            | TransactionStatus::InTransactionImplicit(txn)
1245            | TransactionStatus::Failed(txn) => Some(txn),
1246        }
1247    }
1248
1249    /// Exposes the inner transaction.
1250    pub fn inner_mut(&mut self) -> Option<&mut Transaction> {
1251        match self {
1252            TransactionStatus::Default => None,
1253            TransactionStatus::Started(txn)
1254            | TransactionStatus::InTransaction(txn)
1255            | TransactionStatus::InTransactionImplicit(txn)
1256            | TransactionStatus::Failed(txn) => Some(txn),
1257        }
1258    }
1259
1260    /// Whether inspections must be retained for a possible DDL commit.
1261    ///
1262    /// This includes observations made before the first DDL statement. Once the
1263    /// transaction cannot enter DDL mode, retaining their statements and session
1264    /// metadata would consume memory without any commit-time validation.
1265    fn can_retain_ddl_inspections(&self) -> bool {
1266        match self {
1267            Self::Started(txn) | Self::InTransaction(txn) | Self::InTransactionImplicit(txn) => {
1268                !txn.contains_explain
1269                    && match txn.ops {
1270                        TransactionOps::None | TransactionOps::DDL { .. } => true,
1271                        TransactionOps::Peeks { .. }
1272                        | TransactionOps::Writes(_)
1273                        | TransactionOps::Subscribe
1274                        | TransactionOps::SingleStatement { .. } => false,
1275                    }
1276            }
1277            Self::Default | Self::Failed(_) => false,
1278        }
1279    }
1280
1281    /// Records EXPLAIN execution, rejecting transactions that already contain DDL.
1282    pub(crate) fn record_explain(&mut self) -> Result<(), AdapterError> {
1283        let txn = self.inner_mut().expect("active transaction");
1284        if matches!(txn.ops, TransactionOps::DDL { .. }) {
1285            return Err(AdapterError::DDLOnlyTransaction);
1286        }
1287        // EXPLAIN outcomes are not replayed at commit, so this transaction must
1288        // not subsequently enter DDL mode and rebind its writes.
1289        txn.contains_explain = true;
1290        txn.pending_ddl_inspections.clear();
1291        Ok(())
1292    }
1293
1294    /// Whether the transaction's ops are DDL.
1295    pub fn is_ddl(&self) -> bool {
1296        match self {
1297            TransactionStatus::Default => false,
1298            TransactionStatus::Started(txn)
1299            | TransactionStatus::InTransaction(txn)
1300            | TransactionStatus::InTransactionImplicit(txn)
1301            | TransactionStatus::Failed(txn) => {
1302                matches!(txn.ops, TransactionOps::DDL { .. })
1303            }
1304        }
1305    }
1306
1307    /// Expresses whether or not the transaction was implicitly started.
1308    /// However, its negation does not imply explicitly started.
1309    pub fn is_implicit(&self) -> bool {
1310        match self {
1311            TransactionStatus::Started(_) | TransactionStatus::InTransactionImplicit(_) => true,
1312            TransactionStatus::Default
1313            | TransactionStatus::InTransaction(_)
1314            | TransactionStatus::Failed(_) => false,
1315        }
1316    }
1317
1318    /// Whether this implicit transaction may stay open for the rest of its
1319    /// extended-protocol pipeline, so that the pipeline commits or rolls back as a
1320    /// unit like PostgreSQL's.
1321    ///
1322    /// Only writes may, because they are merely staged. A read already pinned its
1323    /// timestamp without timedomain read holds, so a second read could neither join
1324    /// that timestamp nor pick its own.
1325    pub fn may_span_pipeline(&self) -> bool {
1326        match self {
1327            TransactionStatus::Started(txn) => match &txn.ops {
1328                TransactionOps::Writes(_) => true,
1329                TransactionOps::None
1330                | TransactionOps::Peeks { .. }
1331                | TransactionOps::Subscribe
1332                | TransactionOps::SingleStatement { .. }
1333                | TransactionOps::DDL { .. } => false,
1334            },
1335            TransactionStatus::Default
1336            | TransactionStatus::InTransaction(_)
1337            | TransactionStatus::InTransactionImplicit(_)
1338            | TransactionStatus::Failed(_) => false,
1339        }
1340    }
1341
1342    /// Whether the transaction may contain multiple statements.
1343    pub fn is_in_multi_statement_transaction(&self) -> bool {
1344        match self {
1345            TransactionStatus::InTransaction(_) | TransactionStatus::InTransactionImplicit(_) => {
1346                true
1347            }
1348            TransactionStatus::Default
1349            | TransactionStatus::Started(_)
1350            | TransactionStatus::Failed(_) => false,
1351        }
1352    }
1353
1354    /// Whether a statement other than the current one can belong to this
1355    /// transaction, so committing here would commit more than this statement.
1356    ///
1357    /// This is [`Self::is_in_multi_statement_transaction`] widened to cover the
1358    /// `Started` trap. An extended-protocol pipeline stays `Started` from its
1359    /// first statement until `Sync`, so a `Started` transaction already holding
1360    /// ops has a pipeline accumulating in it, and a statement running now runs
1361    /// alongside those ops.
1362    ///
1363    /// Callers must evaluate this before the current statement stages ops of
1364    /// its own. Afterwards `contains_ops` reports the statement's own ops and
1365    /// every statement looks like it shares a transaction.
1366    ///
1367    /// Reports true for a `Failed` transaction that holds ops, since
1368    /// `contains_ops` reads through to the inner transaction in that state.
1369    /// Callers that treat a failed transaction as one a statement may run in
1370    /// need their own check, because pgwire admits only `COMMIT` and `ROLLBACK`
1371    /// once a transaction has failed and so nothing else can observe the
1372    /// difference.
1373    pub fn may_share_transaction_with_other_statements(&self) -> bool {
1374        self.is_in_multi_statement_transaction() || self.contains_ops()
1375    }
1376
1377    /// Whether we are in a multi-statement transaction, AND the query is immediate.
1378    pub fn in_immediate_multi_stmt_txn(&self, when: &QueryWhen) -> bool {
1379        self.is_in_multi_statement_transaction() && when == &QueryWhen::Immediately
1380    }
1381
1382    /// The timeline of the transaction, if one exists.
1383    pub fn timeline(&self) -> Option<Timeline> {
1384        match self {
1385            TransactionStatus::Default => None,
1386            TransactionStatus::Started(txn)
1387            | TransactionStatus::InTransaction(txn)
1388            | TransactionStatus::InTransactionImplicit(txn)
1389            | TransactionStatus::Failed(txn) => txn.timeline(),
1390        }
1391    }
1392
1393    /// The cluster of the transaction, if one exists.
1394    pub fn cluster(&self) -> Option<ClusterId> {
1395        match self {
1396            TransactionStatus::Default => None,
1397            TransactionStatus::Started(txn)
1398            | TransactionStatus::InTransaction(txn)
1399            | TransactionStatus::InTransactionImplicit(txn)
1400            | TransactionStatus::Failed(txn) => txn.cluster(),
1401        }
1402    }
1403
1404    /// Snapshot of the catalog that reflects DDL operations run in this transaction.
1405    pub fn catalog_state(&self) -> Option<&CatalogState> {
1406        match self.inner() {
1407            Some(Transaction {
1408                ops: TransactionOps::DDL { state, .. },
1409                ..
1410            }) => Some(state),
1411            _ => None,
1412        }
1413    }
1414
1415    /// Reports whether any operations have been executed as part of this transaction
1416    pub fn contains_ops(&self) -> bool {
1417        match self.inner() {
1418            Some(txn) => txn.contains_ops(),
1419            None => false,
1420        }
1421    }
1422
1423    /// Checks whether the current state of this transaction allows writes
1424    /// (adding write ops).
1425    pub fn allows_writes(&self) -> bool {
1426        match self {
1427            TransactionStatus::Started(Transaction { ops, access, .. })
1428            | TransactionStatus::InTransaction(Transaction { ops, access, .. })
1429            | TransactionStatus::InTransactionImplicit(Transaction { ops, access, .. }) => {
1430                match ops {
1431                    TransactionOps::None => access != &Some(TransactionAccessMode::ReadOnly),
1432                    TransactionOps::Peeks { determination, .. } => {
1433                        // We can switch a peek-only transaction to a write
1434                        // transaction only if the peeks thus far are constant
1435                        // (i.e. they do not have a timestamp) and the
1436                        // transaction is not explicitly marked read-only.
1437                        access != &Some(TransactionAccessMode::ReadOnly)
1438                            && !determination.timestamp_context.contains_timestamp()
1439                    }
1440                    TransactionOps::Subscribe => false,
1441                    TransactionOps::Writes(_) => true,
1442                    TransactionOps::SingleStatement { .. } => false,
1443                    TransactionOps::DDL { .. } => false,
1444                }
1445            }
1446            TransactionStatus::Default | TransactionStatus::Failed(_) => {
1447                unreachable!()
1448            }
1449        }
1450    }
1451
1452    /// Adds operations to the current transaction. An error is produced if they cannot be merged
1453    /// (i.e., a timestamp-dependent read cannot be merged to an insert).
1454    ///
1455    /// The `DDL` variant is an exception and does not merge operations, but instead overwrites the
1456    /// old ops with the new ops. This is correct because it is only used in conjunction with the
1457    /// Dry Run catalog op which returns an error containing all of the ops, and those ops are
1458    /// passed to this function which then overwrites.
1459    ///
1460    /// # Panics
1461    /// If the operations are compatible but the operation metadata doesn't match. Such as reads at
1462    /// different timestamps, reads on different timelines, reads on different clusters, etc. It's
1463    /// up to the caller to make sure these are aligned.
1464    pub fn add_ops(&mut self, mut add_ops: TransactionOps) -> Result<(), AdapterError> {
1465        if matches!(add_ops, TransactionOps::DDL { .. })
1466            && self.inner().is_some_and(|txn| txn.contains_explain)
1467        {
1468            return Err(AdapterError::DDLOnlyTransaction);
1469        }
1470        match self {
1471            TransactionStatus::Started(Transaction {
1472                ops,
1473                access,
1474                pending_ddl_inspections,
1475                ..
1476            })
1477            | TransactionStatus::InTransaction(Transaction {
1478                ops,
1479                access,
1480                pending_ddl_inspections,
1481                ..
1482            })
1483            | TransactionStatus::InTransactionImplicit(Transaction {
1484                ops,
1485                access,
1486                pending_ddl_inspections,
1487                ..
1488            }) => {
1489                match ops {
1490                    TransactionOps::None => {
1491                        if matches!(access, Some(TransactionAccessMode::ReadOnly))
1492                            && matches!(add_ops, TransactionOps::Writes(_))
1493                        {
1494                            return Err(AdapterError::ReadOnlyTransaction);
1495                        }
1496                        match &mut add_ops {
1497                            TransactionOps::DDL { commands, .. } => {
1498                                // Pre-DDL inspections belong before every mutation, without
1499                                // restricting the transaction until it actually enters DDL mode.
1500                                commands.splice(
1501                                    0..0,
1502                                    pending_ddl_inspections
1503                                        .drain(..)
1504                                        .map(DdlInspection::into_command),
1505                                );
1506                            }
1507                            TransactionOps::None => {}
1508                            TransactionOps::Peeks { .. }
1509                            | TransactionOps::Writes(_)
1510                            | TransactionOps::Subscribe
1511                            | TransactionOps::SingleStatement { .. } => {
1512                                pending_ddl_inspections.clear();
1513                            }
1514                        }
1515                        *ops = add_ops;
1516                    }
1517                    TransactionOps::Peeks {
1518                        determination,
1519                        cluster_id,
1520                        requires_linearization,
1521                    } => match add_ops {
1522                        TransactionOps::Peeks {
1523                            determination: add_timestamp_determination,
1524                            cluster_id: add_cluster_id,
1525                            requires_linearization: add_requires_linearization,
1526                        } => {
1527                            assert_eq!(*cluster_id, add_cluster_id);
1528                            match (
1529                                &determination.timestamp_context,
1530                                &add_timestamp_determination.timestamp_context,
1531                            ) {
1532                                (
1533                                    TimestampContext::TimelineTimestamp {
1534                                        timeline: txn_timeline,
1535                                        chosen_ts: txn_ts,
1536                                        oracle_ts: _,
1537                                    },
1538                                    TimestampContext::TimelineTimestamp {
1539                                        timeline: add_timeline,
1540                                        chosen_ts: add_ts,
1541                                        oracle_ts: _,
1542                                    },
1543                                ) => {
1544                                    assert_eq!(txn_timeline, add_timeline);
1545                                    assert_eq!(txn_ts, add_ts);
1546                                }
1547                                (TimestampContext::NoTimestamp, _) => {
1548                                    *determination = add_timestamp_determination
1549                                }
1550                                (_, TimestampContext::NoTimestamp) => {}
1551                            };
1552                            if matches!(requires_linearization, RequireLinearization::NotRequired)
1553                                && matches!(
1554                                    add_requires_linearization,
1555                                    RequireLinearization::Required
1556                                )
1557                            {
1558                                *requires_linearization = add_requires_linearization;
1559                            }
1560                        }
1561                        // If the peeks thus far are constant (i.e. they do not
1562                        // have a timestamp), writes can follow and switch the
1563                        // transaction to a write transaction. But a read-only
1564                        // transaction must still reject the write. Without that
1565                        // check the write would silently turn a read-only
1566                        // transaction into a write transaction, and a later
1567                        // write would then trip the assert in the `Writes` arm
1568                        // below.
1569                        writes @ TransactionOps::Writes(..)
1570                            if !determination.timestamp_context.contains_timestamp() =>
1571                        {
1572                            if matches!(access, Some(TransactionAccessMode::ReadOnly)) {
1573                                return Err(AdapterError::ReadOnlyTransaction);
1574                            }
1575                            *ops = writes;
1576                        }
1577                        _ => return Err(AdapterError::ReadOnlyTransaction),
1578                    },
1579                    TransactionOps::Subscribe => {
1580                        return Err(AdapterError::SubscribeOnlyTransaction);
1581                    }
1582                    TransactionOps::Writes(txn_writes) => match add_ops {
1583                        TransactionOps::Writes(mut add_writes) => {
1584                            // We should have already checked the access above, but make sure we don't miss
1585                            // it anyway.
1586                            assert!(!matches!(access, Some(TransactionAccessMode::ReadOnly)));
1587                            txn_writes.append(&mut add_writes);
1588                        }
1589                        // Iff peeks do not have a timestamp (i.e. they are
1590                        // constant), we can permit them.
1591                        TransactionOps::Peeks { determination, .. }
1592                            if !determination.timestamp_context.contains_timestamp() => {}
1593                        _ => {
1594                            return Err(AdapterError::WriteOnlyTransaction);
1595                        }
1596                    },
1597                    TransactionOps::SingleStatement { .. } => {
1598                        return Err(AdapterError::SingleStatementTransaction);
1599                    }
1600                    TransactionOps::DDL {
1601                        commands,
1602                        state,
1603                        snapshot,
1604                    } => match add_ops {
1605                        TransactionOps::DDL {
1606                            commands: new_commands,
1607                            state: new_state,
1608                            snapshot: new_snapshot,
1609                        } => {
1610                            if !new_commands.is_empty() {
1611                                commands.extend(new_commands);
1612                                *state = new_state;
1613                                *snapshot = new_snapshot;
1614                            }
1615                        }
1616                        _ => return Err(AdapterError::DDLOnlyTransaction),
1617                    },
1618                }
1619            }
1620            TransactionStatus::Default | TransactionStatus::Failed(_) => {
1621                unreachable!()
1622            }
1623        }
1624        Ok(())
1625    }
1626}
1627
1628/// An abstraction allowing us to identify different transactions.
1629pub type TransactionId = u64;
1630
1631impl Default for TransactionStatus {
1632    fn default() -> Self {
1633        TransactionStatus::Default
1634    }
1635}
1636
1637/// A catalog result or statement description that must remain valid at DDL commit.
1638///
1639/// Retaining inspections before the first DDL statement prevents commit-time
1640/// replanning from contradicting results already exposed or stored for the client.
1641/// Buffering them does not itself enter DDL mode. Transactions that never enter
1642/// DDL mode discard them without validation.
1643#[derive(Debug)]
1644enum DdlInspection {
1645    CatalogResult(Arc<crate::catalog::command::CatalogResultCommand>),
1646    Description(Arc<crate::catalog::command::DescriptionCommand>),
1647}
1648
1649impl DdlInspection {
1650    fn into_command(self) -> crate::catalog::command::CatalogCommand {
1651        match self {
1652            Self::CatalogResult(command) => {
1653                crate::catalog::command::CatalogCommand::CatalogResult(command)
1654            }
1655            Self::Description(command) => {
1656                crate::catalog::command::CatalogCommand::Description(command)
1657            }
1658        }
1659    }
1660}
1661
1662/// State data for transactions.
1663#[derive(Debug)]
1664pub struct Transaction {
1665    completion_owner: TransactionCompletionOwner,
1666    /// Plan context.
1667    pub pcx: PlanContext,
1668    /// Transaction operations.
1669    pub ops: TransactionOps,
1670    /// Catalog results and descriptions awaiting a possible transition to DDL mode.
1671    pending_ddl_inspections: Vec<DdlInspection>,
1672    contains_explain: bool,
1673    /// Uniquely identifies the transaction on a per connection basis.
1674    /// Two transactions started from separate connections may share the
1675    /// same ID.
1676    /// If all IDs have been exhausted, this will wrap around back to 0.
1677    pub id: TransactionId,
1678    /// Access mode (read only, read write).
1679    access: Option<TransactionAccessMode>,
1680}
1681
1682#[derive(Debug, PartialEq)]
1683enum TransactionCompletionOwner {
1684    Session,
1685    Coordinator,
1686}
1687
1688/// Obligations that can be satisfied without transferring the session to the coordinator.
1689pub(crate) struct ReadOnlyCompletion {
1690    pub action: EndTransactionAction,
1691    pub timestamp: Option<TimestampContext>,
1692}
1693
1694impl Transaction {
1695    /// The timeline of the transaction, if one exists.
1696    fn timeline(&self) -> Option<Timeline> {
1697        match &self.ops {
1698            TransactionOps::Peeks {
1699                determination:
1700                    TimestampDetermination {
1701                        timestamp_context: TimestampContext::TimelineTimestamp { timeline, .. },
1702                        ..
1703                    },
1704                ..
1705            } => Some(timeline.clone()),
1706            TransactionOps::Peeks { .. }
1707            | TransactionOps::None
1708            | TransactionOps::Subscribe
1709            | TransactionOps::Writes(_)
1710            | TransactionOps::SingleStatement { .. }
1711            | TransactionOps::DDL { .. } => None,
1712        }
1713    }
1714
1715    /// The cluster of the transaction, if one exists.
1716    pub fn cluster(&self) -> Option<ClusterId> {
1717        match &self.ops {
1718            TransactionOps::Peeks { cluster_id, .. } => Some(cluster_id.clone()),
1719            TransactionOps::None
1720            | TransactionOps::Subscribe
1721            | TransactionOps::Writes(_)
1722            | TransactionOps::SingleStatement { .. }
1723            | TransactionOps::DDL { .. } => None,
1724        }
1725    }
1726
1727    /// Reports whether any operations have been executed as part of this transaction
1728    fn contains_ops(&self) -> bool {
1729        !matches!(self.ops, TransactionOps::None)
1730    }
1731}
1732
1733/// A transaction's status code.
1734#[derive(Debug, Clone, Copy)]
1735pub enum TransactionCode {
1736    /// Not currently in a transaction
1737    Idle,
1738    /// Currently in a transaction
1739    InTransaction,
1740    /// Currently in a transaction block which is failed
1741    Failed,
1742}
1743
1744impl From<TransactionCode> for u8 {
1745    fn from(code: TransactionCode) -> Self {
1746        match code {
1747            TransactionCode::Idle => b'I',
1748            TransactionCode::InTransaction => b'T',
1749            TransactionCode::Failed => b'E',
1750        }
1751    }
1752}
1753
1754impl From<TransactionCode> for String {
1755    fn from(code: TransactionCode) -> Self {
1756        char::from(u8::from(code)).to_string()
1757    }
1758}
1759
1760impl From<&TransactionStatus> for TransactionCode {
1761    /// Convert from the Session's version
1762    fn from(status: &TransactionStatus) -> TransactionCode {
1763        match status {
1764            TransactionStatus::Default => TransactionCode::Idle,
1765            TransactionStatus::Started(_) => TransactionCode::InTransaction,
1766            TransactionStatus::InTransaction(_) => TransactionCode::InTransaction,
1767            TransactionStatus::InTransactionImplicit(_) => TransactionCode::InTransaction,
1768            TransactionStatus::Failed(_) => TransactionCode::Failed,
1769        }
1770    }
1771}
1772
1773/// The type of operation being performed by the transaction.
1774///
1775/// This is needed because we currently do not allow mixing reads and writes in
1776/// a transaction. Use this to record what we have done, and what may need to
1777/// happen at commit.
1778#[derive(Derivative)]
1779#[derivative(Debug)]
1780pub enum TransactionOps {
1781    /// The transaction has been initiated, but no statement has yet been executed
1782    /// in it.
1783    None,
1784    /// This transaction has had a peek (`SELECT`, `SUBSCRIBE`). If the inner value
1785    /// is has a timestamp, it must only do other peeks. However, if it doesn't
1786    /// have a timestamp (i.e. the values are constants), the transaction can still
1787    /// perform writes.
1788    Peeks {
1789        /// The timestamp and timestamp related metadata for the peek.
1790        determination: TimestampDetermination,
1791        /// The cluster used to execute peeks.
1792        cluster_id: ClusterId,
1793        /// Whether this peek needs to be linearized.
1794        requires_linearization: RequireLinearization,
1795    },
1796    /// This transaction has done a `SUBSCRIBE` and must do nothing else.
1797    Subscribe,
1798    /// This transaction has had a write (`INSERT`, `UPDATE`, `DELETE`) and must
1799    /// only do other writes, or reads whose timestamp is None (i.e. constants).
1800    Writes(Vec<WriteOp>),
1801    /// This transaction has a prospective statement that will execute during commit.
1802    SingleStatement {
1803        /// The prospective statement.
1804        stmt: Arc<Statement<Raw>>,
1805        /// The statement params.
1806        params: mz_sql::plan::Params,
1807    },
1808    /// This transaction has run some _simple_ DDL and must do nothing else. Any statement/plan that
1809    /// uses this must return false in `must_serialize_ddl()` because this is serialized instead in
1810    /// `sequence_plan()` during `COMMIT`.
1811    DDL {
1812        /// Commands in statement order, retaining outcomes already exposed to the client.
1813        commands: Vec<crate::catalog::command::CatalogCommand>,
1814        /// Speculative state after the last accepted command.
1815        state: CatalogState,
1816        /// Durable baseline corresponding to `state`.
1817        snapshot: Snapshot,
1818    },
1819}
1820
1821impl TransactionOps {
1822    /// Reports whether the transaction holds work for its commit: writes, DDL that the commit
1823    /// applies, or a statement that runs at commit. A write that affected no rows counts too,
1824    /// because it leaves a `Writes` op behind.
1825    pub fn stages_work(&self) -> bool {
1826        matches!(
1827            self,
1828            TransactionOps::Writes(_)
1829                | TransactionOps::DDL { .. }
1830                | TransactionOps::SingleStatement { .. }
1831        )
1832    }
1833}
1834
1835impl Default for TransactionOps {
1836    fn default() -> Self {
1837        Self::None
1838    }
1839}
1840
1841/// An `INSERT` waiting to be committed.
1842#[derive(Debug, Clone, PartialEq)]
1843pub struct WriteOp {
1844    /// The target table.
1845    pub id: CatalogItemId,
1846    /// The data rows.
1847    pub rows: TableData,
1848}
1849
1850/// Whether a transaction requires linearization.
1851#[derive(Debug)]
1852pub enum RequireLinearization {
1853    /// Linearization is required.
1854    Required,
1855    /// Linearization is not required.
1856    NotRequired,
1857}
1858
1859impl From<&ExplainContext> for RequireLinearization {
1860    fn from(ctx: &ExplainContext) -> Self {
1861        match ctx {
1862            ExplainContext::None | ExplainContext::PlanInsightsNotice(_) => {
1863                RequireLinearization::Required
1864            }
1865            _ => RequireLinearization::NotRequired,
1866        }
1867    }
1868}
1869#[cfg(test)]
1870mod tests {
1871    use super::*;
1872    use mz_sql::session::vars::{SystemVars, VarInput};
1873    use timely::progress::Antichain;
1874
1875    fn read_session() -> Session {
1876        let mut session = Session::dummy();
1877        session.start_transaction_single_stmt(Utc::now());
1878        session
1879            .add_transaction_ops(TransactionOps::Peeks {
1880                determination: TimestampDetermination {
1881                    timestamp_context: TimestampContext::TimelineTimestamp {
1882                        timeline: Timeline::EpochMilliseconds,
1883                        chosen_ts: 10.into(),
1884                        oracle_ts: Some(10.into()),
1885                    },
1886                    since: Antichain::from_elem(0.into()),
1887                    upper: Antichain::new(),
1888                    largest_not_in_advance_of_upper: Timestamp::MAX,
1889                    oracle_read_ts: Some(10.into()),
1890                    session_oracle_read_ts: None,
1891                    real_time_recency_ts: None,
1892                    constraints: Default::default(),
1893                },
1894                cluster_id: ClusterId::User(1),
1895                requires_linearization: RequireLinearization::Required,
1896            })
1897            .expect("read transaction");
1898        session
1899    }
1900
1901    #[mz_ore::test]
1902    fn read_completion_tracks_ownership_not_just_ops() {
1903        let mut session = read_session();
1904        let completion = session
1905            .read_only_completion(EndTransactionAction::Commit)
1906            .expect("local read");
1907        assert_eq!(completion.action, EndTransactionAction::Commit);
1908        assert!(completion.timestamp.is_some());
1909        session.require_coordinator_completion();
1910        assert!(
1911            session
1912                .read_only_completion(EndTransactionAction::Commit)
1913                .is_none()
1914        );
1915        let _ = session.clear_transaction();
1916        session.start_transaction_single_stmt(Utc::now());
1917        assert!(
1918            session
1919                .read_only_completion(EndTransactionAction::Commit)
1920                .is_some()
1921        );
1922    }
1923
1924    #[mz_ore::test]
1925    fn completion_skips_timestamp_for_failed_and_rolled_back_reads() {
1926        let session = read_session();
1927        let rollback = session
1928            .read_only_completion(EndTransactionAction::Rollback)
1929            .expect("local read");
1930        assert!(rollback.timestamp.is_none());
1931        let session = session.fail_transaction();
1932        let failed = session
1933            .read_only_completion(EndTransactionAction::Commit)
1934            .expect("failed local read");
1935        assert_eq!(failed.action, EndTransactionAction::Rollback);
1936        assert!(failed.timestamp.is_none());
1937    }
1938
1939    #[mz_ore::test]
1940    fn completion_rejects_resource_bearing_transactions() {
1941        for ops in [
1942            TransactionOps::Subscribe,
1943            TransactionOps::Writes(Vec::new()),
1944        ] {
1945            let mut session = Session::dummy();
1946            session.start_transaction_single_stmt(Utc::now());
1947            session.add_transaction_ops(ops).expect("first operation");
1948            assert!(
1949                session
1950                    .read_only_completion(EndTransactionAction::Commit)
1951                    .is_none()
1952            );
1953            assert!(
1954                session
1955                    .fail_transaction()
1956                    .read_only_completion(EndTransactionAction::Rollback)
1957                    .is_none()
1958            );
1959        }
1960    }
1961
1962    #[mz_ore::test]
1963    fn read_completion_preserves_transactional_parameter_changes() {
1964        for action in [EndTransactionAction::Commit, EndTransactionAction::Rollback] {
1965            let mut session = read_session();
1966            session
1967                .vars_mut()
1968                .set_default("application_name", VarInput::Flat("session"))
1969                .expect("default");
1970            session
1971                .vars_mut()
1972                .set(
1973                    &SystemVars::new(),
1974                    "application_name",
1975                    VarInput::Flat("local"),
1976                    true,
1977                )
1978                .expect("local setting");
1979            let completion = session.read_only_completion(action).expect("local read");
1980            let _ = session.clear_transaction();
1981            let params = session.vars_mut().end_transaction(completion.action);
1982            assert_eq!(
1983                params.get("application_name").map(String::as_str),
1984                Some("session")
1985            );
1986            assert_eq!(session.vars().application_name(), "session");
1987            assert!(matches!(session.transaction(), TransactionStatus::Default));
1988        }
1989    }
1990
1991    #[mz_ore::test]
1992    fn ddl_inspections_are_released_when_ddl_becomes_impossible() {
1993        use mz_sql::plan::{Params, StatementDesc};
1994        use timely::progress::Antichain;
1995
1996        let statement = mz_sql_parser::parser::parse_statements("SELECT 1")
1997            .expect("valid SQL")
1998            .remove(0)
1999            .ast;
2000        let modes = [
2001            TransactionOps::Peeks {
2002                determination: TimestampDetermination {
2003                    timestamp_context: TimestampContext::NoTimestamp,
2004                    since: Antichain::new(),
2005                    upper: Antichain::new(),
2006                    largest_not_in_advance_of_upper: mz_repr::Timestamp::MIN,
2007                    oracle_read_ts: None,
2008                    session_oracle_read_ts: None,
2009                    real_time_recency_ts: None,
2010                    constraints: Default::default(),
2011                },
2012                cluster_id: mz_controller_types::ClusterId::User(1),
2013                requires_linearization: RequireLinearization::NotRequired,
2014            },
2015            TransactionOps::Writes(vec![]),
2016            TransactionOps::Subscribe,
2017            TransactionOps::SingleStatement {
2018                stmt: Arc::new(statement.clone()),
2019                params: Params::empty(),
2020            },
2021        ];
2022        // None exercises EXPLAIN, which leaves the operation mode unchanged.
2023        for mode in modes.into_iter().map(Some).chain([None]) {
2024            let mut session = Session::dummy();
2025            session.start_transaction_implicit(Utc::now(), 2);
2026            let command = Arc::new(crate::catalog::command::DescriptionCommand::new(
2027                statement.clone(),
2028                vec![],
2029                session.meta(),
2030                StatementDesc::new(Some(mz_repr::RelationDesc::empty())),
2031            ));
2032            let retained = Arc::downgrade(&command);
2033            session.retain_ddl_inspection(DdlInspection::Description(command));
2034            assert!(retained.upgrade().is_some(), "DDL is still possible");
2035            if let Some(mode) = mode {
2036                session.add_transaction_ops(mode).expect("valid transition");
2037            } else {
2038                session
2039                    .transaction_mut()
2040                    .record_explain()
2041                    .expect("EXPLAIN allowed");
2042            }
2043            assert!(retained.upgrade().is_none(), "release earlier inspections");
2044            let command = Arc::new(crate::catalog::command::DescriptionCommand::new(
2045                statement.clone(),
2046                vec![],
2047                session.meta(),
2048                StatementDesc::new(Some(mz_repr::RelationDesc::empty())),
2049            ));
2050            let discarded = Arc::downgrade(&command);
2051            session.retain_ddl_inspection(DdlInspection::Description(command));
2052            assert!(
2053                discarded.upgrade().is_none(),
2054                "do not retain later inspections"
2055            );
2056        }
2057    }
2058}