Skip to main content

mz_adapter/coord/
peek.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Logic and types for creating, executing, and tracking peeks.
11//!
12//! This module determines if a dataflow can be short-cut, by returning constant values
13//! or by reading out of existing arrangements, and implements the appropriate plan.
14
15use std::collections::{BTreeMap, BTreeSet};
16use std::fmt;
17use std::num::NonZeroUsize;
18use std::ops::Deref;
19use std::sync::Arc;
20
21use differential_dataflow::consolidation::consolidate;
22use itertools::Itertools;
23use mz_adapter_types::connection::ConnectionId;
24use mz_cluster_client::ReplicaId;
25use mz_compute_client::controller::PeekNotification;
26use mz_compute_client::protocol::command::PeekTarget;
27use mz_compute_client::protocol::response::PeekResponse;
28use mz_compute_types::ComputeInstanceId;
29use mz_compute_types::dataflows::{DataflowDescription, IndexImport};
30use mz_controller_types::ClusterId;
31use mz_expr::explain::{HumanizedExplain, HumanizerMode, fmt_text_constant_rows};
32use mz_expr::row::RowCollection;
33use mz_expr::{
34    EvalError, Id, MirRelationExpr, MirScalarExpr, OptimizedMirRelationExpr, RowSetFinishing,
35    RowSetFinishingIncremental, permutation_for_arrangement,
36};
37use mz_ore::cast::CastFrom;
38use mz_ore::collections::CollectionExt;
39use mz_ore::soft_assert_eq_or_log;
40use mz_ore::str::{StrExt, separated};
41use mz_ore::task;
42use mz_ore::tracing::OpenTelemetryContext;
43use mz_persist_client::Schemas;
44use mz_persist_types::codec_impls::UnitSchema;
45use mz_repr::explain::text::DisplayText;
46use mz_repr::explain::{CompactScalars, IndexUsageType, PlanRenderingContext, UsedIndexes};
47use mz_repr::{
48    Diff, GlobalId, IntoRowIterator, RelationDesc, Row, RowIterator, SqlRelationType,
49    preserves_order,
50};
51use mz_storage_types::sources::SourceData;
52use serde::{Deserialize, Serialize};
53use timely::progress::Antichain;
54use tokio::sync::oneshot;
55use tracing::{Instrument, Span};
56use uuid::Uuid;
57
58use crate::active_compute_sink::{ActiveComputeSink, ActiveCopyTo};
59use crate::coord::timestamp_selection::TimestampDetermination;
60use crate::optimize::OptimizerError;
61use crate::statement_logging::WatchSetCreation;
62use crate::statement_logging::{StatementEndedExecutionReason, StatementExecutionStrategy};
63use crate::{AdapterError, ExecuteContextGuard, ExecuteResponse};
64
65/// A peek is a request to read data from a maintained arrangement.
66#[derive(Debug)]
67pub(crate) struct PendingPeek {
68    /// The connection that initiated the peek.
69    pub(crate) conn_id: ConnectionId,
70    /// The cluster that the peek is being executed on.
71    pub(crate) cluster_id: ClusterId,
72    /// All `GlobalId`s that the peek depend on.
73    pub(crate) depends_on: BTreeSet<GlobalId>,
74    /// Context about the execute that produced this peek,
75    /// needed by the coordinator for retiring it.
76    pub(crate) ctx_extra: ExecuteContextGuard,
77    /// Is this a fast-path peek, i.e. one that doesn't require a dataflow?
78    pub(crate) is_fast_path: bool,
79}
80
81/// The response from a `Peek`, with row multiplicities represented in unary.
82///
83/// Note that each `Peek` expects to generate exactly one `PeekResponse`, i.e.
84/// we expect a 1:1 contract between `Peek` and `PeekResponseUnary`.
85#[derive(Debug)]
86pub enum PeekResponseUnary {
87    Rows(Box<dyn RowIterator + Send + Sync>),
88    Error(AdapterError),
89    Canceled,
90    /// A dependency was dropped during execution.
91    DependencyDropped(DroppedDependency),
92}
93
94/// A dependency that was dropped while a peek or subscribe was in flight.
95///
96/// The `name` fields hold the bare name (e.g. `db.schema.t` or `c`); `Display`
97/// applies SQL identifier quoting to produce `relation "db.schema.t"` or
98/// `cluster "c"` for direct use in error wording.
99#[derive(Clone, Debug)]
100pub enum DroppedDependency {
101    Relation { name: String },
102    Cluster { name: String },
103}
104
105impl fmt::Display for DroppedDependency {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            Self::Relation { name } => write!(f, "relation {}", name.quoted()),
109            Self::Cluster { name } => write!(f, "cluster {}", name.quoted()),
110        }
111    }
112}
113
114impl DroppedDependency {
115    /// User-facing error for a query (peek or subscribe) that could not finish
116    /// because this dependency was dropped mid-flight.
117    pub fn query_terminated_error(&self) -> String {
118        format!("query could not complete because {self} was dropped")
119    }
120
121    /// Convert this dropped dependency into an [`AdapterError::ConcurrentDependencyDrop`].
122    pub fn to_concurrent_dependency_drop(&self) -> AdapterError {
123        let (kind, name) = match self {
124            Self::Relation { name } => ("relation", name.clone()),
125            Self::Cluster { name } => ("cluster", name.clone()),
126        };
127        AdapterError::ConcurrentDependencyDrop {
128            dependency_kind: kind,
129            dependency_id: name,
130        }
131    }
132}
133
134#[derive(Clone, Debug)]
135pub struct PeekDataflowPlan {
136    pub(crate) desc: DataflowDescription<mz_compute_types::plan::LirRelationExpr, ()>,
137    pub(crate) id: GlobalId,
138    key: Vec<MirScalarExpr>,
139    permutation: Vec<usize>,
140    thinned_arity: usize,
141}
142
143impl PeekDataflowPlan {
144    pub fn new(
145        desc: DataflowDescription<mz_compute_types::plan::LirRelationExpr, ()>,
146        id: GlobalId,
147        typ: &SqlRelationType,
148    ) -> Self {
149        let arity = typ.arity();
150        let key = typ
151            .default_key()
152            .into_iter()
153            .map(MirScalarExpr::column)
154            .collect::<Vec<_>>();
155        let (permutation, thinning) = permutation_for_arrangement(&key, arity);
156        Self {
157            desc,
158            id,
159            key,
160            permutation,
161            thinned_arity: thinning.len(),
162        }
163    }
164}
165
166#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd)]
167pub enum FastPathPlan {
168    /// The view evaluates to a constant result that can be returned.
169    ///
170    /// The [SqlRelationType] is unnecessary for evaluating the constant result but
171    /// may be helpful when printing out an explanation.
172    Constant(Result<Vec<(Row, Diff)>, EvalError>, SqlRelationType),
173    /// The view can be read out of an existing arrangement.
174    /// (coll_id, idx_id, values to look up, mfp to apply)
175    PeekExisting(GlobalId, GlobalId, Option<Vec<Row>>, mz_expr::SafeMfpPlan),
176    /// The view can be read directly out of Persist.
177    PeekPersist(GlobalId, Option<Row>, mz_expr::SafeMfpPlan),
178}
179
180impl<'a, T: 'a> DisplayText<PlanRenderingContext<'a, T>> for FastPathPlan {
181    fn fmt_text(
182        &self,
183        f: &mut fmt::Formatter<'_>,
184        ctx: &mut PlanRenderingContext<'a, T>,
185    ) -> fmt::Result {
186        if ctx.config.verbose_syntax {
187            self.fmt_verbose_text(f, ctx)
188        } else {
189            self.fmt_default_text(f, ctx)
190        }
191    }
192}
193
194impl FastPathPlan {
195    pub fn fmt_default_text<'a, T>(
196        &self,
197        f: &mut fmt::Formatter<'_>,
198        ctx: &mut PlanRenderingContext<'a, T>,
199    ) -> fmt::Result {
200        let mode = HumanizedExplain::new(ctx.config.redacted);
201
202        match self {
203            FastPathPlan::Constant(rows, _) => {
204                write!(f, "{}→Constant ", ctx.indent)?;
205
206                match rows {
207                    Ok(rows) => writeln!(f, "({} rows)", rows.len())?,
208                    Err(err) => {
209                        if mode.redacted() {
210                            writeln!(f, "(error: █)")?;
211                        } else {
212                            writeln!(f, "(error: {})", err.to_string().quoted(),)?;
213                        }
214                    }
215                }
216            }
217            FastPathPlan::PeekExisting(coll_id, idx_id, literal_constraints, mfp) => {
218                let coll = ctx
219                    .humanizer
220                    .humanize_id(*coll_id)
221                    .unwrap_or_else(|| coll_id.to_string());
222                let idx = ctx
223                    .humanizer
224                    .humanize_id(*idx_id)
225                    .unwrap_or_else(|| idx_id.to_string());
226                writeln!(f, "{}→Map/Filter/Project", ctx.indent)?;
227                ctx.indent.set();
228
229                ctx.indent += 1;
230
231                mode.expr(mfp.deref(), None).fmt_default_text(f, ctx)?;
232                let printed = !mfp.expressions.is_empty() || !mfp.predicates.is_empty();
233
234                if printed {
235                    ctx.indent += 1;
236                }
237                if let Some(literal_constraints) = literal_constraints {
238                    writeln!(f, "{}→Index Lookup on {coll} (using {idx})", ctx.indent)?;
239                    ctx.indent += 1;
240                    let values = separated("; ", mode.seq(literal_constraints, None));
241                    writeln!(f, "{}Lookup values: {values}", ctx.indent)?;
242                } else {
243                    writeln!(f, "{}→Indexed {coll} (using {idx})", ctx.indent)?;
244                }
245
246                ctx.indent.reset();
247            }
248            FastPathPlan::PeekPersist(global_id, literal_constraint, mfp) => {
249                let coll = ctx
250                    .humanizer
251                    .humanize_id(*global_id)
252                    .unwrap_or_else(|| global_id.to_string());
253                writeln!(f, "{}→Map/Filter/Project", ctx.indent)?;
254                ctx.indent.set();
255
256                ctx.indent += 1;
257
258                mode.expr(mfp.deref(), None).fmt_default_text(f, ctx)?;
259                let printed = !mfp.expressions.is_empty() || !mfp.predicates.is_empty();
260
261                if printed {
262                    ctx.indent += 1;
263                }
264                if let Some(literal_constraint) = literal_constraint {
265                    writeln!(f, "{}→ReadStorage Lookup on {coll}", ctx.indent)?;
266                    ctx.indent += 1;
267                    let value = mode.expr(literal_constraint, None);
268                    writeln!(f, "{}Lookup value: {value}", ctx.indent)?;
269                } else {
270                    writeln!(f, "{}→ReadStorage {coll}", ctx.indent)?;
271                }
272
273                ctx.indent.reset();
274            }
275        }
276
277        Ok(())
278    }
279
280    pub fn fmt_verbose_text<'a, T>(
281        &self,
282        f: &mut fmt::Formatter<'_>,
283        ctx: &mut PlanRenderingContext<'a, T>,
284    ) -> fmt::Result {
285        let redacted = ctx.config.redacted;
286        let mode = HumanizedExplain::new(redacted);
287
288        // TODO(aalexandrov): factor out common PeekExisting and PeekPersist
289        // code.
290        match self {
291            FastPathPlan::Constant(Ok(rows), _) => {
292                if !rows.is_empty() {
293                    writeln!(f, "{}Constant", ctx.indent)?;
294                    *ctx.as_mut() += 1;
295                    fmt_text_constant_rows(
296                        f,
297                        rows.iter().map(|(row, diff)| (row, diff)),
298                        ctx.as_mut(),
299                        redacted,
300                    )?;
301                    *ctx.as_mut() -= 1;
302                } else {
303                    writeln!(f, "{}Constant <empty>", ctx.as_mut())?;
304                }
305                Ok(())
306            }
307            FastPathPlan::Constant(Err(err), _) => {
308                if redacted {
309                    writeln!(f, "{}Error █", ctx.as_mut())
310                } else {
311                    writeln!(f, "{}Error {}", ctx.as_mut(), err.to_string().escaped())
312                }
313            }
314            FastPathPlan::PeekExisting(coll_id, idx_id, literal_constraints, mfp) => {
315                ctx.as_mut().set();
316                let (map, filter, project) = mfp.as_map_filter_project();
317
318                let cols = if !ctx.config.humanized_exprs {
319                    None
320                } else if let Some(cols) = ctx.humanizer.column_names_for_id(*idx_id) {
321                    // FIXME: account for thinning and permutation
322                    // See mz_expr::permutation_for_arrangement
323                    // See permute_oneshot_mfp_around_index
324                    let cols = itertools::chain(
325                        cols.iter().cloned(),
326                        std::iter::repeat(String::new()).take(map.len()),
327                    )
328                    .collect();
329                    Some(cols)
330                } else {
331                    None
332                };
333
334                if project.len() != mfp.input_arity + map.len()
335                    || !project.iter().enumerate().all(|(i, o)| i == *o)
336                {
337                    let outputs = mode.seq(&project, cols.as_ref());
338                    let outputs = CompactScalars(outputs);
339                    writeln!(f, "{}Project ({})", ctx.as_mut(), outputs)?;
340                    *ctx.as_mut() += 1;
341                }
342                if !filter.is_empty() {
343                    let predicates = separated(" AND ", mode.seq(&filter, cols.as_ref()));
344                    writeln!(f, "{}Filter {}", ctx.as_mut(), predicates)?;
345                    *ctx.as_mut() += 1;
346                }
347                if !map.is_empty() {
348                    let scalars = mode.seq(&map, cols.as_ref());
349                    let scalars = CompactScalars(scalars);
350                    writeln!(f, "{}Map ({})", ctx.as_mut(), scalars)?;
351                    *ctx.as_mut() += 1;
352                }
353                MirRelationExpr::fmt_indexed_filter(
354                    f,
355                    ctx,
356                    coll_id,
357                    idx_id,
358                    literal_constraints.clone(),
359                    None,
360                )?;
361                writeln!(f)?;
362                ctx.as_mut().reset();
363                Ok(())
364            }
365            FastPathPlan::PeekPersist(gid, literal_constraint, mfp) => {
366                ctx.as_mut().set();
367                let (map, filter, project) = mfp.as_map_filter_project();
368
369                let cols = if !ctx.config.humanized_exprs {
370                    None
371                } else if let Some(cols) = ctx.humanizer.column_names_for_id(*gid) {
372                    let cols = itertools::chain(
373                        cols.iter().cloned(),
374                        std::iter::repeat(String::new()).take(map.len()),
375                    )
376                    .collect::<Vec<_>>();
377                    Some(cols)
378                } else {
379                    None
380                };
381
382                if project.len() != mfp.input_arity + map.len()
383                    || !project.iter().enumerate().all(|(i, o)| i == *o)
384                {
385                    let outputs = mode.seq(&project, cols.as_ref());
386                    let outputs = CompactScalars(outputs);
387                    writeln!(f, "{}Project ({})", ctx.as_mut(), outputs)?;
388                    *ctx.as_mut() += 1;
389                }
390                if !filter.is_empty() {
391                    let predicates = separated(" AND ", mode.seq(&filter, cols.as_ref()));
392                    writeln!(f, "{}Filter {}", ctx.as_mut(), predicates)?;
393                    *ctx.as_mut() += 1;
394                }
395                if !map.is_empty() {
396                    let scalars = mode.seq(&map, cols.as_ref());
397                    let scalars = CompactScalars(scalars);
398                    writeln!(f, "{}Map ({})", ctx.as_mut(), scalars)?;
399                    *ctx.as_mut() += 1;
400                }
401                let human_id = ctx
402                    .humanizer
403                    .humanize_id(*gid)
404                    .unwrap_or_else(|| gid.to_string());
405                write!(f, "{}PeekPersist {human_id}", ctx.as_mut())?;
406                if let Some(literal) = literal_constraint {
407                    let value = mode.expr(literal, None);
408                    writeln!(f, " [value={}]", value)?;
409                } else {
410                    writeln!(f, "")?;
411                }
412                ctx.as_mut().reset();
413                Ok(())
414            }
415        }?;
416        Ok(())
417    }
418}
419
420#[derive(Debug)]
421pub struct PlannedPeek {
422    pub plan: PeekPlan,
423    pub determination: TimestampDetermination,
424    pub conn_id: ConnectionId,
425    /// The result type _after_ reading out of the "source" and applying any
426    /// [MapFilterProject](mz_expr::MapFilterProject), but _before_ applying a
427    /// [RowSetFinishing].
428    ///
429    /// This is _the_ `result_type` as far as compute is concerned and further
430    /// changes through projections happen purely in the adapter.
431    pub intermediate_result_type: SqlRelationType,
432    pub source_arity: usize,
433    pub source_ids: BTreeSet<GlobalId>,
434}
435
436/// Possible ways in which the coordinator could produce the result for a goal view.
437#[derive(Clone, Debug)]
438pub enum PeekPlan {
439    FastPath(FastPathPlan),
440    /// The view must be installed as a dataflow and then read.
441    SlowPath(PeekDataflowPlan),
442}
443
444/// Convert `mfp` to an executable, non-temporal plan.
445/// It should be non-temporal, as OneShot preparation populates `mz_now`.
446///
447/// If the `mfp` can't be converted into a non-temporal plan, this returns an _internal_ error.
448fn mfp_to_safe_plan(
449    mfp: mz_expr::MapFilterProject,
450) -> Result<mz_expr::SafeMfpPlan, OptimizerError> {
451    mfp.into_plan()
452        .map_err(OptimizerError::InternalUnsafeMfpPlan)?
453        .into_nontemporal()
454        .map_err(|e| OptimizerError::InternalUnsafeMfpPlan(format!("{:?}", e)))
455}
456
457/// If it can't convert `mfp` into a `SafeMfpPlan`, this returns an _internal_ error.
458fn permute_oneshot_mfp_around_index(
459    mfp: mz_expr::MapFilterProject,
460    key: &[MirScalarExpr],
461) -> Result<mz_expr::SafeMfpPlan, OptimizerError> {
462    let input_arity = mfp.input_arity;
463    let mut safe_mfp = mfp_to_safe_plan(mfp)?;
464    let (permute, thinning) = permutation_for_arrangement(key, input_arity);
465    safe_mfp.permute_fn(|c| permute[c], key.len() + thinning.len());
466    Ok(safe_mfp)
467}
468
469/// Determine if the dataflow plan can be implemented without an actual dataflow.
470///
471/// If the optimized plan is a `Constant` or a `Get` of a maintained arrangement,
472/// we can avoid building a dataflow (and either just return the results, or peek
473/// out of the arrangement, respectively).
474pub fn create_fast_path_plan(
475    dataflow_plan: &mut DataflowDescription<OptimizedMirRelationExpr>,
476    view_id: GlobalId,
477    finishing: Option<&RowSetFinishing>,
478    persist_fast_path_limit: usize,
479    persist_fast_path_order: bool,
480) -> Result<Option<FastPathPlan>, OptimizerError> {
481    // At this point, `dataflow_plan` contains our best optimized dataflow.
482    // We will check the plan to see if there is a fast path to escape full dataflow construction.
483
484    // We need to restrict ourselves to settings where the inserted transient view is the first thing
485    // to build (no dependent views). There is likely an index to build as well, but we may not be sure.
486    if dataflow_plan.objects_to_build.len() >= 1 && dataflow_plan.objects_to_build[0].id == view_id
487    {
488        let mut mir = &*dataflow_plan.objects_to_build[0].plan.as_inner_mut();
489        if let Some((rows, found_typ)) = mir.as_const() {
490            // In the case of a constant, we can return the result now.
491            let plan = FastPathPlan::Constant(
492                rows.clone(),
493                mz_repr::SqlRelationType::from_repr(found_typ),
494            );
495            return Ok(Some(plan));
496        } else {
497            // If there is a TopK that would be completely covered by the finishing, then jump
498            // through the TopK.
499            if let MirRelationExpr::TopK {
500                input,
501                group_key,
502                order_key,
503                limit,
504                offset,
505                monotonic: _,
506                expected_group_size: _,
507            } = mir
508            {
509                if let Some(finishing) = finishing {
510                    if group_key.is_empty() && *order_key == finishing.order_by && *offset == 0 {
511                        // The following is roughly `limit >= finishing.limit + finishing.offset`,
512                        // but with Options.
513                        let finishing_limits_at_least_as_topk = match (limit, finishing.limit) {
514                            (None, _) => true,
515                            (Some(..), None) => false,
516                            (Some(topk_limit), Some(finishing_limit)) => {
517                                if let Some(l) = topk_limit.as_literal_int64() {
518                                    i128::cast_from(l)
519                                        >= i128::cast_from(*finishing_limit)
520                                            + i128::cast_from(finishing.offset)
521                                } else {
522                                    false
523                                }
524                            }
525                        };
526                        if finishing_limits_at_least_as_topk {
527                            mir = input;
528                        }
529                    }
530                }
531            }
532            // In the case of a linear operator around an indexed view, we
533            // can skip creating a dataflow and instead pull all the rows in
534            // index and apply the linear operator against them.
535            let (mfp, mir) = mz_expr::MapFilterProject::extract_from_expression(mir);
536            match mir {
537                MirRelationExpr::Get {
538                    id: Id::Global(get_id),
539                    typ: repr_typ,
540                    ..
541                } => {
542                    // Just grab any arrangement if an arrangement exists
543                    for (index_id, IndexImport { desc, .. }) in dataflow_plan.index_imports.iter() {
544                        if desc.on_id == *get_id {
545                            return Ok(Some(FastPathPlan::PeekExisting(
546                                *get_id,
547                                *index_id,
548                                None,
549                                permute_oneshot_mfp_around_index(mfp, &desc.key)?,
550                            )));
551                        }
552                    }
553
554                    // If there is no arrangement, consider peeking the persist shard directly.
555                    // Generally, we consider a persist peek when the query can definitely be satisfied
556                    // by scanning through a small, constant number of Persist key-values.
557                    let safe_mfp = mfp_to_safe_plan(mfp)?;
558                    let (_maps, filters, projection) = safe_mfp.as_map_filter_project();
559
560                    let persist_fast_path_order_relation_typ = if persist_fast_path_order {
561                        Some(
562                            dataflow_plan
563                                .source_imports
564                                .get(get_id)
565                                .expect("Get's ID is also imported")
566                                .desc
567                                .typ
568                                .clone(),
569                        )
570                    } else {
571                        None
572                    };
573
574                    let literal_constraint =
575                        if let Some(relation_typ) = &persist_fast_path_order_relation_typ {
576                            let mut row = Row::default();
577                            let mut packer = row.packer();
578                            for (idx, col) in relation_typ.column_types.iter().enumerate() {
579                                if !preserves_order(&col.scalar_type) {
580                                    break;
581                                }
582                                let col_expr = MirScalarExpr::column(idx);
583
584                                let Some((literal, _)) = filters
585                                    .iter()
586                                    .filter_map(|f| f.expr_eq_literal(&col_expr))
587                                    .next()
588                                else {
589                                    break;
590                                };
591                                packer.extend_by_row(&literal);
592                            }
593                            if row.is_empty() { None } else { Some(row) }
594                        } else {
595                            None
596                        };
597
598                    let finish_ok = match &finishing {
599                        None => false,
600                        Some(RowSetFinishing {
601                            order_by,
602                            limit,
603                            offset,
604                            ..
605                        }) => {
606                            let order_ok =
607                                if let Some(relation_typ) = &persist_fast_path_order_relation_typ {
608                                    order_by.iter().enumerate().all(|(idx, order)| {
609                                        // Map the ordering column back to the column in the source data.
610                                        // (If it's not one of the input columns, we can't make any guarantees.)
611                                        let column_idx = projection[order.column];
612                                        if column_idx >= safe_mfp.input_arity {
613                                            return false;
614                                        }
615                                        let column_type = &relation_typ.column_types[column_idx];
616                                        let index_ok = idx == column_idx;
617                                        let nulls_ok = !column_type.nullable || order.nulls_last;
618                                        let asc_ok = !order.desc;
619                                        let type_ok = preserves_order(&column_type.scalar_type);
620                                        index_ok && nulls_ok && asc_ok && type_ok
621                                    })
622                                } else {
623                                    order_by.is_empty()
624                                };
625                            let limit_ok = limit.map_or(false, |l| {
626                                usize::cast_from(l) + *offset < persist_fast_path_limit
627                            });
628                            order_ok && limit_ok
629                        }
630                    };
631
632                    let key_constraint = if let Some(literal) = &literal_constraint {
633                        let prefix_len = literal.iter().count();
634                        repr_typ
635                            .keys
636                            .iter()
637                            .any(|k| k.iter().all(|idx| *idx < prefix_len))
638                    } else {
639                        false
640                    };
641
642                    // We can generate a persist peek when:
643                    // - We have a literal constraint that includes an entire key (so we'll return at most one value)
644                    // - We can return the first N key values (no filters, small limit, consistent order)
645                    if key_constraint || (filters.is_empty() && finish_ok) {
646                        return Ok(Some(FastPathPlan::PeekPersist(
647                            *get_id,
648                            literal_constraint,
649                            safe_mfp,
650                        )));
651                    }
652                }
653                MirRelationExpr::Join { implementation, .. } => {
654                    if let mz_expr::JoinImplementation::IndexedFilter(coll_id, idx_id, key, vals) =
655                        implementation
656                    {
657                        return Ok(Some(FastPathPlan::PeekExisting(
658                            *coll_id,
659                            *idx_id,
660                            Some(vals.clone()),
661                            permute_oneshot_mfp_around_index(mfp, key)?,
662                        )));
663                    }
664                }
665                // nothing can be done for non-trivial expressions.
666                _ => {}
667            }
668        }
669    }
670    Ok(None)
671}
672
673impl FastPathPlan {
674    pub fn used_indexes(&self, finishing: Option<&RowSetFinishing>) -> UsedIndexes {
675        match self {
676            FastPathPlan::Constant(..) => UsedIndexes::default(),
677            FastPathPlan::PeekExisting(_coll_id, idx_id, literal_constraints, _mfp) => {
678                if literal_constraints.is_some() {
679                    UsedIndexes::new([(*idx_id, vec![IndexUsageType::Lookup(*idx_id)])].into())
680                } else if finishing.map_or(false, |f| f.limit.is_some() && f.order_by.is_empty()) {
681                    UsedIndexes::new([(*idx_id, vec![IndexUsageType::FastPathLimit])].into())
682                } else {
683                    UsedIndexes::new([(*idx_id, vec![IndexUsageType::FullScan])].into())
684                }
685            }
686            FastPathPlan::PeekPersist(..) => UsedIndexes::default(),
687        }
688    }
689}
690
691impl crate::coord::Coordinator {
692    /// Implements a peek plan produced by `create_plan` above.
693    ///
694    /// On success this takes the contents of `ctx_extra`, the
695    /// statement-logging guard: a constant peek is retired immediately, and a
696    /// streaming peek moves them into `pending_peeks` for
697    /// `handle_peek_notification` to retire. On an error return the contents
698    /// are left intact and the caller must take ownership of them: `retire`
699    /// if the caller is the sole end-logger, `defuse` if something else logs
700    /// the error end. Dropping the guard armed emits a spurious `Aborted`,
701    /// double-ending the statement.
702    #[mz_ore::instrument(level = "debug")]
703    pub async fn implement_peek_plan(
704        &mut self,
705        ctx_extra: &mut ExecuteContextGuard,
706        plan: PlannedPeek,
707        finishing: RowSetFinishing,
708        compute_instance: ComputeInstanceId,
709        target_replica: Option<ReplicaId>,
710        max_result_size: u64,
711        max_returned_query_size: Option<u64>,
712    ) -> Result<ExecuteResponse, AdapterError> {
713        let PlannedPeek {
714            plan: fast_path,
715            determination,
716            conn_id,
717            intermediate_result_type,
718            source_arity,
719            source_ids,
720        } = plan;
721
722        // If the dataflow optimizes to a constant expression, we can immediately return the result.
723        if let PeekPlan::FastPath(FastPathPlan::Constant(rows, _)) = fast_path {
724            let mut rows = match rows {
725                Ok(rows) => rows,
726                Err(e) => return Err(e.into()),
727            };
728            // Consolidate down the results to get correct totals.
729            consolidate(&mut rows);
730
731            let mut results = Vec::new();
732            for (row, count) in rows {
733                if count.is_negative() {
734                    Err(EvalError::InvalidParameterValue(
735                        format!("Negative multiplicity in constant result: {}", count).into(),
736                    ))?
737                };
738                if count.is_positive() {
739                    let count = usize::cast_from(
740                        u64::try_from(count.into_inner())
741                            .expect("known to be positive from check above"),
742                    );
743                    results.push((
744                        row,
745                        NonZeroUsize::new(count).expect("known to be non-zero from check above"),
746                    ));
747                }
748            }
749            let row_collection = RowCollection::new(results, &finishing.order_by);
750            let duration_histogram = self.metrics.row_set_finishing_seconds();
751
752            let (ret, reason) = match finishing.finish(
753                row_collection,
754                max_result_size,
755                max_returned_query_size,
756                &duration_histogram,
757            ) {
758                Ok((rows, row_size_bytes)) => {
759                    let result_size = u64::cast_from(row_size_bytes);
760                    let rows_returned = u64::cast_from(rows.count());
761                    (
762                        Ok(Self::send_immediate_rows(rows)),
763                        StatementEndedExecutionReason::Success {
764                            result_size: Some(result_size),
765                            rows_returned: Some(rows_returned),
766                            execution_strategy: Some(StatementExecutionStrategy::Constant),
767                        },
768                    )
769                }
770                Err(error) => (
771                    Err(AdapterError::ResultSize(error.clone())),
772                    StatementEndedExecutionReason::Errored { error },
773                ),
774            };
775            self.retire_execution(reason, std::mem::take(ctx_extra).defuse());
776            return ret;
777        }
778
779        let timestamp = determination.timestamp_context.timestamp_or_default();
780        if let Some(id) = ctx_extra.contents() {
781            self.set_statement_execution_timestamp(id, timestamp)
782        }
783
784        // The remaining cases are a peek into a maintained arrangement, or building a dataflow.
785        // In both cases we will want to peek, and the main difference is that we might want to
786        // build a dataflow and drop it once the peek is issued. The peeks are also constructed
787        // differently.
788
789        // Acquire a read hold for the peek target so its `since` cannot advance past
790        // `timestamp` before `compute.peek()` runs. On the slow path we ship the dataflow
791        // first: the implied hold from `create_dataflow` pins the new collection's `since`
792        // at `as_of`, so the subsequent `acquire_read_hold` lands at `as_of <= timestamp`.
793        let (peek_command, drop_dataflow, is_fast_path, peek_target, strategy, read_hold) =
794            match fast_path {
795                PeekPlan::FastPath(FastPathPlan::PeekExisting(
796                    _coll_id,
797                    idx_id,
798                    literal_constraints,
799                    map_filter_project,
800                )) => {
801                    let read_hold = self
802                        .controller
803                        .compute
804                        .acquire_read_hold(compute_instance, idx_id)
805                        .map_err(
806                            AdapterError::concurrent_dependency_drop_from_collection_update_error,
807                        )?;
808                    (
809                        (literal_constraints, timestamp, map_filter_project),
810                        None,
811                        true,
812                        PeekTarget::Index { id: idx_id },
813                        StatementExecutionStrategy::FastPath,
814                        read_hold,
815                    )
816                }
817                PeekPlan::FastPath(FastPathPlan::PeekPersist(
818                    coll_id,
819                    literal_constraint,
820                    map_filter_project,
821                )) => {
822                    let peek_command = (
823                        literal_constraint.map(|r| vec![r]),
824                        timestamp,
825                        map_filter_project,
826                    );
827                    let metadata = self
828                        .controller
829                        .storage
830                        .collection_metadata(coll_id)
831                        .expect("storage collection for fast-path peek")
832                        .clone();
833                    let read_hold = self
834                        .controller
835                        .storage_collections
836                        .acquire_read_holds(vec![coll_id])
837                        .map_err(AdapterError::concurrent_dependency_drop_from_collection_missing)?
838                        .into_element();
839                    (
840                        peek_command,
841                        None,
842                        true,
843                        PeekTarget::Persist {
844                            id: coll_id,
845                            metadata,
846                        },
847                        StatementExecutionStrategy::PersistFastPath,
848                        read_hold,
849                    )
850                }
851                PeekPlan::SlowPath(PeekDataflowPlan {
852                    desc: dataflow,
853                    // n.b. this index_id identifies a transient index the
854                    // caller created, so it is guaranteed to be on
855                    // `compute_instance`.
856                    id: index_id,
857                    key: index_key,
858                    permutation: index_permutation,
859                    thinned_arity: index_thinned_arity,
860                }) => {
861                    // The slow-path peek read-hold strategy below acquires a hold for
862                    // `index_id` only. That is sufficient today because slow-path peek
863                    // dataflows have a single export equal to `index_id`. If we ever
864                    // ship multi-output dataflows on this path, the hold acquisition
865                    // needs to be revisited.
866                    let exports: Vec<GlobalId> = dataflow.export_ids().collect();
867                    soft_assert_eq_or_log!(
868                        exports.as_slice(),
869                        &[index_id],
870                        "slow-path peek dataflow must export exactly [index_id]",
871                    );
872                    if exports.as_slice() != [index_id] {
873                        return Err(AdapterError::internal(
874                            "peek error",
875                            format!(
876                                "slow-path peek dataflow exports {exports:?}, expected [{index_id}]",
877                            ),
878                        ));
879                    }
880
881                    // Very important: actually create the dataflow (here, so we can destructure).
882                    self.controller
883                        .compute
884                        .create_dataflow(compute_instance, dataflow, None)
885                        .map_err(
886                            AdapterError::concurrent_dependency_drop_from_dataflow_creation_error,
887                        )?;
888
889                    // Acquire a bare hold on the freshly-shipped index. On failure we must
890                    // drop the dataflow ourselves, otherwise it leaks.
891                    let acquire_result = self
892                        .controller
893                        .compute
894                        .acquire_read_hold(compute_instance, index_id)
895                        .map_err(
896                            AdapterError::concurrent_dependency_drop_from_collection_update_error,
897                        );
898                    let read_hold = match acquire_result {
899                        Ok(hold) => hold,
900                        Err(e) => {
901                            self.drop_compute_collections(vec![(compute_instance, index_id)]);
902                            return Err(e);
903                        }
904                    };
905
906                    // Create an identity MFP operator.
907                    let mut map_filter_project = mz_expr::MapFilterProject::new(source_arity);
908                    map_filter_project.permute_fn(
909                        |c| index_permutation[c],
910                        index_key.len() + index_thinned_arity,
911                    );
912                    let map_filter_project = mfp_to_safe_plan(map_filter_project)?;
913
914                    (
915                        (None, timestamp, map_filter_project),
916                        Some(index_id),
917                        false,
918                        PeekTarget::Index { id: index_id },
919                        StatementExecutionStrategy::Standard,
920                        read_hold,
921                    )
922                }
923                PeekPlan::FastPath(_) => {
924                    unreachable!()
925                }
926            };
927
928        // Endpoints for sending and receiving peek responses.
929        let (rows_tx, rows_rx) = tokio::sync::oneshot::channel();
930
931        // Generate unique UUID. Guaranteed to be unique to all pending peeks, there's an very
932        // small but unlikely chance that it's not unique to completed peeks.
933        let mut uuid = Uuid::new_v4();
934        while self.pending_peeks.contains_key(&uuid) {
935            uuid = Uuid::new_v4();
936        }
937
938        let (literal_constraints, timestamp, map_filter_project) = peek_command;
939
940        // At this stage we don't know column names for the result because we
941        // only know the peek's result type as a bare SqlRelationType.
942        let peek_result_column_names =
943            (0..intermediate_result_type.arity()).map(|i| format!("peek_{i}"));
944        let peek_result_desc =
945            RelationDesc::new(intermediate_result_type, peek_result_column_names);
946
947        let peek_result = self
948            .controller
949            .compute
950            .peek(
951                compute_instance,
952                peek_target,
953                literal_constraints,
954                uuid,
955                timestamp,
956                peek_result_desc,
957                finishing.clone(),
958                map_filter_project,
959                read_hold,
960                target_replica,
961                rows_tx,
962            )
963            .map_err(AdapterError::concurrent_dependency_drop_from_peek_error);
964        if let Err(e) = peek_result {
965            // If we shipped a transient dataflow above, drop it now to avoid leaking it.
966            if let Some(index_id) = drop_dataflow {
967                self.drop_compute_collections(vec![(compute_instance, index_id)]);
968            }
969            return Err(e);
970        }
971
972        // Register the pending peek only after compute.peek() succeeds. If it
973        // fails (e.g. concurrent replica/cluster drop), inserting first would
974        // leak entries in these maps and misattribute statement execution reasons.
975        self.pending_peeks.insert(
976            uuid,
977            PendingPeek {
978                conn_id: conn_id.clone(),
979                cluster_id: compute_instance,
980                depends_on: source_ids,
981                ctx_extra: std::mem::take(ctx_extra),
982                is_fast_path,
983            },
984        );
985        self.client_pending_peeks
986            .entry(conn_id)
987            .or_default()
988            .insert(uuid, compute_instance);
989
990        let duration_histogram = self.metrics.row_set_finishing_seconds();
991
992        // If a dataflow was created, drop it now that the peek is queued. This is
993        // required: `add_collection` installs implied/warmup holds owned by the
994        // controller and only released via `drop_collections`, so without this call
995        // the transient dataflow's `since` would stay pinned at `as_of` forever. The
996        // peek's own read hold keeps the collection alive on the cluster until the
997        // response arrives.
998        if let Some(index_id) = drop_dataflow {
999            self.drop_compute_collections(vec![(compute_instance, index_id)]);
1000        }
1001
1002        let persist_client = self.persist_client.clone();
1003        let peek_stash_read_batch_size_bytes =
1004            mz_compute_types::dyncfgs::PEEK_RESPONSE_STASH_READ_BATCH_SIZE_BYTES
1005                .get(self.catalog().system_config().dyncfgs());
1006        let peek_stash_read_memory_budget_bytes =
1007            mz_compute_types::dyncfgs::PEEK_RESPONSE_STASH_READ_MEMORY_BUDGET_BYTES
1008                .get(self.catalog().system_config().dyncfgs());
1009
1010        let peek_response_stream = Self::create_peek_response_stream(
1011            rows_rx,
1012            finishing,
1013            max_result_size,
1014            max_returned_query_size,
1015            duration_histogram,
1016            persist_client,
1017            peek_stash_read_batch_size_bytes,
1018            peek_stash_read_memory_budget_bytes,
1019        );
1020
1021        Ok(crate::ExecuteResponse::SendingRowsStreaming {
1022            rows: Box::pin(peek_response_stream),
1023            instance_id: compute_instance,
1024            strategy,
1025        })
1026    }
1027
1028    /// Creates an async stream that processes peek responses and yields rows.
1029    ///
1030    /// TODO(peek-seq): Move this out of `coord` once we delete the old peek sequencing.
1031    #[mz_ore::instrument(level = "debug")]
1032    pub(crate) fn create_peek_response_stream(
1033        rows_rx: tokio::sync::oneshot::Receiver<PeekResponse>,
1034        finishing: RowSetFinishing,
1035        max_result_size: u64,
1036        max_returned_query_size: Option<u64>,
1037        duration_histogram: prometheus::Histogram,
1038        mut persist_client: mz_persist_client::PersistClient,
1039        peek_stash_read_batch_size_bytes: usize,
1040        peek_stash_read_memory_budget_bytes: usize,
1041    ) -> impl futures::Stream<Item = PeekResponseUnary> {
1042        async_stream::stream!({
1043            let result = rows_rx.await;
1044
1045            let rows = match result {
1046                Ok(rows) => rows,
1047                Err(e) => {
1048                    yield PeekResponseUnary::Error(AdapterError::Unstructured(anyhow::anyhow!(e)));
1049                    return;
1050                }
1051            };
1052
1053            match rows {
1054                PeekResponse::Rows(rows) => {
1055                    let rows = RowCollection::merge_sorted(&rows, &finishing.order_by);
1056                    match finishing.finish(
1057                        rows,
1058                        max_result_size,
1059                        max_returned_query_size,
1060                        &duration_histogram,
1061                    ) {
1062                        Ok((rows, _size_bytes)) => yield PeekResponseUnary::Rows(Box::new(rows)),
1063                        Err(e) => {
1064                            yield PeekResponseUnary::Error(AdapterError::Unstructured(
1065                                anyhow::Error::msg(e),
1066                            ))
1067                        }
1068                    }
1069                }
1070                PeekResponse::Stashed(response) => {
1071                    let response = *response;
1072
1073                    let shard_id = response.shard_id;
1074
1075                    let mut batches = Vec::new();
1076                    for proto_batch in response.batches.into_iter() {
1077                        let batch =
1078                            persist_client.batch_from_transmittable_batch(&shard_id, proto_batch);
1079
1080                        batches.push(batch);
1081                    }
1082                    tracing::trace!(?batches, "stashed peek response");
1083
1084                    let as_of = Antichain::from_elem(mz_repr::Timestamp::default());
1085                    let read_schemas: Schemas<SourceData, ()> = Schemas {
1086                        id: None,
1087                        key: Arc::new(response.relation_desc.clone()),
1088                        val: Arc::new(UnitSchema),
1089                    };
1090
1091                    let mut row_cursor = persist_client
1092                        .read_batches_consolidated::<_, _, _, i64>(
1093                            response.shard_id,
1094                            as_of,
1095                            read_schemas,
1096                            batches,
1097                            |_stats| true,
1098                            peek_stash_read_memory_budget_bytes,
1099                        )
1100                        .await
1101                        .expect("invalid usage");
1102
1103                    // NOTE: Using the cursor creates Futures that are not Sync,
1104                    // so we can't drive them on the main Coordinator loop.
1105                    // Spawning a task has the additional benefit that we get to
1106                    // delete batches once we're done.
1107                    //
1108                    // Batch deletion is best-effort, though, and there are
1109                    // multiple known ways in which they can leak, among them:
1110                    //
1111                    // - ProtoBatch is lost in flight
1112                    // - ProtoBatch is lost because when combining PeekResponse
1113                    // from workers a cancellation or error "overrides" other
1114                    // results, meaning we drop them
1115                    // - This task here is not run to completion before it can
1116                    // delete all batches
1117                    //
1118                    // This is semi-ok, because persist needs a reaper of leaked
1119                    // batches already, and so we piggy-back on that, even if it
1120                    // might not exist as of today.
1121                    let (tx, mut rx) = tokio::sync::mpsc::channel(1);
1122                    mz_ore::task::spawn(|| "read_peek_batches", async move {
1123                        // We always send our inline rows first. Ordering
1124                        // doesn't matter because we can only be in this case
1125                        // when there is no ORDER BY.
1126                        //
1127                        // We _could_ write these out as a Batch, and include it
1128                        // in the batches we read via the Consolidator. If we
1129                        // wanted to get a consistent ordering. That's not
1130                        // needed for correctness! But might be nice for more
1131                        // aesthetic reasons.
1132                        for rows in response.inline_rows {
1133                            let result = tx.send(rows).await;
1134                            if result.is_err() {
1135                                tracing::debug!("receiver went away");
1136                            }
1137                        }
1138
1139                        let mut current_batch = Vec::new();
1140                        let mut current_batch_size: usize = 0;
1141
1142                        'outer: while let Some(rows) = row_cursor.next().await {
1143                            for ((source_data, _val), _ts, diff) in rows {
1144                                let row = source_data
1145                                    .0
1146                                    .expect("we are not sending errors on this code path");
1147
1148                                let diff = usize::try_from(diff)
1149                                    .expect("peek responses cannot have negative diffs");
1150
1151                                if diff > 0 {
1152                                    let diff =
1153                                        NonZeroUsize::new(diff).expect("checked to be non-zero");
1154                                    current_batch_size =
1155                                        current_batch_size.saturating_add(row.byte_len());
1156                                    current_batch.push((row, diff));
1157                                }
1158
1159                                if current_batch_size > peek_stash_read_batch_size_bytes {
1160                                    // We're re-encoding the rows as a RowCollection
1161                                    // here, for which we pay in CPU time. We're in a
1162                                    // slow path already, since we're returning a big
1163                                    // stashed result so this is worth the convenience
1164                                    // of that for now.
1165                                    let result = tx
1166                                        .send(RowCollection::new(
1167                                            current_batch.drain(..).collect_vec(),
1168                                            &[],
1169                                        ))
1170                                        .await;
1171                                    if result.is_err() {
1172                                        tracing::debug!("receiver went away");
1173                                        // Don't return but break so we fall out to the
1174                                        // batch delete logic below.
1175                                        break 'outer;
1176                                    }
1177
1178                                    current_batch_size = 0;
1179                                }
1180                            }
1181                        }
1182
1183                        if current_batch.len() > 0 {
1184                            let result = tx.send(RowCollection::new(current_batch, &[])).await;
1185                            if result.is_err() {
1186                                tracing::debug!("receiver went away");
1187                            }
1188                        }
1189
1190                        let batches = row_cursor.into_lease();
1191                        tracing::trace!(?response.shard_id, "cleaning up batches of peek result");
1192                        for batch in batches {
1193                            batch.delete().await;
1194                        }
1195                    });
1196
1197                    assert!(
1198                        finishing.is_streamable(response.relation_desc.arity()),
1199                        "can only get stashed responses when the finishing is streamable"
1200                    );
1201
1202                    tracing::trace!("query result is streamable!");
1203
1204                    assert!(finishing.is_streamable(response.relation_desc.arity()));
1205                    let mut incremental_finishing = RowSetFinishingIncremental::new(
1206                        finishing.offset,
1207                        finishing.limit,
1208                        finishing.project,
1209                        max_returned_query_size,
1210                    );
1211
1212                    let mut got_zero_rows = true;
1213                    while let Some(rows) = rx.recv().await {
1214                        got_zero_rows = false;
1215
1216                        let result_rows = incremental_finishing.finish_incremental(
1217                            rows,
1218                            max_result_size,
1219                            &duration_histogram,
1220                        );
1221
1222                        match result_rows {
1223                            Ok(result_rows) => yield PeekResponseUnary::Rows(Box::new(result_rows)),
1224                            Err(e) => {
1225                                yield PeekResponseUnary::Error(AdapterError::Unstructured(
1226                                    anyhow::Error::msg(e),
1227                                ))
1228                            }
1229                        }
1230                    }
1231
1232                    // Even when there's zero rows, clients still expect an
1233                    // empty PeekResponse.
1234                    if got_zero_rows {
1235                        let row_iter = vec![].into_row_iter();
1236                        yield PeekResponseUnary::Rows(Box::new(row_iter));
1237                    }
1238                }
1239                PeekResponse::Canceled => {
1240                    yield PeekResponseUnary::Canceled;
1241                }
1242                PeekResponse::Error(e) => {
1243                    yield PeekResponseUnary::Error(e.into());
1244                }
1245            }
1246        })
1247    }
1248
1249    /// Cancel and remove all pending peeks that were initiated by the client with `conn_id`.
1250    #[mz_ore::instrument(level = "debug")]
1251    pub(crate) fn cancel_pending_peeks(&mut self, conn_id: &ConnectionId) {
1252        if let Some(uuids) = self.client_pending_peeks.remove(conn_id) {
1253            self.metrics
1254                .canceled_peeks
1255                .inc_by(u64::cast_from(uuids.len()));
1256
1257            let mut inverse: BTreeMap<ComputeInstanceId, BTreeSet<Uuid>> = Default::default();
1258            for (uuid, compute_instance) in &uuids {
1259                inverse.entry(*compute_instance).or_default().insert(*uuid);
1260            }
1261            for (compute_instance, uuids) in inverse {
1262                // It's possible that this compute instance no longer exists because it was dropped
1263                // while the peek was in progress. In this case we ignore the error and move on
1264                // because the dataflow no longer exists.
1265                // TODO(jkosh44) Dropping a cluster should actively cancel all pending queries.
1266                for uuid in uuids {
1267                    let _ = self.controller.compute.cancel_peek(
1268                        compute_instance,
1269                        uuid,
1270                        PeekResponse::Canceled,
1271                    );
1272                }
1273            }
1274
1275            let peeks = uuids
1276                .iter()
1277                .filter_map(|(uuid, _)| self.pending_peeks.remove(uuid))
1278                .collect::<Vec<_>>();
1279            for peek in peeks {
1280                self.retire_execution(
1281                    StatementEndedExecutionReason::Canceled,
1282                    peek.ctx_extra.defuse(),
1283                );
1284            }
1285        }
1286    }
1287
1288    /// Handle a peek notification and retire the corresponding execution. Does nothing for
1289    /// already-removed peeks.
1290    pub(crate) fn handle_peek_notification(
1291        &mut self,
1292        uuid: Uuid,
1293        notification: PeekNotification,
1294        otel_ctx: OpenTelemetryContext,
1295    ) {
1296        // We expect exactly one peek response, which we forward. Then we clean up the
1297        // peek's state in the coordinator.
1298        if let Some(PendingPeek {
1299            conn_id: _,
1300            cluster_id: _,
1301            depends_on: _,
1302            ctx_extra,
1303            is_fast_path,
1304        }) = self.remove_pending_peek(&uuid)
1305        {
1306            let reason = match notification {
1307                PeekNotification::Success {
1308                    rows: num_rows,
1309                    result_size,
1310                } => {
1311                    let strategy = if is_fast_path {
1312                        StatementExecutionStrategy::FastPath
1313                    } else {
1314                        StatementExecutionStrategy::Standard
1315                    };
1316                    StatementEndedExecutionReason::Success {
1317                        result_size: Some(result_size),
1318                        rows_returned: Some(num_rows),
1319                        execution_strategy: Some(strategy),
1320                    }
1321                }
1322                PeekNotification::Error(error) => StatementEndedExecutionReason::Errored { error },
1323                PeekNotification::Canceled => StatementEndedExecutionReason::Canceled,
1324            };
1325            otel_ctx.attach_as_parent();
1326            self.retire_execution(reason, ctx_extra.defuse());
1327        }
1328        // Cancellation may cause us to receive responses for peeks no
1329        // longer in `self.pending_peeks`, so we quietly ignore them.
1330    }
1331
1332    /// Clean up a peek's state.
1333    pub(crate) fn remove_pending_peek(&mut self, uuid: &Uuid) -> Option<PendingPeek> {
1334        let pending_peek = self.pending_peeks.remove(uuid);
1335        if let Some(pending_peek) = &pending_peek {
1336            let uuids = self
1337                .client_pending_peeks
1338                .get_mut(&pending_peek.conn_id)
1339                .expect("coord peek state is inconsistent");
1340            uuids.remove(uuid);
1341            if uuids.is_empty() {
1342                self.client_pending_peeks.remove(&pending_peek.conn_id);
1343            }
1344        }
1345        pending_peek
1346    }
1347
1348    /// Implements a slow-path peek by creating a transient dataflow.
1349    /// This is called from the command handler for ExecuteSlowPathPeek.
1350    ///
1351    /// (For now, this method simply delegates to implement_peek_plan by constructing
1352    /// the necessary PlannedPeek structure.)
1353    pub(crate) async fn implement_slow_path_peek(
1354        &mut self,
1355        dataflow_plan: PeekDataflowPlan,
1356        determination: TimestampDetermination,
1357        finishing: RowSetFinishing,
1358        compute_instance: ComputeInstanceId,
1359        target_replica: Option<ReplicaId>,
1360        intermediate_result_type: SqlRelationType,
1361        source_ids: BTreeSet<GlobalId>,
1362        conn_id: ConnectionId,
1363        max_result_size: u64,
1364        max_query_result_size: Option<u64>,
1365        watch_set: Option<WatchSetCreation>,
1366    ) -> Result<ExecuteResponse, AdapterError> {
1367        // Install watch sets for statement lifecycle logging if enabled.
1368        // This must happen _before_ creating ExecuteContextExtra, so that if it fails,
1369        // we don't have an ExecuteContextExtra that needs to be retired (the frontend
1370        // will handle logging for the error case).
1371        let statement_logging_id = watch_set.as_ref().map(|ws| ws.logging_id);
1372        if let Some(ws) = watch_set {
1373            self.install_peek_watch_sets(conn_id.clone(), ws)
1374                .map_err(|e| {
1375                    AdapterError::concurrent_dependency_drop_from_watch_set_install_error(e)
1376                })?;
1377        }
1378
1379        let source_arity = intermediate_result_type.arity();
1380
1381        let planned_peek = PlannedPeek {
1382            plan: PeekPlan::SlowPath(dataflow_plan),
1383            determination,
1384            conn_id,
1385            intermediate_result_type,
1386            source_arity,
1387            source_ids,
1388        };
1389
1390        // TODO(peek-seq): After the old peek sequencing is completely removed, we should merge the
1391        // relevant parts of the old `implement_peek_plan` into this method, and remove the old
1392        // `implement_peek_plan`.
1393        let mut ctx_guard =
1394            ExecuteContextGuard::new(statement_logging_id, self.internal_cmd_tx.clone());
1395        let result = self
1396            .implement_peek_plan(
1397                &mut ctx_guard,
1398                planned_peek,
1399                finishing,
1400                compute_instance,
1401                target_replica,
1402                max_result_size,
1403                max_query_result_size,
1404            )
1405            .await;
1406        // On error `implement_peek_plan` left the guard's contents intact (see
1407        // its doc comment) and the frontend logs the error end, so we defuse
1408        // rather than let the guard's `Drop` emit a spurious `Aborted`.
1409        if result.is_err() {
1410            let _ = ctx_guard.defuse();
1411        }
1412        result
1413    }
1414
1415    /// Implements a `COPY TO` command by installing peek watch sets,
1416    /// shipping the dataflow, and spawning a background task to wait for completion.
1417    /// This is called from the command handler for ExecuteCopyTo.
1418    ///
1419    /// (The S3 preflight check must be completed successfully via the
1420    /// `CopyToPreflight` command _before_ calling this method. The preflight is
1421    /// handled separately to avoid blocking the coordinator's main task with
1422    /// slow S3 network operations.)
1423    ///
1424    /// This method does NOT block waiting for completion. Instead, it spawns a background task that
1425    /// will send the response through the provided tx channel when the COPY TO completes.
1426    /// All errors (setup or execution) are sent through tx.
1427    pub(crate) async fn implement_copy_to(
1428        &mut self,
1429        df_desc: DataflowDescription<mz_compute_types::plan::LirRelationExpr>,
1430        compute_instance: ComputeInstanceId,
1431        target_replica: Option<ReplicaId>,
1432        source_ids: BTreeSet<GlobalId>,
1433        conn_id: ConnectionId,
1434        watch_set: Option<WatchSetCreation>,
1435        tx: oneshot::Sender<Result<ExecuteResponse, AdapterError>>,
1436    ) {
1437        // Helper to send error and return early
1438        let send_err = |tx: oneshot::Sender<Result<ExecuteResponse, AdapterError>>,
1439                        e: AdapterError| {
1440            let _ = tx.send(Err(e));
1441        };
1442
1443        // Install watch sets for statement lifecycle logging if enabled.
1444        // If this fails, we just send the error back. The frontend will handle logging
1445        // for the error case (no ExecuteContextExtra is created here).
1446        if let Some(ws) = watch_set {
1447            if let Err(e) = self.install_peek_watch_sets(conn_id.clone(), ws) {
1448                let err = AdapterError::concurrent_dependency_drop_from_watch_set_install_error(e);
1449                send_err(tx, err);
1450                return;
1451            }
1452        }
1453
1454        // Note: We don't create an ExecuteContextExtra here because the frontend handles
1455        // all statement logging for COPY TO operations.
1456
1457        let sink_id = df_desc.sink_id();
1458
1459        // Create and register ActiveCopyTo.
1460        // Note: sink_tx/sink_rx is the channel for the compute sink to notify completion
1461        // This is different from the command's tx which sends the response to the client
1462        let (sink_tx, sink_rx) = oneshot::channel();
1463        let active_copy_to = ActiveCopyTo {
1464            conn_id: conn_id.clone(),
1465            tx: sink_tx,
1466            cluster_id: compute_instance,
1467            depends_on: source_ids,
1468        };
1469
1470        // Add metadata for the new COPY TO. CopyTo returns a `ready` future, so it is safe to drop.
1471        drop(self.add_active_compute_sink(sink_id, ActiveComputeSink::CopyTo(active_copy_to)));
1472
1473        // Try to ship the dataflow. We handle errors gracefully because dependencies might have
1474        // disappeared during sequencing.
1475        if let Err(e) = self
1476            .try_ship_dataflow(df_desc, compute_instance, target_replica)
1477            .await
1478            .map_err(AdapterError::concurrent_dependency_drop_from_dataflow_creation_error)
1479        {
1480            // Clean up the active compute sink that was added above, since the dataflow was never
1481            // created. If we don't do this, the sink_id remains in drop_sinks but no collection
1482            // exists in the compute controller, causing a panic when the connection terminates.
1483            self.remove_active_compute_sink(sink_id).await;
1484            send_err(tx, e);
1485            return;
1486        }
1487
1488        // Spawn background task to wait for completion
1489        // We must NOT await sink_rx here directly, as that would block the coordinator's main task
1490        // from processing the completion message. Instead, we spawn a background task that will
1491        // send the result through tx when the COPY TO completes.
1492        let span = Span::current();
1493        task::spawn(
1494            || "copy to completion",
1495            async move {
1496                let res = sink_rx.await;
1497                let result = match res {
1498                    Ok(res) => res,
1499                    Err(_) => Err(AdapterError::Internal("copy to sender dropped".into())),
1500                };
1501
1502                let _ = tx.send(result);
1503            }
1504            .instrument(span),
1505        );
1506    }
1507
1508    /// Constructs an [`ExecuteResponse`] that that will send some rows to the
1509    /// client immediately, as opposed to asking the dataflow layer to send along
1510    /// the rows after some computation.
1511    pub(crate) fn send_immediate_rows<I>(rows: I) -> ExecuteResponse
1512    where
1513        I: IntoRowIterator,
1514        I::Iter: Send + Sync + 'static,
1515    {
1516        let rows = Box::new(rows.into_row_iter());
1517        ExecuteResponse::SendingRowsImmediate { rows }
1518    }
1519}
1520
1521#[cfg(test)]
1522mod tests {
1523    use mz_expr::func::IsNull;
1524    use mz_expr::{MapFilterProject, UnaryFunc};
1525    use mz_ore::str::Indent;
1526    use mz_repr::explain::text::text_string_at;
1527    use mz_repr::explain::{DummyHumanizer, ExplainConfig, PlanRenderingContext};
1528    use mz_repr::{Datum, SqlColumnType, SqlScalarType};
1529
1530    use super::*;
1531
1532    #[mz_ore::test]
1533    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1534    fn test_fast_path_plan_as_text() {
1535        let typ = SqlRelationType::new(vec![SqlColumnType {
1536            scalar_type: SqlScalarType::String,
1537            nullable: false,
1538        }]);
1539        let constant_err = FastPathPlan::Constant(Err(EvalError::DivisionByZero), typ.clone());
1540        let no_lookup = FastPathPlan::PeekExisting(
1541            GlobalId::User(8),
1542            GlobalId::User(10),
1543            None,
1544            MapFilterProject::new(4)
1545                .map(Some(MirScalarExpr::column(0).or(MirScalarExpr::column(2))))
1546                .project([1, 4])
1547                .into_plan()
1548                .expect("invalid plan")
1549                .into_nontemporal()
1550                .expect("invalid nontemporal"),
1551        );
1552        let lookup = FastPathPlan::PeekExisting(
1553            GlobalId::User(9),
1554            GlobalId::User(11),
1555            Some(vec![Row::pack(Some(Datum::Int32(5)))]),
1556            MapFilterProject::new(3)
1557                .filter(Some(
1558                    MirScalarExpr::column(0).call_unary(UnaryFunc::IsNull(IsNull)),
1559                ))
1560                .into_plan()
1561                .expect("invalid plan")
1562                .into_nontemporal()
1563                .expect("invalid nontemporal"),
1564        );
1565
1566        let humanizer = DummyHumanizer;
1567        let config = ExplainConfig {
1568            redacted: false,
1569            verbose_syntax: true,
1570            ..Default::default()
1571        };
1572        let ctx_gen = || {
1573            let indent = Indent::default();
1574            let annotations = BTreeMap::new();
1575            PlanRenderingContext::<FastPathPlan>::new(
1576                indent,
1577                &humanizer,
1578                annotations,
1579                &config,
1580                BTreeSet::default(),
1581            )
1582        };
1583
1584        let constant_err_exp = "Error \"division by zero\"\n";
1585        let no_lookup_exp = "Project (#1, #4)\n  Map ((#0 OR #2))\n    ReadIndex on=u8 [DELETED INDEX]=[*** full scan ***]\n";
1586        let lookup_exp =
1587            "Filter (#0) IS NULL\n  ReadIndex on=u9 [DELETED INDEX]=[lookup value=(5)]\n";
1588
1589        assert_eq!(text_string_at(&constant_err, ctx_gen), constant_err_exp);
1590        assert_eq!(text_string_at(&no_lookup, ctx_gen), no_lookup_exp);
1591        assert_eq!(text_string_at(&lookup, ctx_gen), lookup_exp);
1592
1593        let mut constant_rows = vec![
1594            (Row::pack(Some(Datum::String("hello"))), Diff::ONE),
1595            (Row::pack(Some(Datum::String("world"))), 2.into()),
1596            (Row::pack(Some(Datum::String("star"))), 500.into()),
1597        ];
1598        let constant_exp1 =
1599            "Constant\n  - (\"hello\")\n  - ((\"world\") x 2)\n  - ((\"star\") x 500)\n";
1600        assert_eq!(
1601            text_string_at(
1602                &FastPathPlan::Constant(Ok(constant_rows.clone()), typ.clone()),
1603                ctx_gen
1604            ),
1605            constant_exp1
1606        );
1607        constant_rows
1608            .extend((0..20).map(|i| (Row::pack(Some(Datum::String(&i.to_string()))), Diff::ONE)));
1609        let constant_exp2 = "Constant\n  total_rows (diffs absed): 523\n  first_rows:\n    - (\"hello\")\
1610        \n    - ((\"world\") x 2)\n    - ((\"star\") x 500)\n    - (\"0\")\n    - (\"1\")\
1611        \n    - (\"2\")\n    - (\"3\")\n    - (\"4\")\n    - (\"5\")\n    - (\"6\")\
1612        \n    - (\"7\")\n    - (\"8\")\n    - (\"9\")\n    - (\"10\")\n    - (\"11\")\
1613        \n    - (\"12\")\n    - (\"13\")\n    - (\"14\")\n    - (\"15\")\n    - (\"16\")\n";
1614        assert_eq!(
1615            text_string_at(&FastPathPlan::Constant(Ok(constant_rows), typ), ctx_gen),
1616            constant_exp2
1617        );
1618    }
1619}