Skip to main content

mz_adapter/
peek_sequencing.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::collections::BTreeSet;
12use std::future::Future;
13use std::str::FromStr;
14use std::sync::Arc;
15use std::time::Duration;
16
17use futures::StreamExt;
18use http::Uri;
19use itertools::Itertools;
20use maplit::btreemap;
21use mz_catalog::memory::objects::Cluster;
22use mz_compute_types::ComputeInstanceId;
23use mz_controller_types::{ClusterId, ReplicaId};
24use mz_expr::{CollectionPlan, Eval, ResultSpec, RowSetFinishing};
25use mz_ore::cast::{CastFrom, CastLossy};
26use mz_ore::collections::CollectionExt;
27use mz_ore::now::EpochMillis;
28use mz_ore::task::JoinHandle;
29use mz_ore::{soft_assert_eq_or_log, soft_assert_or_log, soft_panic_or_log};
30use mz_repr::explain::{ExplainFormat, ExprHumanizerExt, TransientItem};
31use mz_repr::optimize::{OptimizerFeatures, OverrideFrom};
32use mz_repr::{Datum, GlobalId, IntoRowIterator, Row, RowArena, RowIterator, Timestamp};
33use mz_sql::ast::Raw;
34use mz_sql::catalog::CatalogCluster;
35use mz_sql::plan::Params;
36use mz_sql::plan::{
37    self, Explainee, ExplaineeStatement, HirScalarExpr, Plan, QueryWhen, SelectPlan,
38    SideEffectingFunc, SubscribeFrom, SubscribePlan,
39};
40use mz_sql::rbac;
41use mz_sql::session::metadata::SessionMetadata;
42use mz_sql::session::user::MZ_SYSTEM_ROLE_ID;
43use mz_sql::session::vars::{
44    self, CLUSTER, CLUSTER_REPLICA, IsolationLevel, SessionVars, TRANSACTION_ISOLATION, Var,
45    VarInput,
46};
47use mz_sql_parser::ast::{CopyDirection, ExplainStage, ShowStatement, Statement, StatementKind};
48use mz_storage_client::storage_collections::StorageCollections;
49use mz_storage_types::controller::StorageError;
50use mz_transform::dataflow::DataflowMetainfo;
51use mz_transform::{EmptyStatisticsOracle, StatisticsOracle};
52use opentelemetry::trace::TraceContextExt;
53use timely::progress::Antichain;
54use tracing::{Span, debug, warn};
55use tracing_opentelemetry::OpenTelemetrySpanExt;
56
57use crate::catalog::{Catalog, CatalogState};
58use crate::command::{CatalogSnapshot, Command};
59use crate::coord;
60use crate::coord::peek::{FastPathPlan, PeekPlan, PeekResponseUnary};
61use crate::coord::timeline::timedomain_for;
62use crate::coord::timestamp_selection::TimestampDetermination;
63use crate::coord::{Coordinator, CopyToContext, ExplainContext, ExplainPlanContext, TargetCluster};
64use crate::explain::insights::PlanInsightsContext;
65use crate::explain::optimizer_trace::OptimizerTrace;
66use crate::optimize::Optimize;
67use crate::optimize::dataflows::{
68    ComputeInstanceSnapshot, DataflowBuilder, EvalTime, ExprPrep, ExprPrepOneShot,
69};
70use crate::optimize::peek;
71use crate::peek_client::{ExecutionLogging, TakeOver};
72use crate::session::{Session, TransactionOps, TransactionStatus};
73use crate::statement_logging::StatementLifecycleEvent;
74use crate::statement_logging::WatchSetCreation;
75use crate::{
76    AdapterError, AdapterNotice, CollectionIdBundle, ExecuteResponse, PeekClient, ReadHolds,
77    TimelineContext, TimestampContext, TimestampProvider, optimize,
78};
79
80impl PeekClient {
81    /// Attempt to sequence a peek from the session task.
82    ///
83    /// Returns `Ok(Some(response))` if we handled the peek, or `Ok(None)` to fall back to the
84    /// Coordinator's sequencing. If it returns an error, it should be returned to the user.
85    ///
86    /// `logging` holds the end-of-execution obligation for this statement. The
87    /// caller retires it, this function only takes the statement over and, at
88    /// the dispatch sites that hand execution off, defuses the slot.
89    ///
90    /// `connection_closed` resolves when the client goes away. Waiting for the optimizer gives up
91    /// then, as it does on a statement timeout.
92    pub(crate) async fn try_peek(
93        &mut self,
94        portal_name: &str,
95        session: &mut Session,
96        logging: &mut ExecutionLogging,
97        connection_closed: impl Future<Output = ()> + Send,
98    ) -> Result<Option<ExecuteResponse>, AdapterError> {
99        // # From handle_execute
100
101        if session.vars().emit_trace_id_notice() {
102            let span_context = tracing::Span::current()
103                .context()
104                .span()
105                .span_context()
106                .clone();
107            if span_context.is_valid() {
108                session.add_notice(AdapterNotice::QueryTrace {
109                    trace_id: span_context.trace_id(),
110                });
111            }
112        }
113
114        let catalog = self.catalog_snapshot("try_peek").await;
115
116        // Extract things from the portal. A failed verification does not begin
117        // an entry, mirroring the coordinator: the portal is what statement
118        // logging draws its record from. An adopted outer entry still gets its
119        // `Errored` end from the retirement site in `SessionClient::execute`.
120        let (stmt, params, logging_info, lifecycle_timestamps) = {
121            Coordinator::verify_portal(&*catalog, session, portal_name)?;
122            let portal = session
123                .get_portal_unverified(portal_name)
124                // The portal is a session-level thing, so it couldn't have concurrently disappeared
125                // since the above verification.
126                .expect("called verify_portal above");
127            let params = portal.parameters.clone();
128            let stmt = portal.stmt.clone();
129            let logging_info = Arc::clone(&portal.logging);
130            let lifecycle_timestamps = portal.lifecycle_timestamps.clone();
131            (stmt, params, logging_info, lifecycle_timestamps)
132        };
133
134        // Before planning, check if this is a statement type we can handle.
135        // This must happen BEFORE statement logging setup to avoid orphaned execution records.
136        if let Some(ref stmt) = stmt {
137            if StatementKind::from(&**stmt).is_explain() {
138                session.transaction_mut().record_explain()?;
139            }
140            match &**stmt {
141                Statement::Select(_)
142                | Statement::ExplainAnalyzeObject(_)
143                | Statement::ExplainAnalyzeCluster(_)
144                | Statement::Show(ShowStatement::ShowObjects(_))
145                | Statement::Show(ShowStatement::ShowColumns(_)) => {
146                    // These are always fine, just continue.
147                    // Note: EXPLAIN ANALYZE will `plan` to `Plan::Select`.
148                    // Note: ShowObjects plans to `Plan::Select`, ShowColumns plans to `Plan::ShowColumns`.
149                    // We handle `Plan::ShowColumns` specially in `try_peek_inner`.
150                }
151                Statement::ExplainPlan(explain_stmt) => {
152                    // Only handle ExplainPlan for SELECT and SUBSCRIBE statements.
153                    // We don't want to handle e.g. EXPLAIN CREATE MATERIALIZED VIEW here, because that
154                    // requires purification before planning, which the frontend peek sequencing doesn't
155                    // do.
156                    match &explain_stmt.explainee {
157                        mz_sql_parser::ast::Explainee::Select(..)
158                        | mz_sql_parser::ast::Explainee::Subscribe(..) => {}
159                        _ => {
160                            debug!(
161                                "Bailing out from try_peek, because EXPLAIN is not for a SELECT or SUBSCRIBE"
162                            );
163                            return Ok(None);
164                        }
165                    }
166                }
167                Statement::ExplainPushdown(explain_stmt) => {
168                    // Only handle EXPLAIN FILTER PUSHDOWN for non-BROKEN SELECT statements
169                    match &explain_stmt.explainee {
170                        mz_sql_parser::ast::Explainee::Select(_, false) => {}
171                        _ => {
172                            debug!(
173                                "Bailing out from try_peek, because EXPLAIN FILTER PUSHDOWN is not for a SELECT query or is for EXPLAIN BROKEN"
174                            );
175                            return Ok(None);
176                        }
177                    }
178                }
179                Statement::Copy(copy_stmt) => {
180                    match &copy_stmt.direction {
181                        CopyDirection::To => {
182                            // This is COPY TO (...), continue
183                        }
184                        CopyDirection::From => {
185                            debug!("Bailing out from try_peek, because COPY FROM is not supported");
186                            return Ok(None);
187                        }
188                    }
189                }
190
191                Statement::Subscribe(_) | Statement::ExplainTimestamp(_) => {}
192                _ => {
193                    debug!("Bailing out from try_peek, because statement type is not supported");
194                    return Ok(None);
195                }
196            }
197        }
198
199        // Past this point the coordinator never sees this statement, so every
200        // exit has to produce an outcome for it. The `Ok(None)` bailouts are all
201        // above.
202        logging.take_over(
203            self,
204            session,
205            stmt.as_deref(),
206            &params,
207            &logging_info,
208            &catalog,
209            lifecycle_timestamps,
210            TakeOver::StatementToRun,
211        );
212
213        self.try_peek_inner(session, catalog, stmt, params, logging, connection_closed)
214            .await
215    }
216
217    /// Executes a coordinator-owned `SELECT`, pinned to `replica_id`, and
218    /// returns its rows.
219    ///
220    /// The caller must build `sql` itself rather than accept it from a user.
221    /// It runs as the system role, in a session of its own, and is not
222    /// statement-logged. This calls into the coordinator, so awaiting it on the
223    /// coordinator's main loop deadlocks.
224    ///
225    /// NOTE: Dropping the returned future does not cancel the peek, since
226    /// there is no connection to cancel it with. A peek dropped after its issue
227    /// keeps its read holds until the replica answers or goes away. One dropped
228    /// between its registration with the coordinator and its issue leaves the
229    /// registration behind, since only a response, a cancel of its connection,
230    /// or a drop of its inputs retires it.
231    pub(crate) async fn background_peek(
232        &mut self,
233        sql: &str,
234        cluster_id: ClusterId,
235        replica_id: ReplicaId,
236    ) -> Result<Vec<Row>, AdapterError> {
237        // Not `catalog_snapshot`, which expects the coordinator to outlive its
238        // caller. A background client may race the coordinator's shutdown.
239        let CatalogSnapshot { catalog } = self
240            .call_coordinator(|tx| Command::CatalogSnapshot { tx })
241            .await?;
242        let (cluster_name, replica_name) = {
243            let cluster = catalog
244                .try_get_cluster(cluster_id)
245                .ok_or_else(|| AdapterError::Internal(format!("unknown cluster {cluster_id}")))?;
246            let replica = cluster
247                .replica(replica_id)
248                .ok_or_else(|| AdapterError::Internal(format!("unknown replica {replica_id}")))?;
249            (cluster.name.clone(), replica.name.clone())
250        };
251        let stmt = mz_sql::parse::parse(sql)?.into_element().ast;
252
253        let mut session = Session::dummy();
254        session.initialize_role_metadata(MZ_SYSTEM_ROLE_ID);
255        let system_vars = catalog.system_config();
256        // Serializable reads at the freshest time the inputs can serve. A
257        // strict-serializable read picks the stalest valid time at or past the
258        // oracle's read timestamp instead. In a read-only environment whose
259        // leader is not running, nothing advances the oracle, and such a read
260        // sees introspection logs as of a time before the replicas started.
261        for (var, value) in [
262            (&CLUSTER, cluster_name.as_str()),
263            (&CLUSTER_REPLICA, replica_name.as_str()),
264            (&TRANSACTION_ISOLATION, "serializable"),
265        ] {
266            session
267                .vars_mut()
268                .set(system_vars, var.name(), VarInput::Flat(value), false)?;
269        }
270        session.start_transaction_single_stmt(mz_ore::now::to_datetime((catalog.config().now)()));
271
272        let mut logging = ExecutionLogging::adopt(None, self);
273        let response = self
274            .try_peek_inner(
275                &mut session,
276                catalog,
277                Some(Arc::new(stmt)),
278                Params::empty(),
279                &mut logging,
280                // A background peek has no client connection.
281                futures::future::pending(),
282            )
283            .await?;
284
285        let mut rows = Vec::new();
286        let mut collect = |mut iter: Box<dyn RowIterator + Send + Sync>| {
287            while let Some(row) = iter.next() {
288                rows.push(row.to_owned());
289            }
290        };
291        match response {
292            Some(ExecuteResponse::SendingRowsImmediate { rows }) => collect(rows),
293            Some(ExecuteResponse::SendingRowsStreaming { mut rows, .. }) => {
294                while let Some(response) = rows.next().await {
295                    match response {
296                        PeekResponseUnary::Rows(iter) => collect(iter),
297                        PeekResponseUnary::Error(error) => return Err(error),
298                        PeekResponseUnary::Canceled => return Err(AdapterError::Canceled),
299                        PeekResponseUnary::DependencyDropped(dependency) => {
300                            return Err(dependency.to_concurrent_dependency_drop());
301                        }
302                    }
303                }
304            }
305            response => {
306                return Err(AdapterError::Internal(format!(
307                    "background peek returned an unexpected response: {response:?}"
308                )));
309            }
310        }
311        Ok(rows)
312    }
313
314    /// This is encapsulated in an inner function so that the outer function can still do statement
315    /// logging after the `?` returns of the inner function.
316    ///
317    /// Dispatch sites that hand the statement to the coordinator for
318    /// asynchronous completion (registered peeks, subscribes) `defuse` the
319    /// logging slot at the point where the coordinator takes over. Everywhere
320    /// else the slot stays armed and `SessionClient::execute` logs the end from
321    /// the returned result.
322    #[mz_ore::instrument(level = "debug")]
323    async fn try_peek_inner(
324        &mut self,
325        session: &mut Session,
326        catalog: Arc<Catalog>,
327        stmt: Option<Arc<Statement<Raw>>>,
328        params: Params,
329        logging: &mut ExecutionLogging,
330        connection_closed: impl Future<Output = ()> + Send,
331    ) -> Result<Option<ExecuteResponse>, AdapterError> {
332        let stmt = match stmt {
333            Some(stmt) => stmt,
334            None => {
335                debug!("try_peek_inner succeeded on an empty query");
336                return Ok(Some(ExecuteResponse::EmptyQuery));
337            }
338        };
339
340        // # From handle_execute_inner
341
342        let conn_catalog = catalog.for_session(session);
343        // (`resolved_ids` should be derivable from `stmt`. If `stmt` is later transformed to
344        // remove/add IDs, then `resolved_ids` should be updated to also remove/add those IDs.)
345        let (stmt, resolved_ids) = mz_sql::names::resolve(&conn_catalog, (*stmt).clone())?;
346
347        let pcx = session.pcx();
348        let (plan, sql_impl_ids) =
349            mz_sql::plan::plan(Some(pcx), &conn_catalog, stmt, &params, &resolved_ids)?;
350
351        /// What do we do with the result of the select?
352        enum QueryPlan<'a> {
353            Select(&'a SelectPlan),
354            CopyTo(&'a SelectPlan, CopyToContext),
355            Subscribe(&'a SubscribePlan),
356            ExplainTimestamp(&'a plan::ExplainTimestampPlan),
357        }
358
359        impl QueryPlan<'_> {
360            /// Whether statement logging records the timestamp the statement executes at as
361            /// soon as it is determined. EXPLAIN only determines a timestamp, and COPY TO does
362            /// not log one.
363            fn logs_execution_timestamp(&self, explain_ctx: &ExplainContext) -> bool {
364                let executes = match explain_ctx {
365                    ExplainContext::None | ExplainContext::PlanInsightsNotice(_) => true,
366                    ExplainContext::Plan(_)
367                    | ExplainContext::Pushdown
368                    | ExplainContext::Timestamp => false,
369                };
370                executes
371                    && match self {
372                        QueryPlan::Select(_) | QueryPlan::Subscribe(_) => true,
373                        QueryPlan::CopyTo(..) | QueryPlan::ExplainTimestamp(_) => false,
374                    }
375            }
376        }
377
378        /// A [`QueryPlan`] that goes on to be optimized.
379        enum OptimizerInput<'a> {
380            Select(&'a SelectPlan),
381            CopyTo(&'a SelectPlan, CopyToContext),
382            Subscribe(&'a SubscribePlan),
383        }
384
385        let (query_plan, explain_ctx) = match &plan {
386            Plan::Select(select_plan) => {
387                let explain_ctx = if session.vars().emit_plan_insights_notice() {
388                    let optimizer_trace = OptimizerTrace::new(ExplainStage::PlanInsights.paths());
389                    ExplainContext::PlanInsightsNotice(optimizer_trace)
390                } else {
391                    ExplainContext::None
392                };
393                (QueryPlan::Select(select_plan), explain_ctx)
394            }
395            Plan::ShowColumns(show_columns_plan) => {
396                // ShowColumns wraps a SelectPlan, extract it and proceed as normal.
397                (
398                    QueryPlan::Select(&show_columns_plan.select_plan),
399                    ExplainContext::None,
400                )
401            }
402            Plan::ExplainPlan(plan::ExplainPlanPlan {
403                stage,
404                format,
405                config,
406                explainee: Explainee::Statement(ExplaineeStatement::Select { broken, plan, desc }),
407            }) => {
408                // Create OptimizerTrace to collect optimizer plans
409                let optimizer_trace = OptimizerTrace::new(stage.paths());
410                let explain_ctx = ExplainContext::Plan(ExplainPlanContext {
411                    broken: *broken,
412                    config: config.clone(),
413                    format: *format,
414                    stage: *stage,
415                    replan: None,
416                    desc: Some(desc.clone()),
417                    optimizer_trace,
418                });
419                (QueryPlan::Select(plan), explain_ctx)
420            }
421            // COPY TO S3
422            Plan::CopyTo(plan::CopyToPlan {
423                select_plan,
424                desc,
425                to,
426                connection,
427                connection_id,
428                format,
429                max_file_size,
430            }) => {
431                let uri = eval_copy_to_uri(to.clone(), session, catalog.state())?;
432
433                // (output_batch_count will be set later)
434                let copy_to_ctx = CopyToContext {
435                    desc: desc.clone(),
436                    uri,
437                    connection: connection.clone(),
438                    connection_id: *connection_id,
439                    format: format.clone(),
440                    max_file_size: *max_file_size,
441                    output_batch_count: None,
442                };
443
444                (
445                    QueryPlan::CopyTo(select_plan, copy_to_ctx),
446                    ExplainContext::None,
447                )
448            }
449            Plan::ExplainPushdown(plan::ExplainPushdownPlan { explainee }) => {
450                // Only handle EXPLAIN FILTER PUSHDOWN for SELECT statements
451                match explainee {
452                    plan::Explainee::Statement(plan::ExplaineeStatement::Select {
453                        broken: false,
454                        plan,
455                        desc: _,
456                    }) => {
457                        let explain_ctx = ExplainContext::Pushdown;
458                        (QueryPlan::Select(plan), explain_ctx)
459                    }
460                    _ => {
461                        // This shouldn't happen because we already checked for this at the AST
462                        // level before calling `try_peek_inner`. The logging takeover
463                        // has already happened, so falling back to the coordinator would count
464                        // the statement twice. Report the inconsistency instead.
465                        soft_panic_or_log!(
466                            "unexpected EXPLAIN FILTER PUSHDOWN plan kind in frontend peek sequencing: {:?}",
467                            explainee
468                        );
469                        return Err(AdapterError::Internal(
470                            "unexpected EXPLAIN FILTER PUSHDOWN plan kind in frontend peek sequencing"
471                                .into(),
472                        ));
473                    }
474                }
475            }
476            Plan::SideEffectingFunc(sef_plan) => {
477                // Look up the target connection's authenticated role, so that
478                // check_plan can perform RBAC for side-effecting functions.
479                //
480                // The RBAC check reflects the state at this point in time. A
481                // concurrent change to the issuer's role membership does not
482                // affect the already in-flight execution, similarly to how
483                // privilege changes don't affect other kinds of in-flight
484                // statements.
485                let target_conn = match sef_plan {
486                    SideEffectingFunc::PgCancelBackend {
487                        connection_id: Some(connection_id),
488                    } => {
489                        self.call_coordinator(|tx| Command::LookupConnection {
490                            connection_id: *connection_id,
491                            tx,
492                        })
493                        .await?
494                    }
495                    SideEffectingFunc::PgCancelBackend {
496                        connection_id: None,
497                    } => None,
498                };
499                let target_conn_role = target_conn.as_ref().map(|(_, role)| *role);
500
501                rbac::check_plan(
502                    &conn_catalog,
503                    target_conn_role,
504                    session,
505                    &plan,
506                    None,
507                    &resolved_ids,
508                    &sql_impl_ids,
509                )?;
510
511                // RBAC passed. Delegate execution to the Coordinator.
512                let response = self
513                    .call_coordinator(|tx| Command::ExecuteSideEffectingFunc {
514                        plan: sef_plan.clone(),
515                        conn_id: session.conn_id().clone(),
516                        tx,
517                    })
518                    .await??;
519
520                // We held the target's `ConnectionId` handle from the RBAC
521                // check until the Coordinator executed the function, which
522                // prevented the raw connection ID from being reused by a new
523                // connection. So the connection the Coordinator acted on (if
524                // it found one) is the one whose role we checked above.
525                drop(target_conn);
526
527                return Ok(Some(response));
528            }
529            Plan::Subscribe(subscribe) => (QueryPlan::Subscribe(subscribe), ExplainContext::None),
530            Plan::ExplainTimestamp(explain_timestamp_plan) => {
531                if explain_timestamp_plan.format == ExplainFormat::Dot {
532                    return Err(AdapterError::Unsupported("EXPLAIN TIMESTAMP AS DOT"));
533                }
534                (
535                    QueryPlan::ExplainTimestamp(explain_timestamp_plan),
536                    ExplainContext::Timestamp,
537                )
538            }
539            Plan::ExplainPlan(plan::ExplainPlanPlan {
540                stage,
541                format,
542                config,
543                explainee: Explainee::Statement(ExplaineeStatement::Subscribe { broken, plan }),
544            }) => {
545                let desc = match &plan.from {
546                    SubscribeFrom::Id(_) => None,
547                    SubscribeFrom::Query { desc, .. } => Some(desc.clone()),
548                };
549                let explain_ctx = ExplainContext::Plan(ExplainPlanContext {
550                    broken: *broken,
551                    config: config.clone(),
552                    format: *format,
553                    stage: *stage,
554                    replan: None,
555                    desc,
556                    optimizer_trace: OptimizerTrace::new(stage.paths()),
557                });
558                (QueryPlan::Subscribe(plan), explain_ctx)
559            }
560            Plan::CreateConnection(_)
561            | Plan::CreateDatabase(_)
562            | Plan::CreateSchema(_)
563            | Plan::CreateRole(_)
564            | Plan::CreateCluster(_)
565            | Plan::CreateClusterReplica(_)
566            | Plan::CreateSource(_)
567            | Plan::CreateSources(_)
568            | Plan::CreateSecret(_)
569            | Plan::CreateSink(_)
570            | Plan::CreateTable(_)
571            | Plan::CreateView(_)
572            | Plan::CreateMaterializedView(_)
573            | Plan::CreateNetworkPolicy(_)
574            | Plan::CreateIndex(_)
575            | Plan::CreateMetricSink(_)
576            | Plan::CreateType(_)
577            | Plan::Comment(_)
578            | Plan::DiscardTemp
579            | Plan::DiscardAll
580            | Plan::DropObjects(_)
581            | Plan::DropOwned(_)
582            | Plan::EmptyQuery
583            | Plan::ShowAllVariables
584            | Plan::ShowCreate(_)
585            | Plan::ShowVariable(_)
586            | Plan::InspectShard(_)
587            | Plan::SetVariable(_)
588            | Plan::ResetVariable(_)
589            | Plan::SetTransaction(_)
590            | Plan::StartTransaction(_)
591            | Plan::CommitTransaction(_)
592            | Plan::AbortTransaction(_)
593            | Plan::CopyFrom(_)
594            | Plan::ExplainPlan(_)
595            | Plan::ExplainSinkSchema(_)
596            | Plan::Insert(_)
597            | Plan::AlterCluster(_)
598            | Plan::AlterClusterSwap(_)
599            | Plan::AlterNoop(_)
600            | Plan::AlterSetCluster(_)
601            | Plan::AlterConnection(_)
602            | Plan::AlterSource(_)
603            | Plan::AlterClusterRename(_)
604            | Plan::AlterClusterReplicaRename(_)
605            | Plan::AlterItemRename(_)
606            | Plan::AlterSchemaRename(_)
607            | Plan::AlterSchemaSwap(_)
608            | Plan::AlterSecret(_)
609            | Plan::AlterSink(_)
610            | Plan::AlterSystemSet(_)
611            | Plan::AlterSystemReset(_)
612            | Plan::AlterSystemResetAll(_)
613            | Plan::AlterRole(_)
614            | Plan::AlterOwner(_)
615            | Plan::AlterTableAddColumn(_)
616            | Plan::AlterMaterializedViewApplyReplacement(_)
617            | Plan::AlterNetworkPolicy(_)
618            | Plan::Declare(_)
619            | Plan::Fetch(_)
620            | Plan::Close(_)
621            | Plan::ReadThenWrite(_)
622            | Plan::Prepare(_)
623            | Plan::Execute(_)
624            | Plan::Deallocate(_)
625            | Plan::Raise(_)
626            | Plan::GrantRole(_)
627            | Plan::RevokeRole(_)
628            | Plan::GrantPrivileges(_)
629            | Plan::RevokePrivileges(_)
630            | Plan::AlterDefaultPrivileges(_)
631            | Plan::ReassignOwned(_)
632            | Plan::ValidateConnection(_)
633            | Plan::AlterRetainHistory(_)
634            | Plan::AlterSourceTimestampInterval(_) => {
635                // The other `EXPLAIN` explainees and every other plan kind are excluded at the
636                // AST level before calling `try_peek_inner`. The logging takeover has
637                // already happened, so falling back to the coordinator would count the
638                // statement twice. Report the inconsistency instead.
639                soft_panic_or_log!(
640                    "Unexpected plan kind in frontend peek sequencing: {:?}",
641                    plan
642                );
643                return Err(AdapterError::Internal(
644                    "unexpected plan kind in frontend peek sequencing".into(),
645                ));
646            }
647        };
648
649        let when = match &query_plan {
650            QueryPlan::Select(s) => &s.when,
651            QueryPlan::CopyTo(s, _) => &s.when,
652            QueryPlan::Subscribe(s) => &s.when,
653            QueryPlan::ExplainTimestamp(p) => &p.when,
654        };
655
656        // # From sequence_plan
657
658        // We have checked the plan kind above.
659        assert!(plan.allowed_in_read_only());
660
661        let (cluster, target_cluster_id, target_cluster_name) = {
662            let target_cluster = match session.transaction().cluster() {
663                // Use the current transaction's cluster.
664                Some(cluster_id) => TargetCluster::Transaction(cluster_id),
665                // If there isn't a current cluster set for a transaction, then try to auto route.
666                None => coord::catalog_serving::auto_run_on_catalog_server(
667                    &conn_catalog,
668                    session,
669                    &plan,
670                ),
671            };
672            let cluster = catalog.resolve_target_cluster(target_cluster, session)?;
673            (cluster, cluster.id, &cluster.name)
674        };
675
676        // Log cluster selection
677        if let Some(logging_id) = logging.id() {
678            self.log_set_cluster(logging_id, target_cluster_id, target_cluster_name.clone());
679        }
680
681        coord::catalog_serving::check_cluster_restrictions(
682            target_cluster_name.as_str(),
683            &conn_catalog,
684            &plan,
685        )?;
686
687        rbac::check_plan(
688            &conn_catalog,
689            // SideEffectingFunc is handled above (with its own check_plan call) and returns
690            // early, so no target connection role is needed for the remaining plan types here.
691            None,
692            session,
693            &plan,
694            Some(target_cluster_id),
695            &resolved_ids,
696            &sql_impl_ids,
697        )?;
698
699        if let Some(wait_future) =
700            coord::appends::waiting_on_startup_appends(&*catalog, session, &plan)
701        {
702            wait_future.await;
703        }
704
705        let (depends_on, contains_temporal) = match &query_plan {
706            QueryPlan::Select(s) | QueryPlan::CopyTo(s, _) => {
707                (s.source.depends_on(), s.source.contains_temporal())
708            }
709            QueryPlan::Subscribe(s) => (s.from.depends_on(), s.from.contains_temporal()),
710            QueryPlan::ExplainTimestamp(p) => {
711                // The timestamp depends on what the locally optimized query reads: folding can
712                // remove dependencies that the unoptimized query has.
713                let mut optimizer = optimize::view::Optimizer::new(
714                    optimize::OptimizerConfig::from(catalog.system_config()),
715                    None,
716                );
717                let raw_plan = p.raw_plan.clone();
718                // HIR ⇒ MIR lowering and MIR ⇒ MIR optimization (local)
719                let optimized = mz_ore::task::spawn_blocking(
720                    || "optimize explain timestamp",
721                    move || optimizer.catch_unwind_optimize(raw_plan),
722                )
723                .await?;
724                (optimized.depends_on(), optimized.contains_temporal())
725            }
726        };
727
728        let max_query_result_size = Some(session.vars().max_query_result_size());
729
730        let compute_instance_snapshot =
731            ComputeInstanceSnapshot::new_without_collections(cluster.id());
732
733        let optimizer_config = optimize::OptimizerConfig::from(catalog.system_config())
734            .override_from(&catalog.get_cluster(cluster.id()).config.features())
735            // A cluster-scoped LaunchDarkly rule beats a manual `FEATURES` pin.
736            .override_from(
737                &catalog
738                    .state()
739                    .cluster_scoped_optimizer_overrides(cluster.id()),
740            )
741            .override_from(&explain_ctx);
742
743        if cluster.replicas().next().is_none() && explain_ctx.needs_cluster() {
744            return Err(AdapterError::NoClusterReplicasAvailable {
745                name: cluster.name.clone(),
746                is_managed: cluster.is_managed(),
747            });
748        }
749
750        let (_, view_id) = self.transient_id_gen.allocate_id();
751        let (_, index_id) = self.transient_id_gen.allocate_id();
752
753        let target_replica_name = session.vars().cluster_replica();
754        let mut target_replica = target_replica_name
755            .map(|name| {
756                cluster
757                    .replica_id(name)
758                    .ok_or(AdapterError::UnknownClusterReplica {
759                        cluster_name: cluster.name.clone(),
760                        replica_name: name.to_string(),
761                    })
762            })
763            .transpose()?;
764
765        let source_ids = depends_on;
766        // TODO(peek-seq): validate_timeline_context can be expensive in real scenarios (not in
767        // simple benchmarks), because it traverses transitive dependencies even of indexed views and
768        // materialized views (also traversing their MIR plans).
769        let mut timeline_context = catalog.validate_timeline_context(source_ids.iter().copied())?;
770        if matches!(timeline_context, TimelineContext::TimestampIndependent) && contains_temporal {
771            // If the source IDs are timestamp independent but the query contains temporal functions,
772            // then the timeline context needs to be upgraded to timestamp dependent. This is
773            // required because `source_ids` doesn't contain functions.
774            timeline_context = TimelineContext::TimestampDependent;
775        }
776
777        if explain_ctx.checks_log_reads() {
778            let notices = check_log_reads(
779                &catalog,
780                cluster,
781                &source_ids,
782                &mut target_replica,
783                session.vars(),
784            )?;
785            session.add_notices(notices);
786        }
787
788        let isolation_level = session.vars().transaction_isolation().clone();
789        let timeline = Coordinator::get_timeline(&timeline_context);
790        let needs_linearized_read_ts =
791            Coordinator::needs_linearized_read_ts(&isolation_level, when);
792
793        let oracle_read_ts = match timeline {
794            Some(timeline) if needs_linearized_read_ts => {
795                let oracle = self.ensure_oracle(timeline).await?;
796                let oracle_read_ts = oracle.read_ts().await;
797                #[cfg(debug_assertions)]
798                fail::fail_point!("frontend_peek_after_oracle_read");
799                Some(oracle_read_ts)
800            }
801            Some(_) | None => None,
802        };
803
804        let vars = session.vars();
805        let real_time_recency_ts: Option<Timestamp> = if vars.real_time_recency()
806            && vars.transaction_isolation() == &IsolationLevel::StrictSerializable
807            && !session.contains_read_timestamp()
808        {
809            // Only call the coordinator when we actually need real-time recency
810            self.call_coordinator(|tx| Command::DetermineRealTimeRecentTimestamp {
811                source_ids: source_ids.clone(),
812                real_time_recency_timeout: *vars.real_time_recency_timeout(),
813                tx,
814            })
815            .await??
816        } else {
817            None
818        };
819
820        let dataflow_builder =
821            DataflowBuilder::new(catalog.state(), compute_instance_snapshot.clone());
822        let input_id_bundle = dataflow_builder.sufficient_collections(source_ids.clone());
823
824        // Warning: This will be false for AS OF queries, even if we are otherwise inside a
825        // multi-statement transaction. (It's also false for FreshestTableWrite, which is currently
826        // only read-then-write queries, which can't be part of multi-statement transactions, so
827        // FreshestTableWrite doesn't matter.)
828        //
829        // TODO(peek-seq): It's not totally clear to me what the intended semantics are for AS OF
830        // queries inside a transaction: We clearly can't use the transaction timestamp, and we don't
831        // do timedomain validation for AS OF queries, which seems natural. But I'm thinking that it
832        // would be the cleanest to just simply disallow AS OF queries inside transactions.
833        let in_immediate_multi_stmt_txn = session.transaction().in_immediate_multi_stmt_txn(when)
834            && !matches!(query_plan, QueryPlan::Subscribe { .. });
835
836        // Fetch or generate a timestamp for this query and fetch or acquire read holds.
837        let (determination, read_holds) = match session.get_transaction_timestamp_determination() {
838            // Use the transaction's timestamp if it exists and this isn't an AS OF query.
839            // (`in_immediate_multi_stmt_txn` is false for AS OF queries.)
840            Some(
841                determination @ TimestampDetermination {
842                    timestamp_context: TimestampContext::TimelineTimestamp { .. },
843                    ..
844                },
845            ) if in_immediate_multi_stmt_txn => {
846                // This is a subsequent (non-AS OF, non-constant) query in a multi-statement
847                // transaction. We now:
848                // - Validate that the query only accesses collections within the transaction's
849                //   timedomain (which we know from the stored read holds).
850                // - Use the transaction's stored timestamp determination.
851                // - Use the (relevant subset of the) transaction's read holds.
852
853                let txn_read_holds_opt = self
854                    .call_coordinator(|tx| Command::GetTransactionReadHoldsBundle {
855                        conn_id: session.conn_id().clone(),
856                        tx,
857                    })
858                    .await?;
859
860                if let Some(txn_read_holds) = txn_read_holds_opt {
861                    let allowed_id_bundle = txn_read_holds.id_bundle();
862                    let outside = input_id_bundle.difference(&allowed_id_bundle);
863
864                    // Queries without a timestamp and timeline can belong to any existing timedomain.
865                    if determination.timestamp_context.contains_timestamp() && !outside.is_empty() {
866                        let valid_names =
867                            allowed_id_bundle.resolve_names(&*catalog, session.conn_id());
868                        let invalid_names = outside.resolve_names(&*catalog, session.conn_id());
869                        return Err(AdapterError::RelationOutsideTimeDomain {
870                            relations: invalid_names,
871                            names: valid_names,
872                        });
873                    }
874
875                    // Extract the subset of read holds for the collections this query accesses.
876                    let read_holds = txn_read_holds.subset(&input_id_bundle);
877
878                    (determination, read_holds)
879                } else {
880                    // This should never happen: we're in a subsequent query of a multi-statement
881                    // transaction (we have a transaction timestamp), but the coordinator has no
882                    // transaction read holds stored. This indicates a bug in the transaction
883                    // handling.
884                    return Err(AdapterError::Internal(
885                        "Missing transaction read holds for multi-statement transaction"
886                            .to_string(),
887                    ));
888                }
889            }
890            _ => {
891                // There is no timestamp determination yet for this transaction. Either:
892                // - We are not in a multi-statement transaction.
893                // - This is the first (non-AS OF) query in a multi-statement transaction.
894                // - This is an AS OF query.
895                // - This is a constant query (`TimestampContext::NoTimestamp`).
896
897                let timedomain_bundle;
898                let determine_bundle = if in_immediate_multi_stmt_txn {
899                    // This is the first (non-AS OF) query in a multi-statement transaction.
900                    // Determine a timestamp that will be valid for anything in any schema
901                    // referenced by the first query.
902                    timedomain_bundle = timedomain_for(
903                        &*catalog,
904                        &dataflow_builder,
905                        &source_ids,
906                        &timeline_context,
907                        session.conn_id(),
908                        target_cluster_id,
909                    )?;
910                    &timedomain_bundle
911                } else {
912                    // Simply use the inputs of the current query.
913                    &input_id_bundle
914                };
915                let (determination, read_holds) = self
916                    .determine_timestamp(
917                        session,
918                        determine_bundle,
919                        when,
920                        target_cluster_id,
921                        &timeline_context,
922                        oracle_read_ts,
923                        real_time_recency_ts,
924                    )
925                    .await?;
926
927                // If this query pins the timestamp of a multi-statement transaction, store
928                // the read holds in the coordinator, so subsequent queries can validate
929                // against them. The stored holds define the transaction's timedomain, so
930                // only the statement that determines the transaction timestamp may establish
931                // them, over the same id bundle. A timestamp-less determination (e.g. a
932                // constant query) doesn't pin the transaction timestamp, so its holds must
933                // not be stored.
934                if in_immediate_multi_stmt_txn
935                    && determination.timestamp_context.contains_timestamp()
936                {
937                    session.require_coordinator_completion();
938                    self.call_coordinator(|tx| Command::StoreTransactionReadHolds {
939                        conn_id: session.conn_id().clone(),
940                        read_holds: read_holds.clone(),
941                        tx,
942                    })
943                    .await?;
944                }
945
946                (determination, read_holds)
947            }
948        };
949
950        {
951            // Assert that we have a read hold for all the collections in our `input_id_bundle`.
952            for id in input_id_bundle.iter() {
953                let s = read_holds.storage_holds.contains_key(&id);
954                let c = read_holds
955                    .compute_ids()
956                    .map(|(_instance, coll)| coll)
957                    .contains(&id);
958                soft_assert_or_log!(
959                    s || c,
960                    "missing read hold for collection {} in `input_id_bundle`; (in_immediate_multi_stmt_txn: {})",
961                    id,
962                    in_immediate_multi_stmt_txn,
963                );
964            }
965
966            // Assert that each part of the `input_id_bundle` corresponds to the right part of
967            // `read_holds`.
968            for id in input_id_bundle.storage_ids.iter() {
969                soft_assert_or_log!(
970                    read_holds.storage_holds.contains_key(id),
971                    "missing storage read hold for collection {} in `input_id_bundle`; (in_immediate_multi_stmt_txn: {})",
972                    id,
973                    in_immediate_multi_stmt_txn,
974                );
975            }
976            for id in input_id_bundle
977                .compute_ids
978                .iter()
979                .flat_map(|(_instance, colls)| colls)
980            {
981                soft_assert_or_log!(
982                    read_holds
983                        .compute_ids()
984                        .map(|(_instance, coll)| coll)
985                        .contains(id),
986                    "missing compute read hold for collection {} in `input_id_bundle`; (in_immediate_multi_stmt_txn: {})",
987                    id,
988                    in_immediate_multi_stmt_txn,
989                );
990            }
991        }
992
993        if let (Some(logging_id), Some(timestamp), true) = (
994            logging.id(),
995            determination.timestamp_context.timestamp(),
996            query_plan.logs_execution_timestamp(&explain_ctx),
997        ) {
998            self.log_set_timestamp(logging_id, *timestamp);
999        }
1000
1001        // TODO: Checking for only `InTransaction` and not `Implied` (also `Started`?) seems
1002        // arbitrary and we don't recall why we did it (possibly an error!). Change this to always
1003        // set the transaction ops. Decide and document what our policy should be on AS OF queries.
1004        // Maybe they shouldn't be allowed in transactions at all because it's hard to explain
1005        // what's going on there. This should probably get a small design document.
1006
1007        // We only track the peeks in the session if the query doesn't use AS
1008        // OF or we're inside an explicit transaction. The latter case is
1009        // necessary to support PG's `BEGIN` semantics, whose behavior can
1010        // depend on whether or not reads have occurred in the txn.
1011        let requires_linearization = (&explain_ctx).into();
1012        let mut transaction_determination = determination.clone();
1013        match &query_plan {
1014            QueryPlan::Subscribe { .. } => {
1015                if when.is_transactional() && explain_ctx.needs_cluster() {
1016                    session.add_transaction_ops(TransactionOps::Subscribe)?;
1017                }
1018            }
1019            QueryPlan::Select(..) | QueryPlan::CopyTo(..) | QueryPlan::ExplainTimestamp(..) => {
1020                if when.is_transactional() {
1021                    session.add_transaction_ops(TransactionOps::Peeks {
1022                        determination: transaction_determination,
1023                        cluster_id: target_cluster_id,
1024                        requires_linearization,
1025                    })?;
1026                } else if matches!(session.transaction(), &TransactionStatus::InTransaction(_)) {
1027                    // If the query uses AS OF, then ignore the timestamp.
1028                    transaction_determination.timestamp_context = TimestampContext::NoTimestamp;
1029                    session.add_transaction_ops(TransactionOps::Peeks {
1030                        determination: transaction_determination,
1031                        cluster_id: target_cluster_id,
1032                        requires_linearization,
1033                    })?;
1034                }
1035            }
1036        }
1037
1038        // EXPLAIN TIMESTAMP is done once its timestamp is determined.
1039        let query_plan = match query_plan {
1040            QueryPlan::Select(select_plan) => OptimizerInput::Select(select_plan),
1041            QueryPlan::CopyTo(select_plan, copy_to_ctx) => {
1042                OptimizerInput::CopyTo(select_plan, copy_to_ctx)
1043            }
1044            QueryPlan::Subscribe(plan) => OptimizerInput::Subscribe(plan),
1045            QueryPlan::ExplainTimestamp(explain_timestamp_plan) => {
1046                // Test-only synchronization point: parks the statement before the coordinator
1047                // looks up the frontiers of `input_id_bundle`, so a test can drop one of its
1048                // collections in between.
1049                fail::fail_point!("explain_timestamp_before_dispatch");
1050                let explanation = self
1051                    .call_coordinator(|tx| Command::ExplainTimestamp {
1052                        conn_id: session.conn_id().clone(),
1053                        session_wall_time: session.pcx().wall_time,
1054                        cluster_id: target_cluster_id,
1055                        id_bundle: input_id_bundle.clone(),
1056                        determination,
1057                        tx,
1058                    })
1059                    .await??;
1060                let s = match explain_timestamp_plan.format {
1061                    ExplainFormat::Json => serde_json::to_string_pretty(&explanation)
1062                        .expect("failed to serialize explanation"),
1063                    // `AS DOT` is rejected before any work, when the plan is matched.
1064                    ExplainFormat::Text | ExplainFormat::Dot => explanation.to_string(),
1065                };
1066                let rows = vec![Row::pack_slice(&[Datum::from(s.as_str())])];
1067                return Ok(Some(Coordinator::send_immediate_rows(rows)));
1068            }
1069        };
1070
1071        let stats = statistics_oracle(
1072            session,
1073            &source_ids,
1074            &determination.timestamp_context.antichain(),
1075            true,
1076            catalog.system_config(),
1077            &*self.storage_collections,
1078        )
1079        .await
1080        .unwrap_or_else(|_| Box::new(EmptyStatisticsOracle));
1081
1082        // Generate data structures that can be moved to another task where we will perform possibly
1083        // expensive optimizations.
1084        let timestamp_context = determination.timestamp_context.clone();
1085        let session_meta = session.meta();
1086        let now = catalog.config().now.clone();
1087        let target_cluster_name = target_cluster_name.clone();
1088        let needs_plan_insights = explain_ctx.needs_plan_insights();
1089        let determination_for_pushdown = if matches!(explain_ctx, ExplainContext::Pushdown) {
1090            // This is a hairy data structure, so avoid this clone if we are not in
1091            // EXPLAIN FILTER PUSHDOWN.
1092            Some(determination.clone())
1093        } else {
1094            None
1095        };
1096
1097        let span = Span::current();
1098
1099        // Prepare data for plan insights if needed
1100        let catalog_for_insights = if needs_plan_insights {
1101            Some(Arc::clone(&catalog))
1102        } else {
1103            None
1104        };
1105        let mut compute_instances = BTreeMap::new();
1106        if needs_plan_insights {
1107            for user_cluster in catalog.user_clusters() {
1108                let snapshot = ComputeInstanceSnapshot::new_without_collections(user_cluster.id);
1109                compute_instances.insert(user_cluster.name.clone(), snapshot);
1110            }
1111        }
1112
1113        let source_ids_for_closure = source_ids.clone();
1114
1115        let optimization_future: JoinHandle<Result<_, AdapterError>> = match query_plan {
1116            OptimizerInput::CopyTo(select_plan, mut copy_to_ctx) => {
1117                let raw_expr = select_plan.source.clone();
1118
1119                // COPY TO path: calculate output_batch_count and create copy_to optimizer
1120                let worker_counts = cluster.replicas().map(|r| {
1121                    let loc = &r.config.location;
1122                    loc.workers().unwrap_or_else(|| loc.num_processes())
1123                });
1124                let max_worker_count = match worker_counts.max() {
1125                    Some(count) => u64::cast_from(count),
1126                    None => {
1127                        return Err(AdapterError::NoClusterReplicasAvailable {
1128                            name: cluster.name.clone(),
1129                            is_managed: cluster.is_managed(),
1130                        });
1131                    }
1132                };
1133                copy_to_ctx.output_batch_count = Some(max_worker_count);
1134
1135                let mut optimizer = optimize::copy_to::Optimizer::new(
1136                    Arc::clone(&catalog),
1137                    compute_instance_snapshot,
1138                    view_id,
1139                    copy_to_ctx,
1140                    optimizer_config,
1141                    self.optimizer_metrics.clone(),
1142                );
1143
1144                mz_ore::task::spawn_blocking(
1145                    || "optimize copy-to",
1146                    move || {
1147                        span.in_scope(|| {
1148                            let _dispatch_guard = explain_ctx.dispatch_guard();
1149
1150                            // COPY TO path: HIR ⇒ local MIR ⇒ resolve ⇒ global LIR.
1151                            let global_lir_plan = optimize::optimize_oneshot(
1152                                &mut optimizer,
1153                                raw_expr.clone(),
1154                                |local_mir_plan| {
1155                                    local_mir_plan.resolve(
1156                                        timestamp_context.clone(),
1157                                        &session_meta,
1158                                        stats,
1159                                    )
1160                                },
1161                            )?;
1162                            Ok(Execution::CopyToS3 {
1163                                global_lir_plan,
1164                                source_ids: source_ids_for_closure,
1165                            })
1166                        })
1167                    },
1168                )
1169            }
1170            OptimizerInput::Select(select_plan) => {
1171                let select_plan = select_plan.clone();
1172                let raw_expr = select_plan.source.clone();
1173
1174                // SELECT/EXPLAIN path: create peek optimizer
1175                let mut optimizer = optimize::peek::Optimizer::new(
1176                    Arc::clone(&catalog),
1177                    compute_instance_snapshot,
1178                    select_plan.finishing.clone(),
1179                    view_id,
1180                    index_id,
1181                    optimizer_config,
1182                    self.optimizer_metrics.clone(),
1183                );
1184
1185                mz_ore::task::spawn_blocking(
1186                    || "optimize peek",
1187                    move || {
1188                        fail::fail_point!("peek_before_optimize");
1189                        span.in_scope(|| {
1190                            let _dispatch_guard = explain_ctx.dispatch_guard();
1191
1192                            // SELECT/EXPLAIN path: HIR ⇒ local MIR ⇒ resolve ⇒
1193                            // global LIR. We capture the result (rather than
1194                            // propagating with `?`) so that a failure can still be
1195                            // routed to `EXPLAIN BROKEN` below.
1196                            let global_lir_plan_result = optimize::optimize_oneshot(
1197                                &mut optimizer,
1198                                raw_expr.clone(),
1199                                |local_mir_plan| {
1200                                    local_mir_plan.resolve(
1201                                        timestamp_context.clone(),
1202                                        &session_meta,
1203                                        stats,
1204                                    )
1205                                },
1206                            )
1207                            .map_err(AdapterError::from);
1208                            let optimization_finished_at = now();
1209
1210                            let create_insights_ctx =
1211                                |optimizer: &optimize::peek::Optimizer,
1212                                 is_notice: bool|
1213                                 -> Option<Box<PlanInsightsContext>> {
1214                                    if !needs_plan_insights {
1215                                        return None;
1216                                    }
1217
1218                                    let catalog = catalog_for_insights.as_ref()?;
1219
1220                                    let enable_re_optimize = if needs_plan_insights {
1221                                        // Disable any plan insights that use the optimizer if we only want the
1222                                        // notice and plan optimization took longer than the threshold. This is
1223                                        // to prevent a situation where optimizing takes a while and there are
1224                                        // lots of clusters, which would delay peek execution by the product of
1225                                        // those.
1226                                        //
1227                                        // (This heuristic doesn't work well, see #9492.)
1228                                        let dyncfgs = catalog.system_config().dyncfgs();
1229                                        let opt_limit = mz_adapter_types::dyncfgs
1230                                        ::PLAN_INSIGHTS_NOTICE_FAST_PATH_CLUSTERS_OPTIMIZE_DURATION
1231                                            .get(dyncfgs);
1232                                        !(is_notice && optimizer.duration() > opt_limit)
1233                                    } else {
1234                                        false
1235                                    };
1236
1237                                    Some(Box::new(PlanInsightsContext {
1238                                        stmt: select_plan
1239                                            .select
1240                                            .as_deref()
1241                                            .map(Clone::clone)
1242                                            .map(Statement::Select),
1243                                        raw_expr: raw_expr.clone(),
1244                                        catalog: Arc::clone(catalog),
1245                                        compute_instances,
1246                                        target_instance: target_cluster_name,
1247                                        metrics: optimizer.metrics().clone(),
1248                                        finishing: optimizer.finishing().clone(),
1249                                        optimizer_config: optimizer.config().clone(),
1250                                        session: session_meta,
1251                                        timestamp_context,
1252                                        view_id: optimizer.select_id(),
1253                                        index_id: optimizer.index_id(),
1254                                        enable_re_optimize,
1255                                    }))
1256                                };
1257
1258                            let global_lir_plan = match global_lir_plan_result {
1259                                Ok(plan) => plan,
1260                                Err(err) => {
1261                                    let result = if let ExplainContext::Plan(explain_ctx) =
1262                                        explain_ctx
1263                                        && explain_ctx.broken
1264                                    {
1265                                        // EXPLAIN BROKEN: log error and continue with defaults
1266                                        tracing::error!(
1267                                            "error while handling EXPLAIN statement: {}",
1268                                            err
1269                                        );
1270                                        Ok(Execution::ExplainPlan {
1271                                            df_meta: Default::default(),
1272                                            explain_ctx,
1273                                            optimizer,
1274                                            insights_ctx: None,
1275                                        })
1276                                    } else {
1277                                        Err(err)
1278                                    };
1279                                    return result;
1280                                }
1281                            };
1282
1283                            match explain_ctx {
1284                                // EXPLAIN TIMESTAMP is done before optimization.
1285                                ExplainContext::Timestamp => {
1286                                    soft_panic_or_log!(
1287                                        "EXPLAIN TIMESTAMP reached peek optimization"
1288                                    );
1289                                    Err(AdapterError::Internal(
1290                                        "EXPLAIN TIMESTAMP reached peek optimization".into(),
1291                                    ))
1292                                }
1293                                ExplainContext::Plan(explain_ctx) => {
1294                                    let (_, df_meta, _) = global_lir_plan.unapply();
1295                                    let insights_ctx = create_insights_ctx(&optimizer, false);
1296                                    Ok(Execution::ExplainPlan {
1297                                        df_meta,
1298                                        explain_ctx,
1299                                        optimizer,
1300                                        insights_ctx,
1301                                    })
1302                                }
1303                                ExplainContext::None => Ok(Execution::Peek {
1304                                    global_lir_plan,
1305                                    optimization_finished_at,
1306                                    plan_insights_optimizer_trace: None,
1307                                    finishing: select_plan.finishing,
1308                                    copy_to: select_plan.copy_to,
1309                                    insights_ctx: None,
1310                                }),
1311                                ExplainContext::PlanInsightsNotice(optimizer_trace) => {
1312                                    let insights_ctx = create_insights_ctx(&optimizer, true);
1313                                    Ok(Execution::Peek {
1314                                        global_lir_plan,
1315                                        optimization_finished_at,
1316                                        plan_insights_optimizer_trace: Some(optimizer_trace),
1317                                        finishing: select_plan.finishing,
1318                                        copy_to: select_plan.copy_to,
1319                                        insights_ctx,
1320                                    })
1321                                }
1322                                ExplainContext::Pushdown => {
1323                                    let (plan, _, _) = global_lir_plan.unapply();
1324                                    let imports = match plan {
1325                                        PeekPlan::SlowPath(plan) => plan
1326                                            .desc
1327                                            .source_imports
1328                                            .into_iter()
1329                                            .filter_map(|(id, import)| {
1330                                                import
1331                                                    .desc
1332                                                    .arguments
1333                                                    .operators
1334                                                    .map(|plan| (id, plan))
1335                                            })
1336                                            .collect(),
1337                                        PeekPlan::FastPath(_) => {
1338                                            std::collections::BTreeMap::default()
1339                                        }
1340                                    };
1341                                    Ok(Execution::ExplainPushdown {
1342                                        imports,
1343                                        determination: determination_for_pushdown
1344                                            .expect("it's present for the ExplainPushdown case"),
1345                                    })
1346                                }
1347                            }
1348                        })
1349                    },
1350                )
1351            }
1352            OptimizerInput::Subscribe(plan) => {
1353                let plan = plan.clone();
1354                let catalog: Arc<Catalog> = Arc::clone(&catalog);
1355                let debug_name = format!("subscribe-{}", index_id);
1356                let mut optimizer = optimize::subscribe::Optimizer::new(
1357                    catalog,
1358                    compute_instance_snapshot.clone(),
1359                    view_id,
1360                    index_id,
1361                    plan.with_snapshot,
1362                    plan.up_to,
1363                    debug_name,
1364                    optimizer_config,
1365                    self.optimizer_metrics.clone(),
1366                );
1367                mz_ore::task::spawn_blocking(
1368                    || "optimize subscribe",
1369                    move || {
1370                        span.in_scope(|| {
1371                            let _dispatch_guard = explain_ctx.dispatch_guard();
1372
1373                            let global_mir_plan = optimizer.catch_unwind_optimize(plan.clone())?;
1374                            let as_of = timestamp_context.timestamp_or_default();
1375
1376                            if let Some(up_to) = optimizer.up_to() {
1377                                if as_of > up_to {
1378                                    return Err(AdapterError::AbsurdSubscribeBounds {
1379                                        as_of,
1380                                        up_to,
1381                                    });
1382                                }
1383                            }
1384                            let local_mir_plan =
1385                                global_mir_plan.resolve(Antichain::from_elem(as_of));
1386
1387                            let global_lir_plan = optimizer.catch_unwind_optimize(local_mir_plan);
1388                            if let ExplainContext::Plan(explain_ctx) = explain_ctx {
1389                                let df_meta = match global_lir_plan {
1390                                    Ok(global_lir_plan) => global_lir_plan.unapply().1,
1391                                    // EXPLAIN BROKEN: log the error and continue with defaults.
1392                                    Err(err) if explain_ctx.broken => {
1393                                        tracing::error!(
1394                                            "error while handling EXPLAIN statement: {}",
1395                                            err
1396                                        );
1397                                        Default::default()
1398                                    }
1399                                    Err(err) => return Err(err.into()),
1400                                };
1401                                return Ok(Execution::ExplainSubscribe {
1402                                    df_meta,
1403                                    explain_ctx,
1404                                    sink_id: optimizer.sink_id(),
1405                                    cluster_id: optimizer.cluster_id(),
1406                                    features: optimizer.config().features.clone(),
1407                                    equal_bounds: optimizer.up_to().filter(|up_to| *up_to == as_of),
1408                                });
1409                            }
1410                            let global_lir_plan = global_lir_plan?;
1411                            let optimization_finished_at = now();
1412
1413                            let (df_desc, df_meta) = global_lir_plan.unapply();
1414                            Ok(Execution::Subscribe {
1415                                subscribe_plan: plan,
1416                                df_desc,
1417                                df_meta,
1418                                optimization_finished_at,
1419                            })
1420                        })
1421                    },
1422                )
1423            }
1424        };
1425
1426        let mut optimization_timeout = *session.vars().statement_timeout();
1427        // Timeout of 0 is equivalent to "off", meaning we will wait "forever."
1428        if optimization_timeout == Duration::ZERO {
1429            optimization_timeout = Duration::MAX;
1430        }
1431        // Note: spawn_blocking tasks cannot be cancelled, so on timeout or when the client goes away
1432        // we stop waiting but the optimization task continues running in the background until
1433        // completion. See https://github.com/MaterializeInc/database-issues/issues/8644 for properly
1434        // cancelling optimizer runs.
1435        let optimization_wait = tokio::time::timeout(optimization_timeout, optimization_future);
1436        let optimization_wait = tokio::select! {
1437            result = optimization_wait => result,
1438            () = connection_closed => return Err(AdapterError::Canceled),
1439        };
1440        let optimization_result = match optimization_wait {
1441            Ok(Ok(result)) => result,
1442            Ok(Err(AdapterError::Optimizer(err))) => {
1443                return Err(AdapterError::Internal(format!(
1444                    "internal error in optimizer: {}",
1445                    err
1446                )));
1447            }
1448            Ok(Err(err)) => {
1449                return Err(err);
1450            }
1451            Err(_elapsed) => {
1452                warn!("optimize peek timed out after {:?}", optimization_timeout);
1453                return Err(AdapterError::StatementTimeout);
1454            }
1455        };
1456
1457        // Log optimization finished
1458        if let Some(logging_id) = logging.id() {
1459            self.log_lifecycle_event(logging_id, StatementLifecycleEvent::OptimizationFinished);
1460        }
1461
1462        // Assert that read holds are correct for the execution plan
1463        Self::assert_read_holds_correct(
1464            &read_holds,
1465            &optimization_result,
1466            &determination,
1467            target_cluster_id,
1468            in_immediate_multi_stmt_txn,
1469        );
1470
1471        // Handle the optimization result: either generate EXPLAIN output or continue with execution
1472        match optimization_result {
1473            Execution::ExplainPlan {
1474                df_meta,
1475                explain_ctx,
1476                optimizer,
1477                insights_ctx,
1478            } => {
1479                let rows = explain_plan_inner(
1480                    session,
1481                    &catalog,
1482                    df_meta,
1483                    explain_ctx,
1484                    optimizer,
1485                    insights_ctx,
1486                )
1487                .await?;
1488
1489                Ok(Some(ExecuteResponse::SendingRowsImmediate {
1490                    rows: Box::new(rows.into_row_iter()),
1491                }))
1492            }
1493            Execution::ExplainSubscribe {
1494                df_meta,
1495                explain_ctx,
1496                sink_id,
1497                cluster_id,
1498                features,
1499                equal_bounds,
1500            } => {
1501                if let Some(bound) = equal_bounds {
1502                    session.add_notice(AdapterNotice::EqualSubscribeBounds { bound });
1503                }
1504                let rows = explain_subscribe_inner(
1505                    session,
1506                    &catalog,
1507                    df_meta,
1508                    explain_ctx,
1509                    sink_id,
1510                    cluster_id,
1511                    &features,
1512                )
1513                .await?;
1514
1515                Ok(Some(ExecuteResponse::SendingRowsImmediate {
1516                    rows: Box::new(rows.into_row_iter()),
1517                }))
1518            }
1519            Execution::ExplainPushdown {
1520                imports,
1521                determination,
1522            } => {
1523                let as_of = determination.timestamp_context.antichain();
1524                let mz_now = determination
1525                    .timestamp_context
1526                    .timestamp()
1527                    .map(|t| ResultSpec::value(Datum::MzTimestamp(*t)))
1528                    .unwrap_or_else(ResultSpec::value_all);
1529
1530                Ok(Some(
1531                    coord::sequencer::explain_pushdown_future_inner(
1532                        session,
1533                        &*catalog,
1534                        &self.storage_collections,
1535                        as_of,
1536                        mz_now,
1537                        imports,
1538                    )
1539                    .await
1540                    .await?,
1541                ))
1542            }
1543            Execution::Peek {
1544                global_lir_plan,
1545                optimization_finished_at: _optimization_finished_at,
1546                plan_insights_optimizer_trace,
1547                finishing,
1548                copy_to,
1549                insights_ctx,
1550            } => {
1551                // Continue with normal execution
1552
1553                // The typ here was generated from the HIR SQL type and simply stored in LIR.
1554                let (peek_plan, df_meta, typ) = global_lir_plan.unapply();
1555
1556                coord::sequencer::emit_optimizer_notices(
1557                    &*catalog,
1558                    session,
1559                    &df_meta.optimizer_notices,
1560                );
1561
1562                // Generate plan insights notice if needed
1563                if let Some(trace) = plan_insights_optimizer_trace {
1564                    let target_cluster = catalog.get_cluster(target_cluster_id);
1565                    let features = OptimizerFeatures::from(catalog.system_config())
1566                        .override_from(&target_cluster.config.features())
1567                        // A cluster-scoped LaunchDarkly rule beats a manual
1568                        // `FEATURES` pin.
1569                        .override_from(
1570                            &catalog
1571                                .state()
1572                                .cluster_scoped_optimizer_overrides(target_cluster_id),
1573                        );
1574                    let insights = trace
1575                        .into_plan_insights(
1576                            &features,
1577                            &catalog.for_session(session),
1578                            Some(finishing.clone()),
1579                            Some(target_cluster),
1580                            df_meta.clone(),
1581                            insights_ctx,
1582                        )
1583                        .await?;
1584                    session.add_notice(AdapterNotice::PlanInsights(insights));
1585                }
1586
1587                let watch_set = logging.id().map(|logging_id| {
1588                    WatchSetCreation::new(
1589                        logging_id,
1590                        catalog.state(),
1591                        &input_id_bundle,
1592                        determination.timestamp_context.timestamp_or_default(),
1593                    )
1594                });
1595
1596                let max_result_size = catalog.system_config().max_result_size();
1597
1598                // Clone determination if we need it for emit_timestamp_notice, since it may be
1599                // moved into Command::ExecuteSlowPathPeek.
1600                let determination_for_notice = if session.vars().emit_timestamp_notice() {
1601                    Some(determination.clone())
1602                } else {
1603                    None
1604                };
1605
1606                let response = match peek_plan {
1607                    PeekPlan::FastPath(fast_path_plan) => {
1608                        let row_set_finishing_seconds =
1609                            session.metrics().row_set_finishing_seconds().clone();
1610
1611                        let peek_stash_read_batch_size_bytes =
1612                            mz_compute_types::dyncfgs::PEEK_RESPONSE_STASH_READ_BATCH_SIZE_BYTES
1613                                .get(catalog.system_config().dyncfgs());
1614                        let peek_stash_read_memory_budget_bytes =
1615                            mz_compute_types::dyncfgs::PEEK_RESPONSE_STASH_READ_MEMORY_BUDGET_BYTES
1616                                .get(catalog.system_config().dyncfgs());
1617
1618                        self.implement_fast_path_peek_plan(
1619                            fast_path_plan,
1620                            determination.timestamp_context.timestamp_or_default(),
1621                            finishing,
1622                            target_cluster_id,
1623                            target_replica,
1624                            typ,
1625                            max_result_size,
1626                            max_query_result_size,
1627                            row_set_finishing_seconds,
1628                            read_holds,
1629                            peek_stash_read_batch_size_bytes,
1630                            peek_stash_read_memory_budget_bytes,
1631                            session.conn_id().clone(),
1632                            source_ids,
1633                            watch_set,
1634                            logging,
1635                        )
1636                        .await?
1637                    }
1638                    PeekPlan::SlowPath(dataflow_plan) => {
1639                        session.require_coordinator_completion();
1640                        if let Some(logging_id) = logging.id() {
1641                            self.log_set_transient_index_id(logging_id, dataflow_plan.id);
1642                        }
1643
1644                        let response = self
1645                            .call_coordinator(|tx| Command::ExecuteSlowPathPeek {
1646                                dataflow_plan: Box::new(dataflow_plan),
1647                                determination,
1648                                finishing,
1649                                compute_instance: target_cluster_id,
1650                                target_replica,
1651                                intermediate_result_type: typ,
1652                                source_ids,
1653                                conn_id: session.conn_id().clone(),
1654                                max_result_size,
1655                                max_query_result_size,
1656                                watch_set,
1657                                tx,
1658                            })
1659                            .await??;
1660                        // On success the peek is registered in `pending_peeks`,
1661                        // which now owns end-of-execution logging. On error the
1662                        // coordinator logs nothing (see
1663                        // `implement_slow_path_peek`), so the guard stays armed
1664                        // and the caller logs the error.
1665                        logging.defuse();
1666                        response
1667                    }
1668                };
1669
1670                // Add timestamp notice if emit_timestamp_notice is enabled
1671                if let Some(determination) = determination_for_notice {
1672                    self.add_timestamp_notice(
1673                        session,
1674                        target_cluster_id,
1675                        input_id_bundle.clone(),
1676                        determination,
1677                    )
1678                    .await?;
1679                }
1680
1681                Ok(Some(match copy_to {
1682                    None => response,
1683                    // COPY TO STDOUT
1684                    Some(format) => ExecuteResponse::CopyTo {
1685                        format,
1686                        resp: Box::new(response),
1687                    },
1688                }))
1689            }
1690            Execution::Subscribe {
1691                subscribe_plan,
1692                df_desc,
1693                df_meta,
1694                optimization_finished_at: _optimization_finished_at,
1695            } => {
1696                session.require_coordinator_completion();
1697                if df_desc.as_of.as_ref().expect("as of set") == &df_desc.until {
1698                    session.add_notice(AdapterNotice::EqualSubscribeBounds {
1699                        bound: *df_desc.until.as_option().expect("as of set"),
1700                    });
1701                }
1702                coord::sequencer::emit_optimizer_notices(
1703                    &*catalog,
1704                    session,
1705                    &df_meta.optimizer_notices,
1706                );
1707
1708                // Test-only synchronization point: parks a subscribe between
1709                // sequencing and dispatch, so a test can land a concurrent DROP
1710                // of a dependency in this window. Used by
1711                // workflow_test_drop_index_during_subscribe_sequencing.
1712                fail::fail_point!("subscribe_before_dispatch");
1713
1714                let response = self
1715                    .call_coordinator(|tx| Command::ExecuteSubscribe {
1716                        df_desc,
1717                        dependency_ids: subscribe_plan.from.depends_on(),
1718                        cluster_id: target_cluster_id,
1719                        replica_id: target_replica,
1720                        conn_id: session.conn_id().clone(),
1721                        session_uuid: session.uuid(),
1722                        read_holds,
1723                        plan: subscribe_plan,
1724                        statement_logging_id: logging.id(),
1725                        tx,
1726                    })
1727                    .await??;
1728                // On success the `Subscribing` response carries the
1729                // coordinator-side logging guard and the protocol layer logs
1730                // the end when the subscribe terminates. On error the
1731                // coordinator logs nothing (`implement_subscribe` creates its
1732                // guard only for the response), so the guard stays armed and
1733                // the caller logs the error.
1734                logging.defuse();
1735                Ok(Some(response))
1736            }
1737            Execution::CopyToS3 {
1738                global_lir_plan,
1739                source_ids,
1740            } => {
1741                let (df_desc, df_meta) = global_lir_plan.unapply();
1742
1743                coord::sequencer::emit_optimizer_notices(
1744                    &*catalog,
1745                    session,
1746                    &df_meta.optimizer_notices,
1747                );
1748
1749                // Extract S3 sink connection info for preflight check
1750                let sink_id = df_desc.sink_id();
1751                let sinks = &df_desc.sink_exports;
1752                if sinks.len() != 1 {
1753                    return Err(AdapterError::Internal(
1754                        "expected exactly one copy to s3 sink".into(),
1755                    ));
1756                }
1757                let (_, sink_desc) = sinks
1758                    .first_key_value()
1759                    .expect("known to be exactly one copy to s3 sink");
1760                let s3_sink_connection = match &sink_desc.connection {
1761                    mz_compute_types::sinks::ComputeSinkConnection::CopyToS3Oneshot(conn) => {
1762                        conn.clone()
1763                    }
1764                    _ => {
1765                        return Err(AdapterError::Internal(
1766                            "expected copy to s3 oneshot sink".into(),
1767                        ));
1768                    }
1769                };
1770
1771                // Perform S3 preflight check in background task (via coordinator).
1772                // This runs slow S3 operations without blocking the coordinator's main task.
1773                session.require_coordinator_completion();
1774                self.call_coordinator(|tx| Command::CopyToPreflight {
1775                    s3_sink_connection,
1776                    sink_id,
1777                    tx,
1778                })
1779                .await??;
1780
1781                // Preflight succeeded, now execute the actual COPY TO dataflow
1782                let watch_set = logging.id().map(|logging_id| {
1783                    WatchSetCreation::new(
1784                        logging_id,
1785                        catalog.state(),
1786                        &input_id_bundle,
1787                        determination.timestamp_context.timestamp_or_default(),
1788                    )
1789                });
1790
1791                // We keep ownership of end-of-execution logging:
1792                // `implement_copy_to` logs nothing, and the final response,
1793                // success or error, comes back through this command and is
1794                // logged by the caller.
1795                let response = self
1796                    .call_coordinator(|tx| Command::ExecuteCopyTo {
1797                        df_desc: Box::new(df_desc),
1798                        compute_instance: target_cluster_id,
1799                        target_replica,
1800                        source_ids,
1801                        conn_id: session.conn_id().clone(),
1802                        watch_set,
1803                        tx,
1804                    })
1805                    .await??;
1806
1807                Ok(Some(response))
1808            }
1809        }
1810    }
1811
1812    /// Determines the timestamp for a query, acquires read holds that ensure the
1813    /// query remains executable at that time, and returns those.
1814    /// The caller is responsible for eventually dropping those read holds.
1815    ///
1816    /// Note: self is taken &mut because of the lazy fetching in `get_compute_instance_client`.
1817    pub(crate) async fn determine_timestamp(
1818        &mut self,
1819        session: &Session,
1820        id_bundle: &CollectionIdBundle,
1821        when: &QueryWhen,
1822        compute_instance: ComputeInstanceId,
1823        timeline_context: &TimelineContext,
1824        oracle_read_ts: Option<Timestamp>,
1825        real_time_recency_ts: Option<Timestamp>,
1826    ) -> Result<(TimestampDetermination, ReadHolds), AdapterError> {
1827        let isolation_level = session.vars().transaction_isolation();
1828
1829        let (read_holds, upper) = self
1830            .acquire_read_holds_and_least_valid_write(id_bundle)
1831            .await
1832            .map_err(|err| {
1833                AdapterError::concurrent_dependency_drop_from_collection_lookup_error(
1834                    err,
1835                    compute_instance,
1836                )
1837            })?;
1838        let (det, read_holds) = <Coordinator as TimestampProvider>::determine_timestamp_for_inner(
1839            session,
1840            id_bundle,
1841            when,
1842            timeline_context,
1843            oracle_read_ts,
1844            real_time_recency_ts,
1845            isolation_level,
1846            read_holds,
1847            upper.clone(),
1848        )?;
1849
1850        session
1851            .metrics()
1852            .by_cluster()
1853            .determine_timestamp(
1854                compute_instance,
1855                det.respond_immediately(),
1856                *isolation_level,
1857            )
1858            .inc();
1859        if !det.respond_immediately()
1860            && isolation_level.is_bounded_staleness()
1861            && real_time_recency_ts.is_none()
1862        {
1863            // Note down the difference between BoundedStaleness and Serializable into a metric.
1864            if let Some(bs_ts) = det.timestamp_context.timestamp() {
1865                let (serializable_det, _tmp_read_holds) =
1866                    <Coordinator as TimestampProvider>::determine_timestamp_for_inner(
1867                        session,
1868                        id_bundle,
1869                        when,
1870                        timeline_context,
1871                        oracle_read_ts,
1872                        real_time_recency_ts,
1873                        &IsolationLevel::Serializable,
1874                        read_holds.clone(),
1875                        upper,
1876                    )?;
1877                if let Some(serializable) = serializable_det.timestamp_context.timestamp() {
1878                    session
1879                        .metrics()
1880                        .by_cluster()
1881                        .timestamp_difference_for_bounded_staleness_ms(compute_instance)
1882                        .observe(f64::cast_lossy(u64::from(
1883                            serializable.saturating_sub(*bs_ts),
1884                        )));
1885                }
1886            }
1887        }
1888
1889        Ok((det, read_holds))
1890    }
1891
1892    fn assert_read_holds_correct(
1893        read_holds: &ReadHolds,
1894        execution: &Execution,
1895        determination: &TimestampDetermination,
1896        target_cluster_id: ClusterId,
1897        in_immediate_multi_stmt_txn: bool,
1898    ) {
1899        // Extract source_imports, index_imports, as_of, and execution_name based on Execution variant
1900        let (source_imports, index_imports, as_of, execution_name): (
1901            Vec<GlobalId>,
1902            Vec<GlobalId>,
1903            Timestamp,
1904            &str,
1905        ) = match execution {
1906            Execution::Peek {
1907                global_lir_plan, ..
1908            } => match global_lir_plan.peek_plan() {
1909                PeekPlan::FastPath(fast_path_plan) => {
1910                    let (sources, indexes) = match fast_path_plan {
1911                        FastPathPlan::Constant(..) => (vec![], vec![]),
1912                        FastPathPlan::PeekExisting(_coll_id, idx_id, ..) => (vec![], vec![*idx_id]),
1913                        FastPathPlan::PeekPersist(global_id, ..) => (vec![*global_id], vec![]),
1914                    };
1915                    (
1916                        sources,
1917                        indexes,
1918                        determination.timestamp_context.timestamp_or_default(),
1919                        "FastPath",
1920                    )
1921                }
1922                PeekPlan::SlowPath(dataflow_plan) => {
1923                    let as_of = dataflow_plan
1924                        .desc
1925                        .as_of
1926                        .clone()
1927                        .expect("dataflow has an as_of")
1928                        .into_element();
1929                    (
1930                        dataflow_plan.desc.source_imports.keys().cloned().collect(),
1931                        dataflow_plan.desc.index_imports.keys().cloned().collect(),
1932                        as_of,
1933                        "SlowPath",
1934                    )
1935                }
1936            },
1937            Execution::CopyToS3 {
1938                global_lir_plan, ..
1939            } => {
1940                let df_desc = global_lir_plan.df_desc();
1941                let as_of = df_desc
1942                    .as_of
1943                    .clone()
1944                    .expect("dataflow has an as_of")
1945                    .into_element();
1946                (
1947                    df_desc.source_imports.keys().cloned().collect(),
1948                    df_desc.index_imports.keys().cloned().collect(),
1949                    as_of,
1950                    "CopyToS3",
1951                )
1952            }
1953            Execution::ExplainPlan { .. }
1954            | Execution::ExplainSubscribe { .. }
1955            | Execution::ExplainPushdown { .. } => {
1956                // No read holds assertions needed for EXPLAIN variants
1957                return;
1958            }
1959            Execution::Subscribe { df_desc, .. } => {
1960                let as_of = df_desc
1961                    .as_of
1962                    .clone()
1963                    .expect("dataflow has an as_of")
1964                    .into_element();
1965                (
1966                    df_desc.source_imports.keys().cloned().collect(),
1967                    df_desc.index_imports.keys().cloned().collect(),
1968                    as_of,
1969                    "Subscribe",
1970                )
1971            }
1972        };
1973
1974        // Assert that we have some read holds for all the imports of the dataflow.
1975        for id in source_imports.iter() {
1976            soft_assert_or_log!(
1977                read_holds.storage_holds.contains_key(id),
1978                "[{}] missing read hold for the source import {}; (in_immediate_multi_stmt_txn: {})",
1979                execution_name,
1980                id,
1981                in_immediate_multi_stmt_txn,
1982            );
1983        }
1984        for id in index_imports.iter() {
1985            soft_assert_or_log!(
1986                read_holds
1987                    .compute_ids()
1988                    .map(|(_instance, coll)| coll)
1989                    .contains(id),
1990                "[{}] missing read hold for the index import {}; (in_immediate_multi_stmt_txn: {})",
1991                execution_name,
1992                id,
1993                in_immediate_multi_stmt_txn,
1994            );
1995        }
1996
1997        // Also check the holds against the as_of.
1998        for (id, h) in read_holds.storage_holds.iter() {
1999            soft_assert_or_log!(
2000                h.since().less_equal(&as_of),
2001                "[{}] storage read hold at {:?} for collection {} is not enough for as_of {:?}, determination: {:?}; (in_immediate_multi_stmt_txn: {})",
2002                execution_name,
2003                h.since(),
2004                id,
2005                as_of,
2006                determination,
2007                in_immediate_multi_stmt_txn,
2008            );
2009        }
2010        for ((instance, id), h) in read_holds.compute_holds.iter() {
2011            soft_assert_eq_or_log!(
2012                *instance,
2013                target_cluster_id,
2014                "[{}] the read hold on {} is on the wrong cluster; (in_immediate_multi_stmt_txn: {})",
2015                execution_name,
2016                id,
2017                in_immediate_multi_stmt_txn,
2018            );
2019            soft_assert_or_log!(
2020                h.since().less_equal(&as_of),
2021                "[{}] compute read hold at {:?} for collection {} is not enough for as_of {:?}, determination: {:?}; (in_immediate_multi_stmt_txn: {})",
2022                execution_name,
2023                h.since(),
2024                id,
2025                as_of,
2026                determination,
2027                in_immediate_multi_stmt_txn,
2028            );
2029        }
2030    }
2031}
2032
2033/// Enum for branching among various execution steps after optimization
2034enum Execution {
2035    Peek {
2036        global_lir_plan: optimize::peek::GlobalLirPlan,
2037        optimization_finished_at: EpochMillis,
2038        plan_insights_optimizer_trace: Option<OptimizerTrace>,
2039        finishing: RowSetFinishing,
2040        copy_to: Option<plan::CopyFormat>,
2041        insights_ctx: Option<Box<PlanInsightsContext>>,
2042    },
2043    Subscribe {
2044        subscribe_plan: SubscribePlan,
2045        df_desc: mz_compute_types::dataflows::LirDataflowDescription,
2046        df_meta: DataflowMetainfo,
2047        optimization_finished_at: EpochMillis,
2048    },
2049    CopyToS3 {
2050        global_lir_plan: optimize::copy_to::GlobalLirPlan,
2051        source_ids: BTreeSet<GlobalId>,
2052    },
2053    ExplainPlan {
2054        df_meta: DataflowMetainfo,
2055        explain_ctx: ExplainPlanContext,
2056        optimizer: optimize::peek::Optimizer,
2057        insights_ctx: Option<Box<PlanInsightsContext>>,
2058    },
2059    ExplainSubscribe {
2060        df_meta: DataflowMetainfo,
2061        explain_ctx: ExplainPlanContext,
2062        sink_id: GlobalId,
2063        cluster_id: ComputeInstanceId,
2064        features: OptimizerFeatures,
2065        equal_bounds: Option<Timestamp>,
2066    },
2067    ExplainPushdown {
2068        imports:
2069            BTreeMap<GlobalId, mz_expr::MfpPlan<mz_compute_types::plan::scalar::LirScalarExpr>>,
2070        determination: TimestampDetermination,
2071    },
2072}
2073
2074/// Evaluates a COPY TO target URI expression and validates it.
2075fn eval_copy_to_uri(
2076    to: HirScalarExpr,
2077    session: &Session,
2078    catalog_state: &CatalogState,
2079) -> Result<Uri, AdapterError> {
2080    let style = ExprPrepOneShot {
2081        logical_time: EvalTime::NotAvailable,
2082        session,
2083        catalog_state,
2084    };
2085    let mut to = to.lower_uncorrelated(catalog_state.system_config())?;
2086    style.prep_scalar_expr(&mut to)?;
2087    let temp_storage = RowArena::new();
2088    let evaled = to.eval(&[], &temp_storage)?;
2089    if evaled == Datum::Null {
2090        coord_bail!("COPY TO target value can not be null");
2091    }
2092    let to_url = match Uri::from_str(evaled.unwrap_str()) {
2093        Ok(url) => {
2094            if url.scheme_str() != Some("s3") && url.scheme_str() != Some("gs") {
2095                coord_bail!("only 's3://...' and 'gs://...' urls are supported as COPY TO target");
2096            }
2097            url
2098        }
2099        Err(e) => coord_bail!("could not parse COPY TO target url: {}", e),
2100    };
2101    Ok(to_url)
2102}
2103
2104/// Generates EXPLAIN PLAN output.
2105async fn explain_plan_inner(
2106    session: &Session,
2107    catalog: &Catalog,
2108    df_meta: DataflowMetainfo,
2109    explain_ctx: ExplainPlanContext,
2110    optimizer: peek::Optimizer,
2111    insights_ctx: Option<Box<PlanInsightsContext>>,
2112) -> Result<Vec<Row>, AdapterError> {
2113    let ExplainPlanContext {
2114        config,
2115        format,
2116        stage,
2117        desc,
2118        optimizer_trace,
2119        ..
2120    } = explain_ctx;
2121
2122    let desc = desc.expect("RelationDesc for SelectPlan in EXPLAIN mode");
2123
2124    let session_catalog = catalog.for_session(session);
2125    let expr_humanizer = {
2126        let transient_items = btreemap! {
2127            optimizer.select_id() => TransientItem::new(
2128                Some(vec![GlobalId::Explain.to_string()]),
2129                Some(desc.iter_names().map(|c| c.to_string()).collect()),
2130            )
2131        };
2132        ExprHumanizerExt::new(transient_items, &session_catalog)
2133    };
2134
2135    let finishing = if optimizer.finishing().is_trivial(desc.arity()) {
2136        None
2137    } else {
2138        Some(optimizer.finishing().clone())
2139    };
2140
2141    let target_cluster = catalog.get_cluster(optimizer.cluster_id());
2142    let features = optimizer.config().features.clone();
2143
2144    let rows = optimizer_trace
2145        .into_rows(
2146            format,
2147            &config,
2148            &features,
2149            &expr_humanizer,
2150            finishing,
2151            Some(target_cluster),
2152            df_meta,
2153            stage,
2154            plan::ExplaineeStatementKind::Select,
2155            insights_ctx,
2156        )
2157        .await?;
2158
2159    Ok(rows)
2160}
2161
2162/// Renders the rows of an `EXPLAIN ... SUBSCRIBE` from its optimizer trace.
2163///
2164/// `features` are the optimizer features the `SUBSCRIBE` was optimized with.
2165async fn explain_subscribe_inner(
2166    session: &Session,
2167    catalog: &Catalog,
2168    df_meta: DataflowMetainfo,
2169    explain_ctx: ExplainPlanContext,
2170    sink_id: GlobalId,
2171    cluster_id: ComputeInstanceId,
2172    features: &OptimizerFeatures,
2173) -> Result<Vec<Row>, AdapterError> {
2174    let ExplainPlanContext {
2175        config,
2176        format,
2177        stage,
2178        desc,
2179        optimizer_trace,
2180        ..
2181    } = explain_ctx;
2182
2183    let session_catalog = catalog.for_session(session);
2184    let expr_humanizer = {
2185        let transient_items = btreemap! {
2186            sink_id => TransientItem::new(
2187                Some(vec![GlobalId::Explain.to_string()]),
2188                desc.map(|d| d.iter_names().map(|c| c.to_string()).collect()),
2189            )
2190        };
2191        ExprHumanizerExt::new(transient_items, &session_catalog)
2192    };
2193
2194    let target_cluster = catalog.get_cluster(cluster_id);
2195
2196    let rows = optimizer_trace
2197        .into_rows(
2198            format,
2199            &config,
2200            features,
2201            &expr_humanizer,
2202            None,
2203            Some(target_cluster),
2204            df_meta,
2205            stage,
2206            plan::ExplaineeStatementKind::Subscribe,
2207            None,
2208        )
2209        .await?;
2210
2211    Ok(rows)
2212}
2213
2214/// Creates a statistics oracle for query optimization.
2215async fn statistics_oracle(
2216    session: &Session,
2217    source_ids: &BTreeSet<GlobalId>,
2218    query_as_of: &Antichain<Timestamp>,
2219    is_oneshot: bool,
2220    system_config: &vars::SystemVars,
2221    storage_collections: &dyn StorageCollections,
2222) -> Result<Box<dyn StatisticsOracle>, AdapterError> {
2223    if !session.vars().enable_session_cardinality_estimates() {
2224        let stats: Box<dyn StatisticsOracle> = Box::new(EmptyStatisticsOracle);
2225        return Ok(stats);
2226    }
2227
2228    let timeout = if is_oneshot {
2229        // TODO(mgree): ideally, we would shorten the timeout even more if we think the query could take the fast path
2230        system_config.optimizer_oneshot_stats_timeout()
2231    } else {
2232        system_config.optimizer_stats_timeout()
2233    };
2234
2235    let cached_stats = mz_ore::future::timeout(
2236        timeout,
2237        CachedStatisticsOracle::new(source_ids, query_as_of, storage_collections),
2238    )
2239    .await;
2240
2241    match cached_stats {
2242        Ok(stats) => Ok(Box::new(stats)),
2243        Err(mz_ore::future::TimeoutError::DeadlineElapsed) => {
2244            warn!(
2245                is_oneshot = is_oneshot,
2246                "optimizer statistics collection timed out after {}ms",
2247                timeout.as_millis()
2248            );
2249
2250            Ok(Box::new(EmptyStatisticsOracle))
2251        }
2252        Err(mz_ore::future::TimeoutError::Inner(e)) => Err(AdapterError::Storage(e)),
2253    }
2254}
2255
2256/// Checks whether we should emit diagnostic
2257/// information associated with reading per-replica sources.
2258///
2259/// If an unrecoverable error is found (today: an untargeted read on a
2260/// cluster with a non-1 number of replicas), return that.  Otherwise,
2261/// return a list of associated notices (today: we always emit exactly
2262/// one notice if there are any per-replica log dependencies and if
2263/// `emit_introspection_query_notice` is set, and none otherwise.)
2264fn check_log_reads(
2265    catalog: &Catalog,
2266    cluster: &Cluster,
2267    source_ids: &BTreeSet<GlobalId>,
2268    target_replica: &mut Option<ReplicaId>,
2269    vars: &SessionVars,
2270) -> Result<impl IntoIterator<Item = AdapterNotice>, AdapterError>
2271where
2272{
2273    let log_names = source_ids
2274        .iter()
2275        .map(|gid| catalog.resolve_item_id(gid))
2276        .flat_map(|item_id| catalog.introspection_dependencies(item_id))
2277        .map(|item_id| catalog.get_entry(&item_id).name().item.clone())
2278        .collect::<Vec<_>>();
2279
2280    if log_names.is_empty() {
2281        return Ok(None);
2282    }
2283
2284    // Reading from log sources on replicated clusters is only allowed if a
2285    // target replica is selected. Otherwise, we have no way of knowing which
2286    // replica we read the introspection data from.
2287    let num_replicas = cluster.replicas().count();
2288    if target_replica.is_none() {
2289        if num_replicas == 1 {
2290            *target_replica = cluster.replicas().map(|r| r.replica_id).next();
2291        } else {
2292            return Err(AdapterError::UntargetedLogRead { log_names });
2293        }
2294    }
2295
2296    // Ensure that logging is initialized for the target replica, lest
2297    // we try to read from a non-existing arrangement.
2298    let replica_id = target_replica.expect("set to `Some` above");
2299    let replica = &cluster.replica(replica_id).expect("Replica must exist");
2300    if !replica.config.compute.logging.enabled() {
2301        return Err(AdapterError::IntrospectionDisabled { log_names });
2302    }
2303
2304    Ok(vars
2305        .emit_introspection_query_notice()
2306        .then_some(AdapterNotice::PerReplicaLogRead { log_names }))
2307}
2308
2309#[derive(Debug)]
2310struct CachedStatisticsOracle {
2311    cache: BTreeMap<GlobalId, usize>,
2312}
2313
2314impl CachedStatisticsOracle {
2315    pub async fn new(
2316        ids: &BTreeSet<GlobalId>,
2317        as_of: &Antichain<Timestamp>,
2318        storage_collections: &dyn StorageCollections,
2319    ) -> Result<Self, StorageError> {
2320        let mut cache = BTreeMap::new();
2321
2322        for id in ids {
2323            let stats = storage_collections.snapshot_stats(*id, as_of.clone()).await;
2324
2325            match stats {
2326                Ok(stats) => {
2327                    cache.insert(*id, stats.num_updates);
2328                }
2329                Err(StorageError::IdentifierMissing(id)) => {
2330                    ::tracing::debug!("no statistics for {id}")
2331                }
2332                Err(e) => return Err(e),
2333            }
2334        }
2335
2336        Ok(Self { cache })
2337    }
2338}
2339
2340impl StatisticsOracle for CachedStatisticsOracle {
2341    fn cardinality_estimate(&self, id: GlobalId) -> Option<usize> {
2342        self.cache.get(&id).map(|estimate| *estimate)
2343    }
2344
2345    fn as_map(&self) -> BTreeMap<GlobalId, usize> {
2346        self.cache.clone()
2347    }
2348}