1use 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#[derive(Debug)]
60pub(crate) struct PendingPeek {
61 pub(crate) conn_id: ConnectionId,
63 pub(crate) cluster_id: ClusterId,
65 pub(crate) depends_on: BTreeSet<GlobalId>,
67 pub(crate) ctx_extra: ExecuteContextGuard,
70 pub(crate) is_fast_path: bool,
72}
73
74#[derive(Debug)]
79pub enum PeekResponseUnary {
80 Rows(Box<dyn RowIterator + Send + Sync>),
81 Error(AdapterError),
82 Canceled,
83 DependencyDropped(DroppedDependency),
85}
86
87#[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 pub fn query_terminated_error(&self) -> String {
111 format!("query could not complete because {self} was dropped")
112 }
113
114 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 Constant(Result<Vec<(Row, Diff)>, EvalError>, SqlRelationType),
166 PeekExisting(GlobalId, GlobalId, Option<Vec<Row>>, mz_expr::SafeMfpPlan),
169 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 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 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#[derive(Clone, Debug)]
415pub enum PeekPlan {
416 FastPath(FastPathPlan),
417 SlowPath(PeekDataflowPlan),
419}
420
421fn 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
434fn 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
446pub 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 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 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 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 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 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 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 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 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 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 _ => {}
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 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 #[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 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 pub(crate) fn handle_peek_notification(
735 &mut self,
736 uuid: Uuid,
737 notification: PeekNotification,
738 otel_ctx: OpenTelemetryContext,
739 ) {
740 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 }
775
776 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 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 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 id: index_id,
833 key: index_key,
834 permutation: index_permutation,
835 thinned_arity: index_thinned_arity,
836 } = dataflow_plan;
837
838 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 self.controller
861 .compute
862 .create_dataflow(compute_instance, dataflow, None)
863 .map_err(AdapterError::concurrent_dependency_drop_from_dataflow_creation_error)?;
864
865 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 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 let (rows_tx, rows_rx) = tokio::sync::oneshot::channel();
889
890 let mut uuid = Uuid::new_v4();
893 while self.pending_peeks.contains_key(&uuid) {
894 uuid = Uuid::new_v4();
895 }
896
897 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 self.drop_compute_collections(vec![(compute_instance, index_id)]);
924 return Err(e);
925 }
926
927 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 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 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 let send_err = |tx: oneshot::Sender<Result<ExecuteResponse, AdapterError>>,
1008 e: AdapterError| {
1009 let _ = tx.send(Err(e));
1010 };
1011
1012 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 let sink_id = df_desc.sink_id();
1027
1028 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 drop(self.add_active_compute_sink(sink_id, ActiveComputeSink::CopyTo(active_copy_to)));
1041
1042 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 self.remove_active_compute_sink(sink_id).await;
1053 send_err(tx, e);
1054 return;
1055 }
1056
1057 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 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)] 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}