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