1use std::collections::{BTreeMap, BTreeSet};
16use std::fmt;
17use std::num::NonZeroUsize;
18use std::ops::Deref;
19use std::sync::Arc;
20
21use differential_dataflow::consolidation::consolidate;
22use itertools::Itertools;
23use mz_adapter_types::connection::ConnectionId;
24use mz_cluster_client::ReplicaId;
25use mz_compute_client::controller::PeekNotification;
26use mz_compute_client::protocol::command::PeekTarget;
27use mz_compute_client::protocol::response::PeekResponse;
28use mz_compute_types::ComputeInstanceId;
29use mz_compute_types::dataflows::{DataflowDescription, IndexImport};
30use mz_controller_types::ClusterId;
31use mz_expr::explain::{HumanizedExplain, HumanizerMode, fmt_text_constant_rows};
32use mz_expr::row::RowCollection;
33use mz_expr::{
34 EvalError, Id, MirRelationExpr, MirScalarExpr, OptimizedMirRelationExpr, RowSetFinishing,
35 RowSetFinishingIncremental, permutation_for_arrangement,
36};
37use mz_ore::cast::CastFrom;
38use mz_ore::collections::CollectionExt;
39use mz_ore::soft_assert_eq_or_log;
40use mz_ore::str::{StrExt, separated};
41use mz_ore::task;
42use mz_ore::tracing::OpenTelemetryContext;
43use mz_persist_client::Schemas;
44use mz_persist_types::codec_impls::UnitSchema;
45use mz_repr::explain::text::DisplayText;
46use mz_repr::explain::{CompactScalars, IndexUsageType, PlanRenderingContext, UsedIndexes};
47use mz_repr::{
48 Diff, GlobalId, IntoRowIterator, RelationDesc, Row, RowIterator, SqlRelationType,
49 preserves_order,
50};
51use mz_storage_types::sources::SourceData;
52use serde::{Deserialize, Serialize};
53use timely::progress::Antichain;
54use tokio::sync::oneshot;
55use tracing::{Instrument, Span};
56use uuid::Uuid;
57
58use crate::active_compute_sink::{ActiveComputeSink, ActiveCopyTo};
59use crate::coord::timestamp_selection::TimestampDetermination;
60use crate::optimize::OptimizerError;
61use crate::statement_logging::WatchSetCreation;
62use crate::statement_logging::{StatementEndedExecutionReason, StatementExecutionStrategy};
63use crate::{AdapterError, ExecuteContextGuard, ExecuteResponse};
64
65#[derive(Debug)]
67pub(crate) struct PendingPeek {
68 pub(crate) conn_id: ConnectionId,
70 pub(crate) cluster_id: ClusterId,
72 pub(crate) depends_on: BTreeSet<GlobalId>,
74 pub(crate) ctx_extra: ExecuteContextGuard,
77 pub(crate) is_fast_path: bool,
79}
80
81#[derive(Debug)]
86pub enum PeekResponseUnary {
87 Rows(Box<dyn RowIterator + Send + Sync>),
88 Error(AdapterError),
89 Canceled,
90 DependencyDropped(DroppedDependency),
92}
93
94#[derive(Clone, Debug)]
100pub enum DroppedDependency {
101 Relation { name: String },
102 Cluster { name: String },
103}
104
105impl fmt::Display for DroppedDependency {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 match self {
108 Self::Relation { name } => write!(f, "relation {}", name.quoted()),
109 Self::Cluster { name } => write!(f, "cluster {}", name.quoted()),
110 }
111 }
112}
113
114impl DroppedDependency {
115 pub fn query_terminated_error(&self) -> String {
118 format!("query could not complete because {self} was dropped")
119 }
120
121 pub fn to_concurrent_dependency_drop(&self) -> AdapterError {
123 let (kind, name) = match self {
124 Self::Relation { name } => ("relation", name.clone()),
125 Self::Cluster { name } => ("cluster", name.clone()),
126 };
127 AdapterError::ConcurrentDependencyDrop {
128 dependency_kind: kind,
129 dependency_id: name,
130 }
131 }
132}
133
134#[derive(Clone, Debug)]
135pub struct PeekDataflowPlan {
136 pub(crate) desc: DataflowDescription<mz_compute_types::plan::LirRelationExpr, ()>,
137 pub(crate) id: GlobalId,
138 key: Vec<MirScalarExpr>,
139 permutation: Vec<usize>,
140 thinned_arity: usize,
141}
142
143impl PeekDataflowPlan {
144 pub fn new(
145 desc: DataflowDescription<mz_compute_types::plan::LirRelationExpr, ()>,
146 id: GlobalId,
147 typ: &SqlRelationType,
148 ) -> Self {
149 let arity = typ.arity();
150 let key = typ
151 .default_key()
152 .into_iter()
153 .map(MirScalarExpr::column)
154 .collect::<Vec<_>>();
155 let (permutation, thinning) = permutation_for_arrangement(&key, arity);
156 Self {
157 desc,
158 id,
159 key,
160 permutation,
161 thinned_arity: thinning.len(),
162 }
163 }
164}
165
166#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd)]
167pub enum FastPathPlan {
168 Constant(Result<Vec<(Row, Diff)>, EvalError>, SqlRelationType),
173 PeekExisting(GlobalId, GlobalId, Option<Vec<Row>>, mz_expr::SafeMfpPlan),
176 PeekPersist(GlobalId, Option<Row>, mz_expr::SafeMfpPlan),
178}
179
180impl<'a, T: 'a> DisplayText<PlanRenderingContext<'a, T>> for FastPathPlan {
181 fn fmt_text(
182 &self,
183 f: &mut fmt::Formatter<'_>,
184 ctx: &mut PlanRenderingContext<'a, T>,
185 ) -> fmt::Result {
186 if ctx.config.verbose_syntax {
187 self.fmt_verbose_text(f, ctx)
188 } else {
189 self.fmt_default_text(f, ctx)
190 }
191 }
192}
193
194impl FastPathPlan {
195 pub fn fmt_default_text<'a, T>(
196 &self,
197 f: &mut fmt::Formatter<'_>,
198 ctx: &mut PlanRenderingContext<'a, T>,
199 ) -> fmt::Result {
200 let mode = HumanizedExplain::new(ctx.config.redacted);
201
202 match self {
203 FastPathPlan::Constant(rows, _) => {
204 write!(f, "{}→Constant ", ctx.indent)?;
205
206 match rows {
207 Ok(rows) => writeln!(f, "({} rows)", rows.len())?,
208 Err(err) => {
209 if mode.redacted() {
210 writeln!(f, "(error: █)")?;
211 } else {
212 writeln!(f, "(error: {})", err.to_string().quoted(),)?;
213 }
214 }
215 }
216 }
217 FastPathPlan::PeekExisting(coll_id, idx_id, literal_constraints, mfp) => {
218 let coll = ctx
219 .humanizer
220 .humanize_id(*coll_id)
221 .unwrap_or_else(|| coll_id.to_string());
222 let idx = ctx
223 .humanizer
224 .humanize_id(*idx_id)
225 .unwrap_or_else(|| idx_id.to_string());
226 writeln!(f, "{}→Map/Filter/Project", ctx.indent)?;
227 ctx.indent.set();
228
229 ctx.indent += 1;
230
231 mode.expr(mfp.deref(), None).fmt_default_text(f, ctx)?;
232 let printed = !mfp.expressions.is_empty() || !mfp.predicates.is_empty();
233
234 if printed {
235 ctx.indent += 1;
236 }
237 if let Some(literal_constraints) = literal_constraints {
238 writeln!(f, "{}→Index Lookup on {coll} (using {idx})", ctx.indent)?;
239 ctx.indent += 1;
240 let values = separated("; ", mode.seq(literal_constraints, None));
241 writeln!(f, "{}Lookup values: {values}", ctx.indent)?;
242 } else {
243 writeln!(f, "{}→Indexed {coll} (using {idx})", ctx.indent)?;
244 }
245
246 ctx.indent.reset();
247 }
248 FastPathPlan::PeekPersist(global_id, literal_constraint, mfp) => {
249 let coll = ctx
250 .humanizer
251 .humanize_id(*global_id)
252 .unwrap_or_else(|| global_id.to_string());
253 writeln!(f, "{}→Map/Filter/Project", ctx.indent)?;
254 ctx.indent.set();
255
256 ctx.indent += 1;
257
258 mode.expr(mfp.deref(), None).fmt_default_text(f, ctx)?;
259 let printed = !mfp.expressions.is_empty() || !mfp.predicates.is_empty();
260
261 if printed {
262 ctx.indent += 1;
263 }
264 if let Some(literal_constraint) = literal_constraint {
265 writeln!(f, "{}→ReadStorage Lookup on {coll}", ctx.indent)?;
266 ctx.indent += 1;
267 let value = mode.expr(literal_constraint, None);
268 writeln!(f, "{}Lookup value: {value}", ctx.indent)?;
269 } else {
270 writeln!(f, "{}→ReadStorage {coll}", ctx.indent)?;
271 }
272
273 ctx.indent.reset();
274 }
275 }
276
277 Ok(())
278 }
279
280 pub fn fmt_verbose_text<'a, T>(
281 &self,
282 f: &mut fmt::Formatter<'_>,
283 ctx: &mut PlanRenderingContext<'a, T>,
284 ) -> fmt::Result {
285 let redacted = ctx.config.redacted;
286 let mode = HumanizedExplain::new(redacted);
287
288 match self {
291 FastPathPlan::Constant(Ok(rows), _) => {
292 if !rows.is_empty() {
293 writeln!(f, "{}Constant", ctx.indent)?;
294 *ctx.as_mut() += 1;
295 fmt_text_constant_rows(
296 f,
297 rows.iter().map(|(row, diff)| (row, diff)),
298 ctx.as_mut(),
299 redacted,
300 )?;
301 *ctx.as_mut() -= 1;
302 } else {
303 writeln!(f, "{}Constant <empty>", ctx.as_mut())?;
304 }
305 Ok(())
306 }
307 FastPathPlan::Constant(Err(err), _) => {
308 if redacted {
309 writeln!(f, "{}Error █", ctx.as_mut())
310 } else {
311 writeln!(f, "{}Error {}", ctx.as_mut(), err.to_string().escaped())
312 }
313 }
314 FastPathPlan::PeekExisting(coll_id, idx_id, literal_constraints, mfp) => {
315 ctx.as_mut().set();
316 let (map, filter, project) = mfp.as_map_filter_project();
317
318 let cols = if !ctx.config.humanized_exprs {
319 None
320 } else if let Some(cols) = ctx.humanizer.column_names_for_id(*idx_id) {
321 let cols = itertools::chain(
325 cols.iter().cloned(),
326 std::iter::repeat(String::new()).take(map.len()),
327 )
328 .collect();
329 Some(cols)
330 } else {
331 None
332 };
333
334 if project.len() != mfp.input_arity + map.len()
335 || !project.iter().enumerate().all(|(i, o)| i == *o)
336 {
337 let outputs = mode.seq(&project, cols.as_ref());
338 let outputs = CompactScalars(outputs);
339 writeln!(f, "{}Project ({})", ctx.as_mut(), outputs)?;
340 *ctx.as_mut() += 1;
341 }
342 if !filter.is_empty() {
343 let predicates = separated(" AND ", mode.seq(&filter, cols.as_ref()));
344 writeln!(f, "{}Filter {}", ctx.as_mut(), predicates)?;
345 *ctx.as_mut() += 1;
346 }
347 if !map.is_empty() {
348 let scalars = mode.seq(&map, cols.as_ref());
349 let scalars = CompactScalars(scalars);
350 writeln!(f, "{}Map ({})", ctx.as_mut(), scalars)?;
351 *ctx.as_mut() += 1;
352 }
353 MirRelationExpr::fmt_indexed_filter(
354 f,
355 ctx,
356 coll_id,
357 idx_id,
358 literal_constraints.clone(),
359 None,
360 )?;
361 writeln!(f)?;
362 ctx.as_mut().reset();
363 Ok(())
364 }
365 FastPathPlan::PeekPersist(gid, literal_constraint, mfp) => {
366 ctx.as_mut().set();
367 let (map, filter, project) = mfp.as_map_filter_project();
368
369 let cols = if !ctx.config.humanized_exprs {
370 None
371 } else if let Some(cols) = ctx.humanizer.column_names_for_id(*gid) {
372 let cols = itertools::chain(
373 cols.iter().cloned(),
374 std::iter::repeat(String::new()).take(map.len()),
375 )
376 .collect::<Vec<_>>();
377 Some(cols)
378 } else {
379 None
380 };
381
382 if project.len() != mfp.input_arity + map.len()
383 || !project.iter().enumerate().all(|(i, o)| i == *o)
384 {
385 let outputs = mode.seq(&project, cols.as_ref());
386 let outputs = CompactScalars(outputs);
387 writeln!(f, "{}Project ({})", ctx.as_mut(), outputs)?;
388 *ctx.as_mut() += 1;
389 }
390 if !filter.is_empty() {
391 let predicates = separated(" AND ", mode.seq(&filter, cols.as_ref()));
392 writeln!(f, "{}Filter {}", ctx.as_mut(), predicates)?;
393 *ctx.as_mut() += 1;
394 }
395 if !map.is_empty() {
396 let scalars = mode.seq(&map, cols.as_ref());
397 let scalars = CompactScalars(scalars);
398 writeln!(f, "{}Map ({})", ctx.as_mut(), scalars)?;
399 *ctx.as_mut() += 1;
400 }
401 let human_id = ctx
402 .humanizer
403 .humanize_id(*gid)
404 .unwrap_or_else(|| gid.to_string());
405 write!(f, "{}PeekPersist {human_id}", ctx.as_mut())?;
406 if let Some(literal) = literal_constraint {
407 let value = mode.expr(literal, None);
408 writeln!(f, " [value={}]", value)?;
409 } else {
410 writeln!(f, "")?;
411 }
412 ctx.as_mut().reset();
413 Ok(())
414 }
415 }?;
416 Ok(())
417 }
418}
419
420#[derive(Debug)]
421pub struct PlannedPeek {
422 pub plan: PeekPlan,
423 pub determination: TimestampDetermination,
424 pub conn_id: ConnectionId,
425 pub intermediate_result_type: SqlRelationType,
432 pub source_arity: usize,
433 pub source_ids: BTreeSet<GlobalId>,
434}
435
436#[derive(Clone, Debug)]
438pub enum PeekPlan {
439 FastPath(FastPathPlan),
440 SlowPath(PeekDataflowPlan),
442}
443
444fn mfp_to_safe_plan(
449 mfp: mz_expr::MapFilterProject,
450) -> Result<mz_expr::SafeMfpPlan, OptimizerError> {
451 mfp.into_plan()
452 .map_err(OptimizerError::InternalUnsafeMfpPlan)?
453 .into_nontemporal()
454 .map_err(|e| OptimizerError::InternalUnsafeMfpPlan(format!("{:?}", e)))
455}
456
457fn permute_oneshot_mfp_around_index(
459 mfp: mz_expr::MapFilterProject,
460 key: &[MirScalarExpr],
461) -> Result<mz_expr::SafeMfpPlan, OptimizerError> {
462 let input_arity = mfp.input_arity;
463 let mut safe_mfp = mfp_to_safe_plan(mfp)?;
464 let (permute, thinning) = permutation_for_arrangement(key, input_arity);
465 safe_mfp.permute_fn(|c| permute[c], key.len() + thinning.len());
466 Ok(safe_mfp)
467}
468
469pub fn create_fast_path_plan(
475 dataflow_plan: &mut DataflowDescription<OptimizedMirRelationExpr>,
476 view_id: GlobalId,
477 finishing: Option<&RowSetFinishing>,
478 persist_fast_path_limit: usize,
479 persist_fast_path_order: bool,
480) -> Result<Option<FastPathPlan>, OptimizerError> {
481 if dataflow_plan.objects_to_build.len() >= 1 && dataflow_plan.objects_to_build[0].id == view_id
487 {
488 let mut mir = &*dataflow_plan.objects_to_build[0].plan.as_inner_mut();
489 if let Some((rows, found_typ)) = mir.as_const() {
490 let plan = FastPathPlan::Constant(
492 rows.clone(),
493 mz_repr::SqlRelationType::from_repr(found_typ),
494 );
495 return Ok(Some(plan));
496 } else {
497 if let MirRelationExpr::TopK {
500 input,
501 group_key,
502 order_key,
503 limit,
504 offset,
505 monotonic: _,
506 expected_group_size: _,
507 } = mir
508 {
509 if let Some(finishing) = finishing {
510 if group_key.is_empty() && *order_key == finishing.order_by && *offset == 0 {
511 let finishing_limits_at_least_as_topk = match (limit, finishing.limit) {
514 (None, _) => true,
515 (Some(..), None) => false,
516 (Some(topk_limit), Some(finishing_limit)) => {
517 if let Some(l) = topk_limit.as_literal_int64() {
518 i128::cast_from(l)
519 >= i128::cast_from(*finishing_limit)
520 + i128::cast_from(finishing.offset)
521 } else {
522 false
523 }
524 }
525 };
526 if finishing_limits_at_least_as_topk {
527 mir = input;
528 }
529 }
530 }
531 }
532 let (mfp, mir) = mz_expr::MapFilterProject::extract_from_expression(mir);
536 match mir {
537 MirRelationExpr::Get {
538 id: Id::Global(get_id),
539 typ: repr_typ,
540 ..
541 } => {
542 for (index_id, IndexImport { desc, .. }) in dataflow_plan.index_imports.iter() {
544 if desc.on_id == *get_id {
545 return Ok(Some(FastPathPlan::PeekExisting(
546 *get_id,
547 *index_id,
548 None,
549 permute_oneshot_mfp_around_index(mfp, &desc.key)?,
550 )));
551 }
552 }
553
554 let safe_mfp = mfp_to_safe_plan(mfp)?;
558 let (_maps, filters, projection) = safe_mfp.as_map_filter_project();
559
560 let persist_fast_path_order_relation_typ = if persist_fast_path_order {
561 Some(
562 dataflow_plan
563 .source_imports
564 .get(get_id)
565 .expect("Get's ID is also imported")
566 .desc
567 .typ
568 .clone(),
569 )
570 } else {
571 None
572 };
573
574 let literal_constraint =
575 if let Some(relation_typ) = &persist_fast_path_order_relation_typ {
576 let mut row = Row::default();
577 let mut packer = row.packer();
578 for (idx, col) in relation_typ.column_types.iter().enumerate() {
579 if !preserves_order(&col.scalar_type) {
580 break;
581 }
582 let col_expr = MirScalarExpr::column(idx);
583
584 let Some((literal, _)) = filters
585 .iter()
586 .filter_map(|f| f.expr_eq_literal(&col_expr))
587 .next()
588 else {
589 break;
590 };
591 packer.extend_by_row(&literal);
592 }
593 if row.is_empty() { None } else { Some(row) }
594 } else {
595 None
596 };
597
598 let finish_ok = match &finishing {
599 None => false,
600 Some(RowSetFinishing {
601 order_by,
602 limit,
603 offset,
604 ..
605 }) => {
606 let order_ok =
607 if let Some(relation_typ) = &persist_fast_path_order_relation_typ {
608 order_by.iter().enumerate().all(|(idx, order)| {
609 let column_idx = projection[order.column];
612 if column_idx >= safe_mfp.input_arity {
613 return false;
614 }
615 let column_type = &relation_typ.column_types[column_idx];
616 let index_ok = idx == column_idx;
617 let nulls_ok = !column_type.nullable || order.nulls_last;
618 let asc_ok = !order.desc;
619 let type_ok = preserves_order(&column_type.scalar_type);
620 index_ok && nulls_ok && asc_ok && type_ok
621 })
622 } else {
623 order_by.is_empty()
624 };
625 let limit_ok = limit.map_or(false, |l| {
626 usize::cast_from(l) + *offset < persist_fast_path_limit
627 });
628 order_ok && limit_ok
629 }
630 };
631
632 let key_constraint = if let Some(literal) = &literal_constraint {
633 let prefix_len = literal.iter().count();
634 repr_typ
635 .keys
636 .iter()
637 .any(|k| k.iter().all(|idx| *idx < prefix_len))
638 } else {
639 false
640 };
641
642 if key_constraint || (filters.is_empty() && finish_ok) {
646 return Ok(Some(FastPathPlan::PeekPersist(
647 *get_id,
648 literal_constraint,
649 safe_mfp,
650 )));
651 }
652 }
653 MirRelationExpr::Join { implementation, .. } => {
654 if let mz_expr::JoinImplementation::IndexedFilter(coll_id, idx_id, key, vals) =
655 implementation
656 {
657 return Ok(Some(FastPathPlan::PeekExisting(
658 *coll_id,
659 *idx_id,
660 Some(vals.clone()),
661 permute_oneshot_mfp_around_index(mfp, key)?,
662 )));
663 }
664 }
665 _ => {}
667 }
668 }
669 }
670 Ok(None)
671}
672
673impl FastPathPlan {
674 pub fn used_indexes(&self, finishing: Option<&RowSetFinishing>) -> UsedIndexes {
675 match self {
676 FastPathPlan::Constant(..) => UsedIndexes::default(),
677 FastPathPlan::PeekExisting(_coll_id, idx_id, literal_constraints, _mfp) => {
678 if literal_constraints.is_some() {
679 UsedIndexes::new([(*idx_id, vec![IndexUsageType::Lookup(*idx_id)])].into())
680 } else if finishing.map_or(false, |f| f.limit.is_some() && f.order_by.is_empty()) {
681 UsedIndexes::new([(*idx_id, vec![IndexUsageType::FastPathLimit])].into())
682 } else {
683 UsedIndexes::new([(*idx_id, vec![IndexUsageType::FullScan])].into())
684 }
685 }
686 FastPathPlan::PeekPersist(..) => UsedIndexes::default(),
687 }
688 }
689}
690
691impl crate::coord::Coordinator {
692 #[mz_ore::instrument(level = "debug")]
703 pub async fn implement_peek_plan(
704 &mut self,
705 ctx_extra: &mut ExecuteContextGuard,
706 plan: PlannedPeek,
707 finishing: RowSetFinishing,
708 compute_instance: ComputeInstanceId,
709 target_replica: Option<ReplicaId>,
710 max_result_size: u64,
711 max_returned_query_size: Option<u64>,
712 ) -> Result<ExecuteResponse, AdapterError> {
713 let PlannedPeek {
714 plan: fast_path,
715 determination,
716 conn_id,
717 intermediate_result_type,
718 source_arity,
719 source_ids,
720 } = plan;
721
722 if let PeekPlan::FastPath(FastPathPlan::Constant(rows, _)) = fast_path {
724 let mut rows = match rows {
725 Ok(rows) => rows,
726 Err(e) => return Err(e.into()),
727 };
728 consolidate(&mut rows);
730
731 let mut results = Vec::new();
732 for (row, count) in rows {
733 if count.is_negative() {
734 Err(EvalError::InvalidParameterValue(
735 format!("Negative multiplicity in constant result: {}", count).into(),
736 ))?
737 };
738 if count.is_positive() {
739 let count = usize::cast_from(
740 u64::try_from(count.into_inner())
741 .expect("known to be positive from check above"),
742 );
743 results.push((
744 row,
745 NonZeroUsize::new(count).expect("known to be non-zero from check above"),
746 ));
747 }
748 }
749 let row_collection = RowCollection::new(results, &finishing.order_by);
750 let duration_histogram = self.metrics.row_set_finishing_seconds();
751
752 let (ret, reason) = match finishing.finish(
753 row_collection,
754 max_result_size,
755 max_returned_query_size,
756 &duration_histogram,
757 ) {
758 Ok((rows, row_size_bytes)) => {
759 let result_size = u64::cast_from(row_size_bytes);
760 let rows_returned = u64::cast_from(rows.count());
761 (
762 Ok(Self::send_immediate_rows(rows)),
763 StatementEndedExecutionReason::Success {
764 result_size: Some(result_size),
765 rows_returned: Some(rows_returned),
766 execution_strategy: Some(StatementExecutionStrategy::Constant),
767 },
768 )
769 }
770 Err(error) => (
771 Err(AdapterError::ResultSize(error.clone())),
772 StatementEndedExecutionReason::Errored { error },
773 ),
774 };
775 self.retire_execution(reason, std::mem::take(ctx_extra).defuse());
776 return ret;
777 }
778
779 let timestamp = determination.timestamp_context.timestamp_or_default();
780 if let Some(id) = ctx_extra.contents() {
781 self.set_statement_execution_timestamp(id, timestamp)
782 }
783
784 let (peek_command, drop_dataflow, is_fast_path, peek_target, strategy, read_hold) =
794 match fast_path {
795 PeekPlan::FastPath(FastPathPlan::PeekExisting(
796 _coll_id,
797 idx_id,
798 literal_constraints,
799 map_filter_project,
800 )) => {
801 let read_hold = self
802 .controller
803 .compute
804 .acquire_read_hold(compute_instance, idx_id)
805 .map_err(
806 AdapterError::concurrent_dependency_drop_from_collection_update_error,
807 )?;
808 (
809 (literal_constraints, timestamp, map_filter_project),
810 None,
811 true,
812 PeekTarget::Index { id: idx_id },
813 StatementExecutionStrategy::FastPath,
814 read_hold,
815 )
816 }
817 PeekPlan::FastPath(FastPathPlan::PeekPersist(
818 coll_id,
819 literal_constraint,
820 map_filter_project,
821 )) => {
822 let peek_command = (
823 literal_constraint.map(|r| vec![r]),
824 timestamp,
825 map_filter_project,
826 );
827 let metadata = self
828 .controller
829 .storage
830 .collection_metadata(coll_id)
831 .expect("storage collection for fast-path peek")
832 .clone();
833 let read_hold = self
834 .controller
835 .storage_collections
836 .acquire_read_holds(vec![coll_id])
837 .map_err(AdapterError::concurrent_dependency_drop_from_collection_missing)?
838 .into_element();
839 (
840 peek_command,
841 None,
842 true,
843 PeekTarget::Persist {
844 id: coll_id,
845 metadata,
846 },
847 StatementExecutionStrategy::PersistFastPath,
848 read_hold,
849 )
850 }
851 PeekPlan::SlowPath(PeekDataflowPlan {
852 desc: dataflow,
853 id: index_id,
857 key: index_key,
858 permutation: index_permutation,
859 thinned_arity: index_thinned_arity,
860 }) => {
861 let exports: Vec<GlobalId> = dataflow.export_ids().collect();
867 soft_assert_eq_or_log!(
868 exports.as_slice(),
869 &[index_id],
870 "slow-path peek dataflow must export exactly [index_id]",
871 );
872 if exports.as_slice() != [index_id] {
873 return Err(AdapterError::internal(
874 "peek error",
875 format!(
876 "slow-path peek dataflow exports {exports:?}, expected [{index_id}]",
877 ),
878 ));
879 }
880
881 self.controller
883 .compute
884 .create_dataflow(compute_instance, dataflow, None)
885 .map_err(
886 AdapterError::concurrent_dependency_drop_from_dataflow_creation_error,
887 )?;
888
889 let acquire_result = self
892 .controller
893 .compute
894 .acquire_read_hold(compute_instance, index_id)
895 .map_err(
896 AdapterError::concurrent_dependency_drop_from_collection_update_error,
897 );
898 let read_hold = match acquire_result {
899 Ok(hold) => hold,
900 Err(e) => {
901 self.drop_compute_collections(vec![(compute_instance, index_id)]);
902 return Err(e);
903 }
904 };
905
906 let mut map_filter_project = mz_expr::MapFilterProject::new(source_arity);
908 map_filter_project.permute_fn(
909 |c| index_permutation[c],
910 index_key.len() + index_thinned_arity,
911 );
912 let map_filter_project = mfp_to_safe_plan(map_filter_project)?;
913
914 (
915 (None, timestamp, map_filter_project),
916 Some(index_id),
917 false,
918 PeekTarget::Index { id: index_id },
919 StatementExecutionStrategy::Standard,
920 read_hold,
921 )
922 }
923 PeekPlan::FastPath(_) => {
924 unreachable!()
925 }
926 };
927
928 let (rows_tx, rows_rx) = tokio::sync::oneshot::channel();
930
931 let mut uuid = Uuid::new_v4();
934 while self.pending_peeks.contains_key(&uuid) {
935 uuid = Uuid::new_v4();
936 }
937
938 let (literal_constraints, timestamp, map_filter_project) = peek_command;
939
940 let peek_result_column_names =
943 (0..intermediate_result_type.arity()).map(|i| format!("peek_{i}"));
944 let peek_result_desc =
945 RelationDesc::new(intermediate_result_type, peek_result_column_names);
946
947 let peek_result = self
948 .controller
949 .compute
950 .peek(
951 compute_instance,
952 peek_target,
953 literal_constraints,
954 uuid,
955 timestamp,
956 peek_result_desc,
957 finishing.clone(),
958 map_filter_project,
959 read_hold,
960 target_replica,
961 rows_tx,
962 )
963 .map_err(AdapterError::concurrent_dependency_drop_from_peek_error);
964 if let Err(e) = peek_result {
965 if let Some(index_id) = drop_dataflow {
967 self.drop_compute_collections(vec![(compute_instance, index_id)]);
968 }
969 return Err(e);
970 }
971
972 self.pending_peeks.insert(
976 uuid,
977 PendingPeek {
978 conn_id: conn_id.clone(),
979 cluster_id: compute_instance,
980 depends_on: source_ids,
981 ctx_extra: std::mem::take(ctx_extra),
982 is_fast_path,
983 },
984 );
985 self.client_pending_peeks
986 .entry(conn_id)
987 .or_default()
988 .insert(uuid, compute_instance);
989
990 let duration_histogram = self.metrics.row_set_finishing_seconds();
991
992 if let Some(index_id) = drop_dataflow {
999 self.drop_compute_collections(vec![(compute_instance, index_id)]);
1000 }
1001
1002 let persist_client = self.persist_client.clone();
1003 let peek_stash_read_batch_size_bytes =
1004 mz_compute_types::dyncfgs::PEEK_RESPONSE_STASH_READ_BATCH_SIZE_BYTES
1005 .get(self.catalog().system_config().dyncfgs());
1006 let peek_stash_read_memory_budget_bytes =
1007 mz_compute_types::dyncfgs::PEEK_RESPONSE_STASH_READ_MEMORY_BUDGET_BYTES
1008 .get(self.catalog().system_config().dyncfgs());
1009
1010 let peek_response_stream = Self::create_peek_response_stream(
1011 rows_rx,
1012 finishing,
1013 max_result_size,
1014 max_returned_query_size,
1015 duration_histogram,
1016 persist_client,
1017 peek_stash_read_batch_size_bytes,
1018 peek_stash_read_memory_budget_bytes,
1019 );
1020
1021 Ok(crate::ExecuteResponse::SendingRowsStreaming {
1022 rows: Box::pin(peek_response_stream),
1023 instance_id: compute_instance,
1024 strategy,
1025 })
1026 }
1027
1028 #[mz_ore::instrument(level = "debug")]
1032 pub(crate) fn create_peek_response_stream(
1033 rows_rx: tokio::sync::oneshot::Receiver<PeekResponse>,
1034 finishing: RowSetFinishing,
1035 max_result_size: u64,
1036 max_returned_query_size: Option<u64>,
1037 duration_histogram: prometheus::Histogram,
1038 mut persist_client: mz_persist_client::PersistClient,
1039 peek_stash_read_batch_size_bytes: usize,
1040 peek_stash_read_memory_budget_bytes: usize,
1041 ) -> impl futures::Stream<Item = PeekResponseUnary> {
1042 async_stream::stream!({
1043 let result = rows_rx.await;
1044
1045 let rows = match result {
1046 Ok(rows) => rows,
1047 Err(e) => {
1048 yield PeekResponseUnary::Error(AdapterError::Unstructured(anyhow::anyhow!(e)));
1049 return;
1050 }
1051 };
1052
1053 match rows {
1054 PeekResponse::Rows(rows) => {
1055 let rows = RowCollection::merge_sorted(&rows, &finishing.order_by);
1056 match finishing.finish(
1057 rows,
1058 max_result_size,
1059 max_returned_query_size,
1060 &duration_histogram,
1061 ) {
1062 Ok((rows, _size_bytes)) => yield PeekResponseUnary::Rows(Box::new(rows)),
1063 Err(e) => {
1064 yield PeekResponseUnary::Error(AdapterError::Unstructured(
1065 anyhow::Error::msg(e),
1066 ))
1067 }
1068 }
1069 }
1070 PeekResponse::Stashed(response) => {
1071 let response = *response;
1072
1073 let shard_id = response.shard_id;
1074
1075 let mut batches = Vec::new();
1076 for proto_batch in response.batches.into_iter() {
1077 let batch =
1078 persist_client.batch_from_transmittable_batch(&shard_id, proto_batch);
1079
1080 batches.push(batch);
1081 }
1082 tracing::trace!(?batches, "stashed peek response");
1083
1084 let as_of = Antichain::from_elem(mz_repr::Timestamp::default());
1085 let read_schemas: Schemas<SourceData, ()> = Schemas {
1086 id: None,
1087 key: Arc::new(response.relation_desc.clone()),
1088 val: Arc::new(UnitSchema),
1089 };
1090
1091 let mut row_cursor = persist_client
1092 .read_batches_consolidated::<_, _, _, i64>(
1093 response.shard_id,
1094 as_of,
1095 read_schemas,
1096 batches,
1097 |_stats| true,
1098 peek_stash_read_memory_budget_bytes,
1099 )
1100 .await
1101 .expect("invalid usage");
1102
1103 let (tx, mut rx) = tokio::sync::mpsc::channel(1);
1122 mz_ore::task::spawn(|| "read_peek_batches", async move {
1123 for rows in response.inline_rows {
1133 let result = tx.send(rows).await;
1134 if result.is_err() {
1135 tracing::debug!("receiver went away");
1136 }
1137 }
1138
1139 let mut current_batch = Vec::new();
1140 let mut current_batch_size: usize = 0;
1141
1142 'outer: while let Some(rows) = row_cursor.next().await {
1143 for ((source_data, _val), _ts, diff) in rows {
1144 let row = source_data
1145 .0
1146 .expect("we are not sending errors on this code path");
1147
1148 let diff = usize::try_from(diff)
1149 .expect("peek responses cannot have negative diffs");
1150
1151 if diff > 0 {
1152 let diff =
1153 NonZeroUsize::new(diff).expect("checked to be non-zero");
1154 current_batch_size =
1155 current_batch_size.saturating_add(row.byte_len());
1156 current_batch.push((row, diff));
1157 }
1158
1159 if current_batch_size > peek_stash_read_batch_size_bytes {
1160 let result = tx
1166 .send(RowCollection::new(
1167 current_batch.drain(..).collect_vec(),
1168 &[],
1169 ))
1170 .await;
1171 if result.is_err() {
1172 tracing::debug!("receiver went away");
1173 break 'outer;
1176 }
1177
1178 current_batch_size = 0;
1179 }
1180 }
1181 }
1182
1183 if current_batch.len() > 0 {
1184 let result = tx.send(RowCollection::new(current_batch, &[])).await;
1185 if result.is_err() {
1186 tracing::debug!("receiver went away");
1187 }
1188 }
1189
1190 let batches = row_cursor.into_lease();
1191 tracing::trace!(?response.shard_id, "cleaning up batches of peek result");
1192 for batch in batches {
1193 batch.delete().await;
1194 }
1195 });
1196
1197 assert!(
1198 finishing.is_streamable(response.relation_desc.arity()),
1199 "can only get stashed responses when the finishing is streamable"
1200 );
1201
1202 tracing::trace!("query result is streamable!");
1203
1204 assert!(finishing.is_streamable(response.relation_desc.arity()));
1205 let mut incremental_finishing = RowSetFinishingIncremental::new(
1206 finishing.offset,
1207 finishing.limit,
1208 finishing.project,
1209 max_returned_query_size,
1210 );
1211
1212 let mut got_zero_rows = true;
1213 while let Some(rows) = rx.recv().await {
1214 got_zero_rows = false;
1215
1216 let result_rows = incremental_finishing.finish_incremental(
1217 rows,
1218 max_result_size,
1219 &duration_histogram,
1220 );
1221
1222 match result_rows {
1223 Ok(result_rows) => yield PeekResponseUnary::Rows(Box::new(result_rows)),
1224 Err(e) => {
1225 yield PeekResponseUnary::Error(AdapterError::Unstructured(
1226 anyhow::Error::msg(e),
1227 ))
1228 }
1229 }
1230 }
1231
1232 if got_zero_rows {
1235 let row_iter = vec![].into_row_iter();
1236 yield PeekResponseUnary::Rows(Box::new(row_iter));
1237 }
1238 }
1239 PeekResponse::Canceled => {
1240 yield PeekResponseUnary::Canceled;
1241 }
1242 PeekResponse::Error(e) => {
1243 yield PeekResponseUnary::Error(e.into());
1244 }
1245 }
1246 })
1247 }
1248
1249 #[mz_ore::instrument(level = "debug")]
1251 pub(crate) fn cancel_pending_peeks(&mut self, conn_id: &ConnectionId) {
1252 if let Some(uuids) = self.client_pending_peeks.remove(conn_id) {
1253 self.metrics
1254 .canceled_peeks
1255 .inc_by(u64::cast_from(uuids.len()));
1256
1257 let mut inverse: BTreeMap<ComputeInstanceId, BTreeSet<Uuid>> = Default::default();
1258 for (uuid, compute_instance) in &uuids {
1259 inverse.entry(*compute_instance).or_default().insert(*uuid);
1260 }
1261 for (compute_instance, uuids) in inverse {
1262 for uuid in uuids {
1267 let _ = self.controller.compute.cancel_peek(
1268 compute_instance,
1269 uuid,
1270 PeekResponse::Canceled,
1271 );
1272 }
1273 }
1274
1275 let peeks = uuids
1276 .iter()
1277 .filter_map(|(uuid, _)| self.pending_peeks.remove(uuid))
1278 .collect::<Vec<_>>();
1279 for peek in peeks {
1280 self.retire_execution(
1281 StatementEndedExecutionReason::Canceled,
1282 peek.ctx_extra.defuse(),
1283 );
1284 }
1285 }
1286 }
1287
1288 pub(crate) fn handle_peek_notification(
1291 &mut self,
1292 uuid: Uuid,
1293 notification: PeekNotification,
1294 otel_ctx: OpenTelemetryContext,
1295 ) {
1296 if let Some(PendingPeek {
1299 conn_id: _,
1300 cluster_id: _,
1301 depends_on: _,
1302 ctx_extra,
1303 is_fast_path,
1304 }) = self.remove_pending_peek(&uuid)
1305 {
1306 let reason = match notification {
1307 PeekNotification::Success {
1308 rows: num_rows,
1309 result_size,
1310 } => {
1311 let strategy = if is_fast_path {
1312 StatementExecutionStrategy::FastPath
1313 } else {
1314 StatementExecutionStrategy::Standard
1315 };
1316 StatementEndedExecutionReason::Success {
1317 result_size: Some(result_size),
1318 rows_returned: Some(num_rows),
1319 execution_strategy: Some(strategy),
1320 }
1321 }
1322 PeekNotification::Error(error) => StatementEndedExecutionReason::Errored { error },
1323 PeekNotification::Canceled => StatementEndedExecutionReason::Canceled,
1324 };
1325 otel_ctx.attach_as_parent();
1326 self.retire_execution(reason, ctx_extra.defuse());
1327 }
1328 }
1331
1332 pub(crate) fn remove_pending_peek(&mut self, uuid: &Uuid) -> Option<PendingPeek> {
1334 let pending_peek = self.pending_peeks.remove(uuid);
1335 if let Some(pending_peek) = &pending_peek {
1336 let uuids = self
1337 .client_pending_peeks
1338 .get_mut(&pending_peek.conn_id)
1339 .expect("coord peek state is inconsistent");
1340 uuids.remove(uuid);
1341 if uuids.is_empty() {
1342 self.client_pending_peeks.remove(&pending_peek.conn_id);
1343 }
1344 }
1345 pending_peek
1346 }
1347
1348 pub(crate) async fn implement_slow_path_peek(
1354 &mut self,
1355 dataflow_plan: PeekDataflowPlan,
1356 determination: TimestampDetermination,
1357 finishing: RowSetFinishing,
1358 compute_instance: ComputeInstanceId,
1359 target_replica: Option<ReplicaId>,
1360 intermediate_result_type: SqlRelationType,
1361 source_ids: BTreeSet<GlobalId>,
1362 conn_id: ConnectionId,
1363 max_result_size: u64,
1364 max_query_result_size: Option<u64>,
1365 watch_set: Option<WatchSetCreation>,
1366 ) -> Result<ExecuteResponse, AdapterError> {
1367 let statement_logging_id = watch_set.as_ref().map(|ws| ws.logging_id);
1372 if let Some(ws) = watch_set {
1373 self.install_peek_watch_sets(conn_id.clone(), ws)
1374 .map_err(|e| {
1375 AdapterError::concurrent_dependency_drop_from_watch_set_install_error(e)
1376 })?;
1377 }
1378
1379 let source_arity = intermediate_result_type.arity();
1380
1381 let planned_peek = PlannedPeek {
1382 plan: PeekPlan::SlowPath(dataflow_plan),
1383 determination,
1384 conn_id,
1385 intermediate_result_type,
1386 source_arity,
1387 source_ids,
1388 };
1389
1390 let mut ctx_guard =
1394 ExecuteContextGuard::new(statement_logging_id, self.internal_cmd_tx.clone());
1395 let result = self
1396 .implement_peek_plan(
1397 &mut ctx_guard,
1398 planned_peek,
1399 finishing,
1400 compute_instance,
1401 target_replica,
1402 max_result_size,
1403 max_query_result_size,
1404 )
1405 .await;
1406 if result.is_err() {
1410 let _ = ctx_guard.defuse();
1411 }
1412 result
1413 }
1414
1415 pub(crate) async fn implement_copy_to(
1428 &mut self,
1429 df_desc: DataflowDescription<mz_compute_types::plan::LirRelationExpr>,
1430 compute_instance: ComputeInstanceId,
1431 target_replica: Option<ReplicaId>,
1432 source_ids: BTreeSet<GlobalId>,
1433 conn_id: ConnectionId,
1434 watch_set: Option<WatchSetCreation>,
1435 tx: oneshot::Sender<Result<ExecuteResponse, AdapterError>>,
1436 ) {
1437 let send_err = |tx: oneshot::Sender<Result<ExecuteResponse, AdapterError>>,
1439 e: AdapterError| {
1440 let _ = tx.send(Err(e));
1441 };
1442
1443 if let Some(ws) = watch_set {
1447 if let Err(e) = self.install_peek_watch_sets(conn_id.clone(), ws) {
1448 let err = AdapterError::concurrent_dependency_drop_from_watch_set_install_error(e);
1449 send_err(tx, err);
1450 return;
1451 }
1452 }
1453
1454 let sink_id = df_desc.sink_id();
1458
1459 let (sink_tx, sink_rx) = oneshot::channel();
1463 let active_copy_to = ActiveCopyTo {
1464 conn_id: conn_id.clone(),
1465 tx: sink_tx,
1466 cluster_id: compute_instance,
1467 depends_on: source_ids,
1468 };
1469
1470 drop(self.add_active_compute_sink(sink_id, ActiveComputeSink::CopyTo(active_copy_to)));
1472
1473 if let Err(e) = self
1476 .try_ship_dataflow(df_desc, compute_instance, target_replica)
1477 .await
1478 .map_err(AdapterError::concurrent_dependency_drop_from_dataflow_creation_error)
1479 {
1480 self.remove_active_compute_sink(sink_id).await;
1484 send_err(tx, e);
1485 return;
1486 }
1487
1488 let span = Span::current();
1493 task::spawn(
1494 || "copy to completion",
1495 async move {
1496 let res = sink_rx.await;
1497 let result = match res {
1498 Ok(res) => res,
1499 Err(_) => Err(AdapterError::Internal("copy to sender dropped".into())),
1500 };
1501
1502 let _ = tx.send(result);
1503 }
1504 .instrument(span),
1505 );
1506 }
1507
1508 pub(crate) fn send_immediate_rows<I>(rows: I) -> ExecuteResponse
1512 where
1513 I: IntoRowIterator,
1514 I::Iter: Send + Sync + 'static,
1515 {
1516 let rows = Box::new(rows.into_row_iter());
1517 ExecuteResponse::SendingRowsImmediate { rows }
1518 }
1519}
1520
1521#[cfg(test)]
1522mod tests {
1523 use mz_expr::func::IsNull;
1524 use mz_expr::{MapFilterProject, UnaryFunc};
1525 use mz_ore::str::Indent;
1526 use mz_repr::explain::text::text_string_at;
1527 use mz_repr::explain::{DummyHumanizer, ExplainConfig, PlanRenderingContext};
1528 use mz_repr::{Datum, SqlColumnType, SqlScalarType};
1529
1530 use super::*;
1531
1532 #[mz_ore::test]
1533 #[cfg_attr(miri, ignore)] fn test_fast_path_plan_as_text() {
1535 let typ = SqlRelationType::new(vec![SqlColumnType {
1536 scalar_type: SqlScalarType::String,
1537 nullable: false,
1538 }]);
1539 let constant_err = FastPathPlan::Constant(Err(EvalError::DivisionByZero), typ.clone());
1540 let no_lookup = FastPathPlan::PeekExisting(
1541 GlobalId::User(8),
1542 GlobalId::User(10),
1543 None,
1544 MapFilterProject::new(4)
1545 .map(Some(MirScalarExpr::column(0).or(MirScalarExpr::column(2))))
1546 .project([1, 4])
1547 .into_plan()
1548 .expect("invalid plan")
1549 .into_nontemporal()
1550 .expect("invalid nontemporal"),
1551 );
1552 let lookup = FastPathPlan::PeekExisting(
1553 GlobalId::User(9),
1554 GlobalId::User(11),
1555 Some(vec![Row::pack(Some(Datum::Int32(5)))]),
1556 MapFilterProject::new(3)
1557 .filter(Some(
1558 MirScalarExpr::column(0).call_unary(UnaryFunc::IsNull(IsNull)),
1559 ))
1560 .into_plan()
1561 .expect("invalid plan")
1562 .into_nontemporal()
1563 .expect("invalid nontemporal"),
1564 );
1565
1566 let humanizer = DummyHumanizer;
1567 let config = ExplainConfig {
1568 redacted: false,
1569 verbose_syntax: true,
1570 ..Default::default()
1571 };
1572 let ctx_gen = || {
1573 let indent = Indent::default();
1574 let annotations = BTreeMap::new();
1575 PlanRenderingContext::<FastPathPlan>::new(
1576 indent,
1577 &humanizer,
1578 annotations,
1579 &config,
1580 BTreeSet::default(),
1581 )
1582 };
1583
1584 let constant_err_exp = "Error \"division by zero\"\n";
1585 let no_lookup_exp = "Project (#1, #4)\n Map ((#0 OR #2))\n ReadIndex on=u8 [DELETED INDEX]=[*** full scan ***]\n";
1586 let lookup_exp =
1587 "Filter (#0) IS NULL\n ReadIndex on=u9 [DELETED INDEX]=[lookup value=(5)]\n";
1588
1589 assert_eq!(text_string_at(&constant_err, ctx_gen), constant_err_exp);
1590 assert_eq!(text_string_at(&no_lookup, ctx_gen), no_lookup_exp);
1591 assert_eq!(text_string_at(&lookup, ctx_gen), lookup_exp);
1592
1593 let mut constant_rows = vec![
1594 (Row::pack(Some(Datum::String("hello"))), Diff::ONE),
1595 (Row::pack(Some(Datum::String("world"))), 2.into()),
1596 (Row::pack(Some(Datum::String("star"))), 500.into()),
1597 ];
1598 let constant_exp1 =
1599 "Constant\n - (\"hello\")\n - ((\"world\") x 2)\n - ((\"star\") x 500)\n";
1600 assert_eq!(
1601 text_string_at(
1602 &FastPathPlan::Constant(Ok(constant_rows.clone()), typ.clone()),
1603 ctx_gen
1604 ),
1605 constant_exp1
1606 );
1607 constant_rows
1608 .extend((0..20).map(|i| (Row::pack(Some(Datum::String(&i.to_string()))), Diff::ONE)));
1609 let constant_exp2 = "Constant\n total_rows (diffs absed): 523\n first_rows:\n - (\"hello\")\
1610 \n - ((\"world\") x 2)\n - ((\"star\") x 500)\n - (\"0\")\n - (\"1\")\
1611 \n - (\"2\")\n - (\"3\")\n - (\"4\")\n - (\"5\")\n - (\"6\")\
1612 \n - (\"7\")\n - (\"8\")\n - (\"9\")\n - (\"10\")\n - (\"11\")\
1613 \n - (\"12\")\n - (\"13\")\n - (\"14\")\n - (\"15\")\n - (\"16\")\n";
1614 assert_eq!(
1615 text_string_at(&FastPathPlan::Constant(Ok(constant_rows), typ), ctx_gen),
1616 constant_exp2
1617 );
1618 }
1619}