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::ops::Deref;
18use std::sync::Arc;
19
20use mz_adapter_types::connection::ConnectionId;
21use mz_cluster_client::ReplicaId;
22use mz_compute_client::controller::PeekNotification;
23use mz_compute_client::protocol::command::PeekTarget;
24use mz_compute_client::protocol::response::PeekResponse;
25use mz_compute_types::ComputeInstanceId;
26use mz_compute_types::dataflows::{DataflowDescription, IndexImport};
27use mz_controller_types::ClusterId;
28use mz_expr::explain::{HumanizedExplain, HumanizerMode, fmt_text_constant_rows};
29use mz_expr::{
30    EvalError, Id, MirRelationExpr, MirScalarExpr, OptimizedMirRelationExpr, RowSetFinishing,
31    permutation_for_arrangement,
32};
33use mz_ore::cast::CastFrom;
34use mz_ore::soft_assert_eq_or_log;
35use mz_ore::str::{StrExt, separated};
36use mz_ore::task;
37use mz_ore::tracing::OpenTelemetryContext;
38use mz_repr::explain::text::DisplayText;
39use mz_repr::explain::{CompactScalars, IndexUsageType, PlanRenderingContext, UsedIndexes};
40use mz_repr::{
41    Diff, GlobalId, IntoRowIterator, RelationDesc, Row, RowIterator, SqlRelationType,
42    preserves_order,
43};
44use serde::{Deserialize, Serialize};
45use tokio::sync::oneshot;
46use tracing::{Instrument, Span};
47use uuid::Uuid;
48
49use crate::active_compute_sink::{ActiveComputeSink, ActiveCopyTo};
50use crate::catalog::Catalog;
51use crate::coord::timestamp_selection::TimestampDetermination;
52use crate::optimize::OptimizerError;
53use crate::peek_client::CoordinatorClient;
54use crate::statement_logging::WatchSetCreation;
55use crate::statement_logging::{StatementEndedExecutionReason, StatementExecutionStrategy};
56use crate::{AdapterError, ExecuteContextGuard, ExecuteResponse, PeekClient};
57
58/// A peek is a request to read data from a maintained arrangement.
59#[derive(Debug)]
60pub(crate) struct PendingPeek {
61    /// The connection that initiated the peek.
62    pub(crate) conn_id: ConnectionId,
63    /// The cluster that the peek is being executed on.
64    pub(crate) cluster_id: ClusterId,
65    /// All `GlobalId`s that the peek depend on.
66    pub(crate) depends_on: BTreeSet<GlobalId>,
67    /// Context about the execute that produced this peek,
68    /// needed by the coordinator for retiring it.
69    pub(crate) ctx_extra: ExecuteContextGuard,
70    /// Is this a fast-path peek, i.e. one that doesn't require a dataflow?
71    pub(crate) is_fast_path: bool,
72}
73
74/// The response from a `Peek`, with row multiplicities represented in unary.
75///
76/// Note that each `Peek` expects to generate exactly one `PeekResponse`, i.e.
77/// we expect a 1:1 contract between `Peek` and `PeekResponseUnary`.
78#[derive(Debug)]
79pub enum PeekResponseUnary {
80    Rows(Box<dyn RowIterator + Send + Sync>),
81    Error(AdapterError),
82    Canceled,
83    /// A dependency was dropped during execution.
84    DependencyDropped(DroppedDependency),
85}
86
87/// A dependency that was dropped while a peek or subscribe was in flight.
88///
89/// The `name` fields hold the bare name (e.g. `db.schema.t` or `c`); `Display`
90/// applies SQL identifier quoting to produce `relation "db.schema.t"` or
91/// `cluster "c"` for direct use in error wording.
92#[derive(Clone, Debug)]
93pub enum DroppedDependency {
94    Relation { name: String },
95    Cluster { name: String },
96}
97
98impl fmt::Display for DroppedDependency {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        match self {
101            Self::Relation { name } => write!(f, "relation {}", name.quoted()),
102            Self::Cluster { name } => write!(f, "cluster {}", name.quoted()),
103        }
104    }
105}
106
107impl DroppedDependency {
108    /// User-facing error for a query (peek or subscribe) that could not finish
109    /// because this dependency was dropped mid-flight.
110    pub fn query_terminated_error(&self) -> String {
111        format!("query could not complete because {self} was dropped")
112    }
113
114    /// Convert this dropped dependency into an [`AdapterError::ConcurrentDependencyDrop`].
115    pub fn to_concurrent_dependency_drop(&self) -> AdapterError {
116        let (kind, name) = match self {
117            Self::Relation { name } => ("relation", name.clone()),
118            Self::Cluster { name } => ("cluster", name.clone()),
119        };
120        AdapterError::ConcurrentDependencyDrop {
121            dependency_kind: kind,
122            dependency_id: name,
123        }
124    }
125}
126
127#[derive(Clone, Debug)]
128pub struct PeekDataflowPlan {
129    pub(crate) desc: mz_compute_types::dataflows::LirDataflowDescription,
130    pub(crate) id: GlobalId,
131    key: Vec<MirScalarExpr>,
132    permutation: Vec<usize>,
133    thinned_arity: usize,
134}
135
136impl PeekDataflowPlan {
137    pub fn new(
138        desc: mz_compute_types::dataflows::LirDataflowDescription,
139        id: GlobalId,
140        typ: &SqlRelationType,
141    ) -> Self {
142        let arity = typ.arity();
143        let key = typ
144            .default_key()
145            .into_iter()
146            .map(MirScalarExpr::column)
147            .collect::<Vec<_>>();
148        let (permutation, thinning) = permutation_for_arrangement(&key, arity);
149        Self {
150            desc,
151            id,
152            key,
153            permutation,
154            thinned_arity: thinning.len(),
155        }
156    }
157}
158
159#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd)]
160pub enum FastPathPlan {
161    /// The view evaluates to a constant result that can be returned.
162    ///
163    /// The [SqlRelationType] is unnecessary for evaluating the constant result but
164    /// may be helpful when printing out an explanation.
165    Constant(Result<Vec<(Row, Diff)>, EvalError>, SqlRelationType),
166    /// The view can be read out of an existing arrangement.
167    /// (coll_id, idx_id, values to look up, mfp to apply)
168    PeekExisting(GlobalId, GlobalId, Option<Vec<Row>>, mz_expr::SafeMfpPlan),
169    /// The view can be read directly out of Persist.
170    PeekPersist(GlobalId, Option<Row>, mz_expr::SafeMfpPlan),
171}
172
173impl<'a, T: 'a> DisplayText<PlanRenderingContext<'a, T>> for FastPathPlan {
174    fn fmt_text(
175        &self,
176        f: &mut fmt::Formatter<'_>,
177        ctx: &mut PlanRenderingContext<'a, T>,
178    ) -> fmt::Result {
179        if ctx.config.verbose_syntax {
180            self.fmt_verbose_text(f, ctx)
181        } else {
182            self.fmt_default_text(f, ctx)
183        }
184    }
185}
186
187impl FastPathPlan {
188    pub fn fmt_default_text<'a, T>(
189        &self,
190        f: &mut fmt::Formatter<'_>,
191        ctx: &mut PlanRenderingContext<'a, T>,
192    ) -> fmt::Result {
193        let mode = HumanizedExplain::new(ctx.config.redacted);
194
195        match self {
196            FastPathPlan::Constant(rows, _) => {
197                write!(f, "{}→Constant ", ctx.indent)?;
198
199                match rows {
200                    Ok(rows) => writeln!(f, "({} rows)", rows.len())?,
201                    Err(err) => {
202                        if mode.redacted() {
203                            writeln!(f, "(error: █)")?;
204                        } else {
205                            writeln!(f, "(error: {})", err.to_string().quoted(),)?;
206                        }
207                    }
208                }
209            }
210            FastPathPlan::PeekExisting(coll_id, idx_id, literal_constraints, mfp) => {
211                let coll = ctx
212                    .humanizer
213                    .humanize_id(*coll_id)
214                    .unwrap_or_else(|| coll_id.to_string());
215                let idx = ctx
216                    .humanizer
217                    .humanize_id(*idx_id)
218                    .unwrap_or_else(|| idx_id.to_string());
219                writeln!(f, "{}→Map/Filter/Project", ctx.indent)?;
220                ctx.indent.set();
221
222                ctx.indent += 1;
223
224                mode.expr(mfp.deref(), None).fmt_default_text(f, ctx)?;
225                let printed = !mfp.expressions.is_empty() || !mfp.predicates.is_empty();
226
227                if printed {
228                    ctx.indent += 1;
229                }
230                if let Some(literal_constraints) = literal_constraints {
231                    writeln!(f, "{}→Index Lookup on {coll} (using {idx})", ctx.indent)?;
232                    ctx.indent += 1;
233                    let values = separated("; ", mode.seq(literal_constraints, None));
234                    writeln!(f, "{}Lookup values: {values}", ctx.indent)?;
235                } else {
236                    writeln!(f, "{}→Indexed {coll} (using {idx})", ctx.indent)?;
237                }
238
239                ctx.indent.reset();
240            }
241            FastPathPlan::PeekPersist(global_id, literal_constraint, mfp) => {
242                let coll = ctx
243                    .humanizer
244                    .humanize_id(*global_id)
245                    .unwrap_or_else(|| global_id.to_string());
246                writeln!(f, "{}→Map/Filter/Project", ctx.indent)?;
247                ctx.indent.set();
248
249                ctx.indent += 1;
250
251                mode.expr(mfp.deref(), None).fmt_default_text(f, ctx)?;
252                let printed = !mfp.expressions.is_empty() || !mfp.predicates.is_empty();
253
254                if printed {
255                    ctx.indent += 1;
256                }
257                if let Some(literal_constraint) = literal_constraint {
258                    writeln!(f, "{}→ReadStorage Lookup on {coll}", ctx.indent)?;
259                    ctx.indent += 1;
260                    let value = mode.expr(literal_constraint, None);
261                    writeln!(f, "{}Lookup value: {value}", ctx.indent)?;
262                } else {
263                    writeln!(f, "{}→ReadStorage {coll}", ctx.indent)?;
264                }
265
266                ctx.indent.reset();
267            }
268        }
269
270        Ok(())
271    }
272
273    pub fn fmt_verbose_text<'a, T>(
274        &self,
275        f: &mut fmt::Formatter<'_>,
276        ctx: &mut PlanRenderingContext<'a, T>,
277    ) -> fmt::Result {
278        let redacted = ctx.config.redacted;
279        let mode = HumanizedExplain::new(redacted);
280
281        // TODO(aalexandrov): factor out common PeekExisting and PeekPersist
282        // code.
283        match self {
284            FastPathPlan::Constant(Ok(rows), _) => {
285                if !rows.is_empty() {
286                    writeln!(f, "{}Constant", ctx.indent)?;
287                    *ctx.as_mut() += 1;
288                    fmt_text_constant_rows(
289                        f,
290                        rows.iter().map(|(row, diff)| (row, diff)),
291                        ctx.as_mut(),
292                        redacted,
293                    )?;
294                    *ctx.as_mut() -= 1;
295                } else {
296                    writeln!(f, "{}Constant <empty>", ctx.as_mut())?;
297                }
298                Ok(())
299            }
300            FastPathPlan::Constant(Err(err), _) => {
301                if redacted {
302                    writeln!(f, "{}Error █", ctx.as_mut())
303                } else {
304                    writeln!(f, "{}Error {}", ctx.as_mut(), err.to_string().escaped())
305                }
306            }
307            FastPathPlan::PeekExisting(coll_id, idx_id, literal_constraints, mfp) => {
308                ctx.as_mut().set();
309                let (map, filter, project) = mfp.as_map_filter_project();
310
311                let cols = if !ctx.config.humanized_exprs {
312                    None
313                } else if let Some(cols) = ctx.humanizer.column_names_for_id(*idx_id) {
314                    // FIXME: account for thinning and permutation
315                    // See mz_expr::permutation_for_arrangement
316                    // See permute_oneshot_mfp_around_index
317                    let cols = itertools::chain(
318                        cols.iter().cloned(),
319                        std::iter::repeat(String::new()).take(map.len()),
320                    )
321                    .collect();
322                    Some(cols)
323                } else {
324                    None
325                };
326
327                if project.len() != mfp.input_arity + map.len()
328                    || !project.iter().enumerate().all(|(i, o)| i == *o)
329                {
330                    let outputs = mode.seq(&project, cols.as_ref());
331                    let outputs = CompactScalars(outputs);
332                    writeln!(f, "{}Project ({})", ctx.as_mut(), outputs)?;
333                    *ctx.as_mut() += 1;
334                }
335                if !filter.is_empty() {
336                    let predicates = separated(" AND ", mode.seq(&filter, cols.as_ref()));
337                    writeln!(f, "{}Filter {}", ctx.as_mut(), predicates)?;
338                    *ctx.as_mut() += 1;
339                }
340                if !map.is_empty() {
341                    let scalars = mode.seq(&map, cols.as_ref());
342                    let scalars = CompactScalars(scalars);
343                    writeln!(f, "{}Map ({})", ctx.as_mut(), scalars)?;
344                    *ctx.as_mut() += 1;
345                }
346                MirRelationExpr::fmt_indexed_filter(
347                    f,
348                    ctx,
349                    coll_id,
350                    idx_id,
351                    literal_constraints.clone(),
352                    None,
353                )?;
354                writeln!(f)?;
355                ctx.as_mut().reset();
356                Ok(())
357            }
358            FastPathPlan::PeekPersist(gid, literal_constraint, mfp) => {
359                ctx.as_mut().set();
360                let (map, filter, project) = mfp.as_map_filter_project();
361
362                let cols = if !ctx.config.humanized_exprs {
363                    None
364                } else if let Some(cols) = ctx.humanizer.column_names_for_id(*gid) {
365                    let cols = itertools::chain(
366                        cols.iter().cloned(),
367                        std::iter::repeat(String::new()).take(map.len()),
368                    )
369                    .collect::<Vec<_>>();
370                    Some(cols)
371                } else {
372                    None
373                };
374
375                if project.len() != mfp.input_arity + map.len()
376                    || !project.iter().enumerate().all(|(i, o)| i == *o)
377                {
378                    let outputs = mode.seq(&project, cols.as_ref());
379                    let outputs = CompactScalars(outputs);
380                    writeln!(f, "{}Project ({})", ctx.as_mut(), outputs)?;
381                    *ctx.as_mut() += 1;
382                }
383                if !filter.is_empty() {
384                    let predicates = separated(" AND ", mode.seq(&filter, cols.as_ref()));
385                    writeln!(f, "{}Filter {}", ctx.as_mut(), predicates)?;
386                    *ctx.as_mut() += 1;
387                }
388                if !map.is_empty() {
389                    let scalars = mode.seq(&map, cols.as_ref());
390                    let scalars = CompactScalars(scalars);
391                    writeln!(f, "{}Map ({})", ctx.as_mut(), scalars)?;
392                    *ctx.as_mut() += 1;
393                }
394                let human_id = ctx
395                    .humanizer
396                    .humanize_id(*gid)
397                    .unwrap_or_else(|| gid.to_string());
398                write!(f, "{}PeekPersist {human_id}", ctx.as_mut())?;
399                if let Some(literal) = literal_constraint {
400                    let value = mode.expr(literal, None);
401                    writeln!(f, " [value={}]", value)?;
402                } else {
403                    writeln!(f, "")?;
404                }
405                ctx.as_mut().reset();
406                Ok(())
407            }
408        }?;
409        Ok(())
410    }
411}
412
413/// Possible ways in which the coordinator could produce the result for a goal view.
414#[derive(Clone, Debug)]
415pub enum PeekPlan {
416    FastPath(FastPathPlan),
417    /// The view must be installed as a dataflow and then read.
418    SlowPath(PeekDataflowPlan),
419}
420
421/// Convert `mfp` to an executable, non-temporal plan.
422/// It should be non-temporal, as OneShot preparation populates `mz_now`.
423///
424/// If the `mfp` can't be converted into a non-temporal plan, this returns an _internal_ error.
425fn mfp_to_safe_plan(
426    mfp: mz_expr::MapFilterProject,
427) -> Result<mz_expr::SafeMfpPlan, OptimizerError> {
428    mfp.into_plan()
429        .map_err(OptimizerError::InternalUnsafeMfpPlan)?
430        .into_nontemporal()
431        .map_err(|e| OptimizerError::InternalUnsafeMfpPlan(format!("{:?}", e)))
432}
433
434/// If it can't convert `mfp` into a `SafeMfpPlan`, this returns an _internal_ error.
435fn permute_oneshot_mfp_around_index(
436    mfp: mz_expr::MapFilterProject,
437    key: &[MirScalarExpr],
438) -> Result<mz_expr::SafeMfpPlan, OptimizerError> {
439    let input_arity = mfp.input_arity;
440    let mut safe_mfp = mfp_to_safe_plan(mfp)?;
441    let (permute, thinning) = permutation_for_arrangement(key, input_arity);
442    safe_mfp.permute_fn(|c| permute[c], key.len() + thinning.len());
443    Ok(safe_mfp)
444}
445
446/// Determine if the dataflow plan can be implemented without an actual dataflow.
447///
448/// If the optimized plan is a `Constant` or a `Get` of a maintained arrangement,
449/// we can avoid building a dataflow (and either just return the results, or peek
450/// out of the arrangement, respectively).
451pub fn create_fast_path_plan(
452    dataflow_plan: &mut DataflowDescription<OptimizedMirRelationExpr>,
453    view_id: GlobalId,
454    finishing: Option<&RowSetFinishing>,
455    persist_fast_path_limit: usize,
456    persist_fast_path_order: bool,
457) -> Result<Option<FastPathPlan>, OptimizerError> {
458    // At this point, `dataflow_plan` contains our best optimized dataflow.
459    // We will check the plan to see if there is a fast path to escape full dataflow construction.
460
461    // We need to restrict ourselves to settings where the inserted transient view is the first thing
462    // to build (no dependent views). There is likely an index to build as well, but we may not be sure.
463    if dataflow_plan.objects_to_build.len() >= 1 && dataflow_plan.objects_to_build[0].id == view_id
464    {
465        let mut mir = &*dataflow_plan.objects_to_build[0].plan.as_inner_mut();
466        if let Some((rows, found_typ)) = mir.as_const() {
467            // In the case of a constant, we can return the result now.
468            let plan = FastPathPlan::Constant(
469                rows.clone(),
470                mz_repr::SqlRelationType::from_repr(found_typ),
471            );
472            return Ok(Some(plan));
473        } else {
474            // If there is a TopK that would be completely covered by the finishing, then jump
475            // through the TopK.
476            if let MirRelationExpr::TopK {
477                input,
478                group_key,
479                order_key,
480                limit,
481                offset,
482                monotonic: _,
483                expected_group_size: _,
484            } = mir
485            {
486                if let Some(finishing) = finishing {
487                    if group_key.is_empty() && *order_key == finishing.order_by && *offset == 0 {
488                        // The following is roughly `limit >= finishing.limit + finishing.offset`,
489                        // but with Options.
490                        let finishing_limits_at_least_as_topk = match (limit, finishing.limit) {
491                            (None, _) => true,
492                            (Some(..), None) => false,
493                            (Some(topk_limit), Some(finishing_limit)) => {
494                                if let Some(l) = topk_limit.as_literal_int64() {
495                                    i128::cast_from(l)
496                                        >= i128::cast_from(*finishing_limit)
497                                            + i128::cast_from(finishing.offset)
498                                } else {
499                                    false
500                                }
501                            }
502                        };
503                        if finishing_limits_at_least_as_topk {
504                            mir = input;
505                        }
506                    }
507                }
508            }
509            // In the case of a linear operator around an indexed view, we
510            // can skip creating a dataflow and instead pull all the rows in
511            // index and apply the linear operator against them.
512            let (mfp, mir) = mz_expr::MapFilterProject::extract_from_expression(mir);
513            match mir {
514                MirRelationExpr::Get {
515                    id: Id::Global(get_id),
516                    typ: repr_typ,
517                    ..
518                } => {
519                    // Just grab any arrangement if an arrangement exists
520                    for (index_id, IndexImport { desc, .. }) in dataflow_plan.index_imports.iter() {
521                        if desc.on_id == *get_id {
522                            return Ok(Some(FastPathPlan::PeekExisting(
523                                *get_id,
524                                *index_id,
525                                None,
526                                permute_oneshot_mfp_around_index(mfp, &desc.key)?,
527                            )));
528                        }
529                    }
530
531                    // If there is no arrangement, consider peeking the persist shard directly.
532                    // Generally, we consider a persist peek when the query can definitely be satisfied
533                    // by scanning through a small, constant number of Persist key-values.
534                    let safe_mfp = mfp_to_safe_plan(mfp)?;
535                    let (_maps, filters, projection) = safe_mfp.as_map_filter_project();
536
537                    let persist_fast_path_order_relation_typ = if persist_fast_path_order {
538                        Some(
539                            dataflow_plan
540                                .source_imports
541                                .get(get_id)
542                                .expect("Get's ID is also imported")
543                                .desc
544                                .typ
545                                .clone(),
546                        )
547                    } else {
548                        None
549                    };
550
551                    let literal_constraint =
552                        if let Some(relation_typ) = &persist_fast_path_order_relation_typ {
553                            let mut row = Row::default();
554                            let mut packer = row.packer();
555                            for (idx, col) in relation_typ.column_types.iter().enumerate() {
556                                if !preserves_order(&col.scalar_type) {
557                                    break;
558                                }
559                                let col_expr = MirScalarExpr::column(idx);
560
561                                let Some((literal, _)) = filters
562                                    .iter()
563                                    .filter_map(|f| f.expr_eq_literal(&col_expr))
564                                    .next()
565                                else {
566                                    break;
567                                };
568                                packer.extend_by_row(&literal);
569                            }
570                            if row.is_empty() { None } else { Some(row) }
571                        } else {
572                            None
573                        };
574
575                    let finish_ok = match &finishing {
576                        None => false,
577                        Some(RowSetFinishing {
578                            order_by,
579                            limit,
580                            offset,
581                            ..
582                        }) => {
583                            let order_ok =
584                                if let Some(relation_typ) = &persist_fast_path_order_relation_typ {
585                                    order_by.iter().enumerate().all(|(idx, order)| {
586                                        // Map the ordering column back to the column in the source data.
587                                        // (If it's not one of the input columns, we can't make any guarantees.)
588                                        let column_idx = projection[order.column];
589                                        if column_idx >= safe_mfp.input_arity {
590                                            return false;
591                                        }
592                                        let column_type = &relation_typ.column_types[column_idx];
593                                        let index_ok = idx == column_idx;
594                                        let nulls_ok = !column_type.nullable || order.nulls_last;
595                                        let asc_ok = !order.desc;
596                                        let type_ok = preserves_order(&column_type.scalar_type);
597                                        index_ok && nulls_ok && asc_ok && type_ok
598                                    })
599                                } else {
600                                    order_by.is_empty()
601                                };
602                            let limit_ok = limit.map_or(false, |l| {
603                                usize::cast_from(l) + *offset < persist_fast_path_limit
604                            });
605                            order_ok && limit_ok
606                        }
607                    };
608
609                    let key_constraint = if let Some(literal) = &literal_constraint {
610                        let prefix_len = literal.iter().count();
611                        repr_typ
612                            .keys
613                            .iter()
614                            .any(|k| k.iter().all(|idx| *idx < prefix_len))
615                    } else {
616                        false
617                    };
618
619                    // We can generate a persist peek when:
620                    // - We have a literal constraint that includes an entire key (so we'll return at most one value)
621                    // - We can return the first N key values (no filters, small limit, consistent order)
622                    if key_constraint || (filters.is_empty() && finish_ok) {
623                        return Ok(Some(FastPathPlan::PeekPersist(
624                            *get_id,
625                            literal_constraint,
626                            safe_mfp,
627                        )));
628                    }
629                }
630                MirRelationExpr::Join { implementation, .. } => {
631                    if let mz_expr::JoinImplementation::IndexedFilter(coll_id, idx_id, key, vals) =
632                        implementation
633                    {
634                        return Ok(Some(FastPathPlan::PeekExisting(
635                            *coll_id,
636                            *idx_id,
637                            Some(vals.clone()),
638                            permute_oneshot_mfp_around_index(mfp, key)?,
639                        )));
640                    }
641                }
642                // nothing can be done for non-trivial expressions.
643                _ => {}
644            }
645        }
646    }
647    Ok(None)
648}
649
650impl FastPathPlan {
651    pub fn used_indexes(&self, finishing: Option<&RowSetFinishing>) -> UsedIndexes {
652        match self {
653            FastPathPlan::Constant(..) => UsedIndexes::default(),
654            FastPathPlan::PeekExisting(_coll_id, idx_id, literal_constraints, _mfp) => {
655                if literal_constraints.is_some() {
656                    UsedIndexes::new([(*idx_id, vec![IndexUsageType::Lookup(*idx_id)])].into())
657                } else if finishing.map_or(false, |f| f.limit.is_some() && f.order_by.is_empty()) {
658                    UsedIndexes::new([(*idx_id, vec![IndexUsageType::FastPathLimit])].into())
659                } else {
660                    UsedIndexes::new([(*idx_id, vec![IndexUsageType::FullScan])].into())
661                }
662            }
663            FastPathPlan::PeekPersist(..) => UsedIndexes::default(),
664        }
665    }
666}
667
668impl crate::coord::Coordinator {
669    /// Returns a [`PeekClient`] for coordinator-owned queries, which have to
670    /// run off the main loop because the client calls back into it.
671    ///
672    /// The client holds no session [`Client`](crate::Client), so it does not
673    /// keep the coordinator alive.
674    pub(crate) fn background_peek_client(&self, catalog: &Arc<Catalog>) -> PeekClient {
675        let build_version = catalog.state().config().build_info.human_version(None);
676        PeekClient::new(
677            CoordinatorClient::Background {
678                tx: self.internal_cmd_tx.clone(),
679                metrics: self.metrics.clone(),
680            },
681            catalog,
682            Arc::clone(&self.controller.storage_collections),
683            Arc::clone(&self.transient_id_gen),
684            self.optimizer_metrics.clone(),
685            self.persist_client.clone(),
686            self.statement_logging.create_frontend(build_version),
687            Arc::clone(&self.occ_write_semaphore),
688            self.group_commit_tx.clone(),
689            self.controller.read_only(),
690        )
691    }
692
693    /// Cancel and remove all pending peeks that were initiated by the client with `conn_id`.
694    #[mz_ore::instrument(level = "debug")]
695    pub(crate) fn cancel_pending_peeks(&mut self, conn_id: &ConnectionId) {
696        if let Some(uuids) = self.client_pending_peeks.remove(conn_id) {
697            self.metrics
698                .canceled_peeks
699                .inc_by(u64::cast_from(uuids.len()));
700
701            let mut inverse: BTreeMap<ComputeInstanceId, BTreeSet<Uuid>> = Default::default();
702            for (uuid, compute_instance) in &uuids {
703                inverse.entry(*compute_instance).or_default().insert(*uuid);
704            }
705            for (compute_instance, uuids) in inverse {
706                // It's possible that this compute instance no longer exists because it was dropped
707                // while the peek was in progress. In this case we ignore the error and move on
708                // because the dataflow no longer exists.
709                // TODO(jkosh44) Dropping a cluster should actively cancel all pending queries.
710                for uuid in uuids {
711                    let _ = self.controller.compute.cancel_peek(
712                        compute_instance,
713                        uuid,
714                        PeekResponse::Canceled,
715                    );
716                }
717            }
718
719            let peeks = uuids
720                .iter()
721                .filter_map(|(uuid, _)| self.pending_peeks.remove(uuid))
722                .collect::<Vec<_>>();
723            for peek in peeks {
724                self.retire_execution(
725                    StatementEndedExecutionReason::Canceled,
726                    peek.ctx_extra.defuse(),
727                );
728            }
729        }
730    }
731
732    /// Handle a peek notification and retire the corresponding execution. Does nothing for
733    /// already-removed peeks.
734    pub(crate) fn handle_peek_notification(
735        &mut self,
736        uuid: Uuid,
737        notification: PeekNotification,
738        otel_ctx: OpenTelemetryContext,
739    ) {
740        // We expect exactly one peek response, which we forward. Then we clean up the
741        // peek's state in the coordinator.
742        if let Some(PendingPeek {
743            conn_id: _,
744            cluster_id: _,
745            depends_on: _,
746            ctx_extra,
747            is_fast_path,
748        }) = self.remove_pending_peek(&uuid)
749        {
750            let reason = match notification {
751                PeekNotification::Success {
752                    rows: num_rows,
753                    result_size,
754                } => {
755                    let strategy = if is_fast_path {
756                        StatementExecutionStrategy::FastPath
757                    } else {
758                        StatementExecutionStrategy::Standard
759                    };
760                    StatementEndedExecutionReason::Success {
761                        result_size: Some(result_size),
762                        rows_returned: Some(num_rows),
763                        execution_strategy: Some(strategy),
764                    }
765                }
766                PeekNotification::Error(error) => StatementEndedExecutionReason::Errored { error },
767                PeekNotification::Canceled => StatementEndedExecutionReason::Canceled,
768            };
769            otel_ctx.attach_as_parent();
770            self.retire_execution(reason, ctx_extra.defuse());
771        }
772        // Cancellation may cause us to receive responses for peeks no
773        // longer in `self.pending_peeks`, so we quietly ignore them.
774    }
775
776    /// Clean up a peek's state.
777    pub(crate) fn remove_pending_peek(&mut self, uuid: &Uuid) -> Option<PendingPeek> {
778        let pending_peek = self.pending_peeks.remove(uuid);
779        if let Some(pending_peek) = &pending_peek {
780            let uuids = self
781                .client_pending_peeks
782                .get_mut(&pending_peek.conn_id)
783                .expect("coord peek state is inconsistent");
784            uuids.remove(uuid);
785            if uuids.is_empty() {
786                self.client_pending_peeks.remove(&pending_peek.conn_id);
787            }
788        }
789        pending_peek
790    }
791
792    /// Implements a slow-path peek: ships its transient dataflow, peeks the
793    /// dataflow's index, and registers the pending peek. This is called from the
794    /// command handler for `ExecuteSlowPathPeek`.
795    ///
796    /// On an error return nothing is registered, and the caller logs the
797    /// statement's end. Once registered, `handle_peek_notification` logs it.
798    pub(crate) fn implement_slow_path_peek(
799        &mut self,
800        dataflow_plan: PeekDataflowPlan,
801        determination: TimestampDetermination,
802        finishing: RowSetFinishing,
803        compute_instance: ComputeInstanceId,
804        target_replica: Option<ReplicaId>,
805        intermediate_result_type: SqlRelationType,
806        source_ids: BTreeSet<GlobalId>,
807        conn_id: ConnectionId,
808        max_result_size: u64,
809        max_query_result_size: Option<u64>,
810        watch_set: Option<WatchSetCreation>,
811    ) -> Result<ExecuteResponse, AdapterError> {
812        // Install watch sets for statement lifecycle logging if enabled.
813        // This must happen _before_ creating ExecuteContextExtra, so that if it fails,
814        // we don't have an ExecuteContextExtra that needs to be retired (the frontend
815        // will handle logging for the error case).
816        let statement_logging_id = watch_set.as_ref().map(|ws| ws.logging_id);
817        if let Some(ws) = watch_set {
818            self.install_peek_watch_sets(conn_id.clone(), ws)
819                .map_err(|e| {
820                    AdapterError::concurrent_dependency_drop_from_compute_lookup_error(e)
821                })?;
822        }
823
824        let source_arity = intermediate_result_type.arity();
825        let timestamp = determination.timestamp_context.timestamp_or_default();
826
827        let PeekDataflowPlan {
828            desc: dataflow,
829            // n.b. this index_id identifies a transient index the
830            // caller created, so it is guaranteed to be on
831            // `compute_instance`.
832            id: index_id,
833            key: index_key,
834            permutation: index_permutation,
835            thinned_arity: index_thinned_arity,
836        } = dataflow_plan;
837
838        // The read-hold strategy below acquires a hold for `index_id` only. That
839        // is sufficient today because slow-path peek dataflows have a single
840        // export equal to `index_id`. If we ever ship multi-output dataflows on
841        // this path, the hold acquisition needs to be revisited.
842        let exports: Vec<GlobalId> = dataflow.export_ids().collect();
843        soft_assert_eq_or_log!(
844            exports.as_slice(),
845            &[index_id],
846            "slow-path peek dataflow must export exactly [index_id]",
847        );
848        if exports.as_slice() != [index_id] {
849            return Err(AdapterError::internal(
850                "peek error",
851                format!("slow-path peek dataflow exports {exports:?}, expected [{index_id}]",),
852            ));
853        }
854
855        // Ship the dataflow, then acquire a read hold for the peek target so its
856        // `since` cannot advance past `timestamp` before `compute.peek()` runs: the
857        // implied hold from `create_dataflow` pins the new collection's `since` at
858        // `as_of`, so the subsequent `acquire_read_hold` lands at
859        // `as_of <= timestamp`.
860        self.controller
861            .compute
862            .create_dataflow(compute_instance, dataflow, None)
863            .map_err(AdapterError::concurrent_dependency_drop_from_dataflow_creation_error)?;
864
865        // On failure we must drop the dataflow ourselves, otherwise it leaks.
866        let acquire_result = self
867            .controller
868            .compute
869            .acquire_read_hold(compute_instance, index_id)
870            .map_err(AdapterError::concurrent_dependency_drop_from_collection_update_error);
871        let read_hold = match acquire_result {
872            Ok(hold) => hold,
873            Err(e) => {
874                self.drop_compute_collections(vec![(compute_instance, index_id)]);
875                return Err(e);
876            }
877        };
878
879        // Create an identity MFP operator.
880        let mut map_filter_project = mz_expr::MapFilterProject::new(source_arity);
881        map_filter_project.permute_fn(
882            |c| index_permutation[c],
883            index_key.len() + index_thinned_arity,
884        );
885        let map_filter_project = mfp_to_safe_plan(map_filter_project)?;
886
887        // Endpoints for sending and receiving peek responses.
888        let (rows_tx, rows_rx) = tokio::sync::oneshot::channel();
889
890        // Generate unique UUID. Guaranteed to be unique to all pending peeks, there's an very
891        // small but unlikely chance that it's not unique to completed peeks.
892        let mut uuid = Uuid::new_v4();
893        while self.pending_peeks.contains_key(&uuid) {
894            uuid = Uuid::new_v4();
895        }
896
897        // At this stage we don't know column names for the result because we
898        // only know the peek's result type as a bare SqlRelationType.
899        let peek_result_column_names =
900            (0..intermediate_result_type.arity()).map(|i| format!("peek_{i}"));
901        let peek_result_desc =
902            RelationDesc::new(intermediate_result_type, peek_result_column_names);
903
904        let peek_result = self
905            .controller
906            .compute
907            .peek(
908                compute_instance,
909                PeekTarget::Index { id: index_id },
910                None,
911                uuid,
912                timestamp,
913                peek_result_desc,
914                finishing.clone(),
915                map_filter_project,
916                read_hold,
917                target_replica,
918                rows_tx,
919            )
920            .map_err(AdapterError::concurrent_dependency_drop_from_peek_error);
921        if let Err(e) = peek_result {
922            // Drop the transient dataflow shipped above to avoid leaking it.
923            self.drop_compute_collections(vec![(compute_instance, index_id)]);
924            return Err(e);
925        }
926
927        // Register the pending peek only after compute.peek() succeeds. If it
928        // fails (e.g. concurrent replica/cluster drop), inserting first would
929        // leak entries in these maps and misattribute statement execution reasons.
930        self.pending_peeks.insert(
931            uuid,
932            PendingPeek {
933                conn_id: conn_id.clone(),
934                cluster_id: compute_instance,
935                depends_on: source_ids,
936                ctx_extra: ExecuteContextGuard::new(
937                    statement_logging_id,
938                    self.internal_cmd_tx.clone(),
939                ),
940                is_fast_path: false,
941            },
942        );
943        self.client_pending_peeks
944            .entry(conn_id)
945            .or_default()
946            .insert(uuid, compute_instance);
947
948        let duration_histogram = self.metrics.row_set_finishing_seconds();
949
950        // Drop the dataflow now that the peek is queued. This is required:
951        // `add_collection` installs implied/warmup holds owned by the controller
952        // and only released via `drop_collections`, so without this call the
953        // transient dataflow's `since` would stay pinned at `as_of` forever. The
954        // peek's own read hold keeps the collection alive on the cluster until
955        // the response arrives.
956        self.drop_compute_collections(vec![(compute_instance, index_id)]);
957
958        let persist_client = self.persist_client.clone();
959        let peek_stash_read_batch_size_bytes =
960            mz_compute_types::dyncfgs::PEEK_RESPONSE_STASH_READ_BATCH_SIZE_BYTES
961                .get(self.catalog().system_config().dyncfgs());
962        let peek_stash_read_memory_budget_bytes =
963            mz_compute_types::dyncfgs::PEEK_RESPONSE_STASH_READ_MEMORY_BUDGET_BYTES
964                .get(self.catalog().system_config().dyncfgs());
965
966        let peek_response_stream = crate::peek_client::create_peek_response_stream(
967            rows_rx,
968            finishing,
969            max_result_size,
970            max_query_result_size,
971            duration_histogram,
972            persist_client,
973            peek_stash_read_batch_size_bytes,
974            peek_stash_read_memory_budget_bytes,
975        );
976
977        Ok(crate::ExecuteResponse::SendingRowsStreaming {
978            rows: Box::pin(peek_response_stream),
979            instance_id: compute_instance,
980            strategy: StatementExecutionStrategy::Standard,
981        })
982    }
983
984    /// Implements a `COPY TO` command by installing peek watch sets,
985    /// shipping the dataflow, and spawning a background task to wait for completion.
986    /// This is called from the command handler for ExecuteCopyTo.
987    ///
988    /// (The S3 preflight check must be completed successfully via the
989    /// `CopyToPreflight` command _before_ calling this method. The preflight is
990    /// handled separately to avoid blocking the coordinator's main task with
991    /// slow S3 network operations.)
992    ///
993    /// This method does NOT block waiting for completion. Instead, it spawns a background task that
994    /// will send the response through the provided tx channel when the COPY TO completes.
995    /// All errors (setup or execution) are sent through tx.
996    pub(crate) async fn implement_copy_to(
997        &mut self,
998        df_desc: mz_compute_types::dataflows::LirDataflowDescription,
999        compute_instance: ComputeInstanceId,
1000        target_replica: Option<ReplicaId>,
1001        source_ids: BTreeSet<GlobalId>,
1002        conn_id: ConnectionId,
1003        watch_set: Option<WatchSetCreation>,
1004        tx: oneshot::Sender<Result<ExecuteResponse, AdapterError>>,
1005    ) {
1006        // Helper to send error and return early
1007        let send_err = |tx: oneshot::Sender<Result<ExecuteResponse, AdapterError>>,
1008                        e: AdapterError| {
1009            let _ = tx.send(Err(e));
1010        };
1011
1012        // Install watch sets for statement lifecycle logging if enabled.
1013        // If this fails, we just send the error back. The frontend will handle logging
1014        // for the error case (no ExecuteContextExtra is created here).
1015        if let Some(ws) = watch_set {
1016            if let Err(e) = self.install_peek_watch_sets(conn_id.clone(), ws) {
1017                let err = AdapterError::concurrent_dependency_drop_from_compute_lookup_error(e);
1018                send_err(tx, err);
1019                return;
1020            }
1021        }
1022
1023        // Note: We don't create an ExecuteContextExtra here because the frontend handles
1024        // all statement logging for COPY TO operations.
1025
1026        let sink_id = df_desc.sink_id();
1027
1028        // Create and register ActiveCopyTo.
1029        // Note: sink_tx/sink_rx is the channel for the compute sink to notify completion
1030        // This is different from the command's tx which sends the response to the client
1031        let (sink_tx, sink_rx) = oneshot::channel();
1032        let active_copy_to = ActiveCopyTo {
1033            conn_id: conn_id.clone(),
1034            tx: sink_tx,
1035            cluster_id: compute_instance,
1036            depends_on: source_ids,
1037        };
1038
1039        // Add metadata for the new COPY TO. CopyTo returns a `ready` future, so it is safe to drop.
1040        drop(self.add_active_compute_sink(sink_id, ActiveComputeSink::CopyTo(active_copy_to)));
1041
1042        // Try to ship the dataflow. We handle errors gracefully because dependencies might have
1043        // disappeared during sequencing.
1044        if let Err(e) = self
1045            .try_ship_dataflow(df_desc, compute_instance, target_replica)
1046            .await
1047            .map_err(AdapterError::concurrent_dependency_drop_from_dataflow_creation_error)
1048        {
1049            // Clean up the active compute sink that was added above, since the dataflow was never
1050            // created. If we don't do this, the sink_id remains in drop_sinks but no collection
1051            // exists in the compute controller, causing a panic when the connection terminates.
1052            self.remove_active_compute_sink(sink_id).await;
1053            send_err(tx, e);
1054            return;
1055        }
1056
1057        // Spawn background task to wait for completion
1058        // We must NOT await sink_rx here directly, as that would block the coordinator's main task
1059        // from processing the completion message. Instead, we spawn a background task that will
1060        // send the result through tx when the COPY TO completes.
1061        let span = Span::current();
1062        task::spawn(
1063            || "copy to completion",
1064            async move {
1065                let res = sink_rx.await;
1066                let result = match res {
1067                    Ok(res) => res,
1068                    Err(_) => Err(AdapterError::Internal("copy to sender dropped".into())),
1069                };
1070
1071                let _ = tx.send(result);
1072            }
1073            .instrument(span),
1074        );
1075    }
1076
1077    /// Constructs an [`ExecuteResponse`] that that will send some rows to the
1078    /// client immediately, as opposed to asking the dataflow layer to send along
1079    /// the rows after some computation.
1080    pub(crate) fn send_immediate_rows<I>(rows: I) -> ExecuteResponse
1081    where
1082        I: IntoRowIterator,
1083        I::Iter: Send + Sync + 'static,
1084    {
1085        let rows = Box::new(rows.into_row_iter());
1086        ExecuteResponse::SendingRowsImmediate { rows }
1087    }
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092    use mz_expr::func::IsNull;
1093    use mz_expr::{MapFilterProject, UnaryFunc};
1094    use mz_ore::str::Indent;
1095    use mz_repr::explain::text::text_string_at;
1096    use mz_repr::explain::{DummyHumanizer, ExplainConfig, PlanRenderingContext};
1097    use mz_repr::{Datum, SqlColumnType, SqlScalarType};
1098
1099    use super::*;
1100
1101    #[mz_ore::test]
1102    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1103    fn test_fast_path_plan_as_text() {
1104        let typ = SqlRelationType::new(vec![SqlColumnType {
1105            scalar_type: SqlScalarType::String,
1106            nullable: false,
1107        }]);
1108        let constant_err = FastPathPlan::Constant(Err(EvalError::DivisionByZero), typ.clone());
1109        let no_lookup = FastPathPlan::PeekExisting(
1110            GlobalId::User(8),
1111            GlobalId::User(10),
1112            None,
1113            MapFilterProject::new(4)
1114                .map(Some(MirScalarExpr::column(0).or(MirScalarExpr::column(2))))
1115                .project([1, 4])
1116                .into_plan()
1117                .expect("invalid plan")
1118                .into_nontemporal()
1119                .expect("invalid nontemporal"),
1120        );
1121        let lookup = FastPathPlan::PeekExisting(
1122            GlobalId::User(9),
1123            GlobalId::User(11),
1124            Some(vec![Row::pack(Some(Datum::Int32(5)))]),
1125            MapFilterProject::new(3)
1126                .filter(Some(
1127                    MirScalarExpr::column(0).call_unary(UnaryFunc::IsNull(IsNull)),
1128                ))
1129                .into_plan()
1130                .expect("invalid plan")
1131                .into_nontemporal()
1132                .expect("invalid nontemporal"),
1133        );
1134
1135        let humanizer = DummyHumanizer;
1136        let config = ExplainConfig {
1137            redacted: false,
1138            verbose_syntax: true,
1139            ..Default::default()
1140        };
1141        let ctx_gen = || {
1142            let indent = Indent::default();
1143            let annotations = BTreeMap::new();
1144            PlanRenderingContext::<FastPathPlan>::new(
1145                indent,
1146                &humanizer,
1147                annotations,
1148                &config,
1149                BTreeSet::default(),
1150            )
1151        };
1152
1153        let constant_err_exp = "Error \"division by zero\"\n";
1154        let no_lookup_exp = "Project (#1, #4)\n  Map ((#0 OR #2))\n    ReadIndex on=u8 [DELETED INDEX]=[*** full scan ***]\n";
1155        let lookup_exp =
1156            "Filter (#0) IS NULL\n  ReadIndex on=u9 [DELETED INDEX]=[lookup value=(5)]\n";
1157
1158        assert_eq!(text_string_at(&constant_err, ctx_gen), constant_err_exp);
1159        assert_eq!(text_string_at(&no_lookup, ctx_gen), no_lookup_exp);
1160        assert_eq!(text_string_at(&lookup, ctx_gen), lookup_exp);
1161
1162        let mut constant_rows = vec![
1163            (Row::pack(Some(Datum::String("hello"))), Diff::ONE),
1164            (Row::pack(Some(Datum::String("world"))), 2.into()),
1165            (Row::pack(Some(Datum::String("star"))), 500.into()),
1166        ];
1167        let constant_exp1 =
1168            "Constant\n  - (\"hello\")\n  - ((\"world\") x 2)\n  - ((\"star\") x 500)\n";
1169        assert_eq!(
1170            text_string_at(
1171                &FastPathPlan::Constant(Ok(constant_rows.clone()), typ.clone()),
1172                ctx_gen
1173            ),
1174            constant_exp1
1175        );
1176        constant_rows
1177            .extend((0..20).map(|i| (Row::pack(Some(Datum::String(&i.to_string()))), Diff::ONE)));
1178        let constant_exp2 = "Constant\n  total_rows (diffs absed): 523\n  first_rows:\n    - (\"hello\")\
1179        \n    - ((\"world\") x 2)\n    - ((\"star\") x 500)\n    - (\"0\")\n    - (\"1\")\
1180        \n    - (\"2\")\n    - (\"3\")\n    - (\"4\")\n    - (\"5\")\n    - (\"6\")\
1181        \n    - (\"7\")\n    - (\"8\")\n    - (\"9\")\n    - (\"10\")\n    - (\"11\")\
1182        \n    - (\"12\")\n    - (\"13\")\n    - (\"14\")\n    - (\"15\")\n    - (\"16\")\n";
1183        assert_eq!(
1184            text_string_at(&FastPathPlan::Constant(Ok(constant_rows), typ), ctx_gen),
1185            constant_exp2
1186        );
1187    }
1188}