1use std::borrow::Cow;
16use std::collections::{BTreeMap, BTreeSet};
17
18use itertools::Itertools;
19use mz_arrow_util::builder::ArrowBuilder;
20use mz_expr::{ColumnOrder, RowSetFinishing};
21use mz_ore::num::NonNeg;
22use mz_ore::soft_panic_or_log;
23use mz_ore::str::separated;
24use mz_pgcopy::{CopyCsvFormatParams, CopyFormatParams, CopyTextFormatParams};
25use mz_repr::adt::numeric::NumericMaxScale;
26use mz_repr::bytes::ByteSize;
27use mz_repr::explain::{ExplainConfig, ExplainFormat};
28use mz_repr::optimize::OptimizerFeatureOverrides;
29use mz_repr::{CatalogItemId, Datum, RelationDesc, Row, SqlRelationType, SqlScalarType};
30use mz_sql_parser::ast::{
31 CteBlock, ExplainAnalyzeClusterStatement, ExplainAnalyzeComputationProperties,
32 ExplainAnalyzeComputationProperty, ExplainAnalyzeObjectStatement, ExplainAnalyzeProperty,
33 ExplainPlanOption, ExplainPlanOptionName, ExplainPushdownStatement, ExplainSinkSchemaFor,
34 ExplainSinkSchemaStatement, ExplainTimestampStatement, Expr, IfExistsBehavior, OrderByExpr,
35 SetExpr, SubscribeOutput, UnresolvedItemName,
36};
37use mz_sql_parser::ident;
38use mz_storage_types::sinks::{
39 KafkaSinkConnection, KafkaSinkFormat, KafkaSinkFormatType, MAX_S3_SINK_FILE_SIZE,
40 MIN_S3_SINK_FILE_SIZE, S3SinkFormat, StorageSinkConnection,
41};
42
43use crate::ast::display::{AstDisplay, escaped_string_literal};
44use crate::ast::{
45 AstInfo, CopyDirection, CopyOption, CopyOptionName, CopyRelation, CopyStatement, CopyTarget,
46 DeleteStatement, ExplainPlanStatement, ExplainStage, Explainee, Ident, InsertStatement, Query,
47 SelectStatement, SubscribeOption, SubscribeOptionName, SubscribeRelation, SubscribeStatement,
48 UpdateStatement,
49};
50use crate::catalog::CatalogItemType;
51use crate::names::{Aug, ResolvedItemName};
52use crate::normalize;
53use crate::plan::query::{
54 ExprContext, QueryLifetime, negative_offset_error, offset_into_value, plan_as_of_or_up_to,
55 plan_expr,
56};
57use crate::plan::scope::Scope;
58use crate::plan::statement::show::ShowSelect;
59use crate::plan::statement::{StatementContext, StatementDesc, ddl};
60use crate::plan::{
61 self, CopyFromFilter, CopyToPlan, CreateSinkPlan, ExplainPushdownPlan, ExplainSinkSchemaPlan,
62 ExplainTimestampPlan, HirRelationExpr, side_effecting_func, transform_ast,
63};
64use crate::plan::{
65 CopyFormat, CopyFromPlan, ExplainPlanPlan, InsertPlan, MutationKind, Params, Plan, PlanError,
66 QueryContext, ReadThenWritePlan, SelectPlan, SubscribeFrom, SubscribePlan, query,
67};
68use crate::plan::{CopyFromSource, with_options};
69use crate::session::vars::{self, DISALLOW_UNMATERIALIZABLE_FUNCTIONS_AS_OF};
70
71pub fn describe_insert(
78 scx: &StatementContext,
79 InsertStatement {
80 table_name,
81 columns,
82 source,
83 returning,
84 }: InsertStatement<Aug>,
85) -> Result<StatementDesc, PlanError> {
86 let (_, _, returning) = query::plan_insert_query(scx, table_name, columns, source, returning)?;
87 let desc = if returning.expr.is_empty() {
88 None
89 } else {
90 Some(returning.desc)
91 };
92 Ok(StatementDesc::new(desc))
93}
94
95pub fn plan_insert(
96 scx: &StatementContext,
97 InsertStatement {
98 table_name,
99 columns,
100 source,
101 returning,
102 }: InsertStatement<Aug>,
103 params: &Params,
104) -> Result<Plan, PlanError> {
105 let (id, mut expr, returning) =
106 query::plan_insert_query(scx, table_name, columns, source, returning)?;
107 expr.bind_parameters_and_simplify_offset(scx, QueryLifetime::OneShot, params)?;
108 let returning = returning
109 .expr
110 .into_iter()
111 .map(|mut expr| {
112 expr.bind_parameters_and_simplify_offset(scx, QueryLifetime::OneShot, params)?;
113 expr.lower_uncorrelated(scx.catalog.system_vars())
114 })
115 .collect::<Result<Vec<_>, _>>()?;
116
117 Ok(Plan::Insert(InsertPlan {
118 id,
119 values: expr,
120 returning,
121 }))
122}
123
124pub fn describe_delete(
125 scx: &StatementContext,
126 stmt: DeleteStatement<Aug>,
127) -> Result<StatementDesc, PlanError> {
128 query::plan_delete_query(scx, stmt)?;
129 Ok(StatementDesc::new(None))
130}
131
132pub fn plan_delete(
133 scx: &StatementContext,
134 stmt: DeleteStatement<Aug>,
135 params: &Params,
136) -> Result<Plan, PlanError> {
137 let rtw_plan = query::plan_delete_query(scx, stmt)?;
138 plan_read_then_write(scx, MutationKind::Delete, params, rtw_plan)
139}
140
141pub fn describe_update(
142 scx: &StatementContext,
143 stmt: UpdateStatement<Aug>,
144) -> Result<StatementDesc, PlanError> {
145 query::plan_update_query(scx, stmt)?;
146 Ok(StatementDesc::new(None))
147}
148
149pub fn plan_update(
150 scx: &StatementContext,
151 stmt: UpdateStatement<Aug>,
152 params: &Params,
153) -> Result<Plan, PlanError> {
154 let rtw_plan = query::plan_update_query(scx, stmt)?;
155 plan_read_then_write(scx, MutationKind::Update, params, rtw_plan)
156}
157
158pub fn plan_read_then_write(
159 scx: &StatementContext,
160 kind: MutationKind,
161 params: &Params,
162 query::ReadThenWritePlan {
163 id,
164 mut selection,
165 finishing,
166 assignments,
167 }: query::ReadThenWritePlan,
168) -> Result<Plan, PlanError> {
169 selection.bind_parameters_and_simplify_offset(scx, QueryLifetime::OneShot, params)?;
170 let mut assignments_outer = BTreeMap::new();
171 for (idx, mut set) in assignments {
172 set.bind_parameters_and_simplify_offset(scx, QueryLifetime::OneShot, params)?;
173 let set = set.lower_uncorrelated(scx.catalog.system_vars())?;
174 assignments_outer.insert(idx, set);
175 }
176
177 Ok(Plan::ReadThenWrite(ReadThenWritePlan {
178 id,
179 selection,
180 finishing,
181 assignments: assignments_outer,
182 kind,
183 returning: Vec::new(),
184 }))
185}
186
187pub fn describe_select(
188 scx: &StatementContext,
189 stmt: SelectStatement<Aug>,
190) -> Result<StatementDesc, PlanError> {
191 if let Some(desc) = side_effecting_func::describe_select_if_side_effecting(scx, &stmt)? {
192 return Ok(StatementDesc::new(Some(desc)));
193 }
194
195 let query::PlannedRootQuery { desc, .. } =
196 query::plan_root_query(scx, stmt.query, QueryLifetime::OneShot)?;
197 Ok(StatementDesc::new(Some(desc)))
198}
199
200pub fn plan_select(
201 scx: &StatementContext,
202 select: SelectStatement<Aug>,
203 params: &Params,
204 copy_to: Option<CopyFormat>,
205) -> Result<Plan, PlanError> {
206 if let Some(f) = side_effecting_func::plan_select_if_side_effecting(scx, &select, params)? {
207 return Ok(Plan::SideEffectingFunc(f));
208 }
209
210 let (plan, _desc) = plan_select_inner(scx, select, params, copy_to)?;
211 Ok(Plan::Select(plan))
212}
213
214fn plan_select_inner(
215 scx: &StatementContext,
216 select: SelectStatement<Aug>,
217 params: &Params,
218 copy_to: Option<CopyFormat>,
219) -> Result<(SelectPlan, RelationDesc), PlanError> {
220 let when = query::plan_as_of(scx, select.as_of.clone())?;
221 let lifetime = QueryLifetime::OneShot;
222 let query::PlannedRootQuery {
223 mut expr,
224 desc,
225 finishing,
226 scope: _,
227 } = query::plan_root_query(scx, select.query.clone(), lifetime)?;
228 expr.bind_parameters_and_simplify_offset(scx, lifetime, params)?;
229
230 let limit = match finishing.limit {
236 None => None,
237 Some(mut limit) => {
238 limit.bind_parameters_and_simplify_offset(scx, lifetime, params)?;
239 let Some(limit) = limit.as_literal() else {
241 sql_bail!(
242 "Top-level LIMIT must be a constant expression, got {}",
243 limit
244 )
245 };
246 match limit {
247 Datum::Null => None,
248 Datum::Int64(v) if v >= 0 => NonNeg::<i64>::try_from(v).ok(),
249 _ => {
250 soft_panic_or_log!("Valid literal limit must be asserted in `plan_select`");
251 sql_bail!("LIMIT must be a non-negative INT or NULL")
252 }
253 }
254 }
255 };
256 let offset = {
257 let mut offset = finishing.offset.clone();
258 offset.bind_parameters_and_simplify_offset(scx, lifetime, params)?;
259 let offset = offset_into_value(offset.take())?;
260 offset.try_into().map_err(|_| {
261 soft_panic_or_log!("unexpectedly negative OFFSET");
263 negative_offset_error(offset)
264 })?
265 };
266
267 if scx.is_feature_flag_enabled(&DISALLOW_UNMATERIALIZABLE_FUNCTIONS_AS_OF)
271 && select.as_of.is_some()
272 && expr.contains_unmaterializable_except_temporal()
273 {
274 bail_unsupported!("unmaterializable function (except `mz_now`) in an AS OF query");
275 }
276
277 let plan = SelectPlan {
278 source: expr,
279 when,
280 finishing: RowSetFinishing {
281 limit,
282 offset,
283 project: finishing.project,
284 order_by: finishing.order_by,
285 },
286 copy_to,
287 select: Some(Box::new(select)),
288 };
289
290 Ok((plan, desc))
291}
292
293pub fn describe_explain_plan(
294 scx: &StatementContext,
295 explain: ExplainPlanStatement<Aug>,
296) -> Result<StatementDesc, PlanError> {
297 let mut relation_desc = RelationDesc::builder();
298
299 match explain.stage() {
300 ExplainStage::RawPlan => {
301 let name = "Raw Plan";
302 relation_desc = relation_desc.with_column(name, SqlScalarType::String.nullable(false));
303 }
304 ExplainStage::DecorrelatedPlan => {
305 let name = "Decorrelated Plan";
306 relation_desc = relation_desc.with_column(name, SqlScalarType::String.nullable(false));
307 }
308 ExplainStage::LocalPlan => {
309 let name = "Locally Optimized Plan";
310 relation_desc = relation_desc.with_column(name, SqlScalarType::String.nullable(false));
311 }
312 ExplainStage::GlobalPlan => {
313 let name = "Optimized Plan";
314 relation_desc = relation_desc.with_column(name, SqlScalarType::String.nullable(false));
315 }
316 ExplainStage::PhysicalPlan => {
317 let name = "Physical Plan";
318 relation_desc = relation_desc.with_column(name, SqlScalarType::String.nullable(false));
319 }
320 ExplainStage::Trace => {
321 relation_desc = relation_desc
322 .with_column("Time", SqlScalarType::UInt64.nullable(false))
323 .with_column("Path", SqlScalarType::String.nullable(false))
324 .with_column("Plan", SqlScalarType::String.nullable(false));
325 }
326 ExplainStage::PlanInsights => {
327 let name = "Plan Insights";
328 relation_desc = relation_desc.with_column(name, SqlScalarType::String.nullable(false));
329 }
330 };
331 let relation_desc = relation_desc.finish();
332
333 Ok(
334 StatementDesc::new(Some(relation_desc)).with_params(match explain.explainee {
335 Explainee::Select(select, _) => describe_select(scx, *select)?.param_types,
336 _ => vec![],
337 }),
338 )
339}
340
341pub fn describe_explain_pushdown(
342 scx: &StatementContext,
343 statement: ExplainPushdownStatement<Aug>,
344) -> Result<StatementDesc, PlanError> {
345 let relation_desc = RelationDesc::builder()
346 .with_column("Source", SqlScalarType::String.nullable(false))
347 .with_column("Total Bytes", SqlScalarType::UInt64.nullable(false))
348 .with_column("Selected Bytes", SqlScalarType::UInt64.nullable(false))
349 .with_column("Total Parts", SqlScalarType::UInt64.nullable(false))
350 .with_column("Selected Parts", SqlScalarType::UInt64.nullable(false))
351 .finish();
352
353 Ok(
354 StatementDesc::new(Some(relation_desc)).with_params(match statement.explainee {
355 Explainee::Select(select, _) => describe_select(scx, *select)?.param_types,
356 _ => vec![],
357 }),
358 )
359}
360
361pub fn describe_explain_analyze_object(
362 _scx: &StatementContext,
363 statement: ExplainAnalyzeObjectStatement<Aug>,
364) -> Result<StatementDesc, PlanError> {
365 if statement.as_sql {
366 let relation_desc = RelationDesc::builder()
367 .with_column("SQL", SqlScalarType::String.nullable(false))
368 .finish();
369 return Ok(StatementDesc::new(Some(relation_desc)));
370 }
371
372 match statement.properties {
373 ExplainAnalyzeProperty::Computation(ExplainAnalyzeComputationProperties {
374 properties,
375 skew,
376 }) => {
377 let mut relation_desc = RelationDesc::builder()
378 .with_column("operator", SqlScalarType::String.nullable(false));
379
380 if skew {
381 relation_desc =
382 relation_desc.with_column("worker_id", SqlScalarType::UInt64.nullable(true));
383 }
384
385 let mut seen_properties = BTreeSet::new();
386 for property in properties {
387 if !seen_properties.insert(property) {
389 continue;
390 }
391
392 match property {
393 ExplainAnalyzeComputationProperty::Memory if skew => {
394 let numeric = SqlScalarType::Numeric { max_scale: None }.nullable(true);
395 relation_desc = relation_desc
396 .with_column("memory_ratio", numeric.clone())
397 .with_column("worker_memory", SqlScalarType::String.nullable(true))
398 .with_column("avg_memory", SqlScalarType::String.nullable(true))
399 .with_column("total_memory", SqlScalarType::String.nullable(true))
400 .with_column("records_ratio", numeric.clone())
401 .with_column("worker_records", numeric.clone())
402 .with_column("avg_records", numeric.clone())
403 .with_column("total_records", numeric);
404 }
405 ExplainAnalyzeComputationProperty::Memory => {
406 relation_desc = relation_desc
407 .with_column("total_memory", SqlScalarType::String.nullable(true))
408 .with_column(
409 "total_records",
410 SqlScalarType::Numeric { max_scale: None }.nullable(true),
411 );
412 }
413 ExplainAnalyzeComputationProperty::Cpu => {
414 if skew {
415 relation_desc = relation_desc
416 .with_column(
417 "cpu_ratio",
418 SqlScalarType::Numeric { max_scale: None }.nullable(true),
419 )
420 .with_column(
421 "worker_elapsed",
422 SqlScalarType::Interval.nullable(true),
423 )
424 .with_column("avg_elapsed", SqlScalarType::Interval.nullable(true));
425 }
426 relation_desc = relation_desc
427 .with_column("total_elapsed", SqlScalarType::Interval.nullable(true));
428 }
429 }
430 }
431
432 let relation_desc = relation_desc.finish();
433 Ok(StatementDesc::new(Some(relation_desc)))
434 }
435 ExplainAnalyzeProperty::Hints => {
436 let relation_desc = RelationDesc::builder()
437 .with_column("operator", SqlScalarType::String.nullable(true))
438 .with_column("levels", SqlScalarType::Int64.nullable(true))
439 .with_column("to_cut", SqlScalarType::Int64.nullable(true))
440 .with_column("hint", SqlScalarType::Float64.nullable(true))
441 .with_column("savings", SqlScalarType::String.nullable(true))
442 .finish();
443 Ok(StatementDesc::new(Some(relation_desc)))
444 }
445 }
446}
447
448pub fn describe_explain_analyze_cluster(
449 _scx: &StatementContext,
450 statement: ExplainAnalyzeClusterStatement,
451) -> Result<StatementDesc, PlanError> {
452 if statement.as_sql {
453 let relation_desc = RelationDesc::builder()
454 .with_column("SQL", SqlScalarType::String.nullable(false))
455 .finish();
456 return Ok(StatementDesc::new(Some(relation_desc)));
457 }
458
459 let ExplainAnalyzeComputationProperties { properties, skew } = statement.properties;
460
461 let mut relation_desc = RelationDesc::builder()
462 .with_column("object", SqlScalarType::String.nullable(false))
463 .with_column("global_id", SqlScalarType::String.nullable(false));
464
465 if skew {
466 relation_desc =
467 relation_desc.with_column("worker_id", SqlScalarType::UInt64.nullable(true));
468 }
469
470 let mut seen_properties = BTreeSet::new();
471 for property in properties {
472 if !seen_properties.insert(property) {
474 continue;
475 }
476
477 match property {
478 ExplainAnalyzeComputationProperty::Memory if skew => {
479 let numeric = SqlScalarType::Numeric { max_scale: None }.nullable(true);
480 relation_desc = relation_desc
481 .with_column("max_operator_memory_ratio", numeric.clone())
482 .with_column("worker_memory", SqlScalarType::String.nullable(true))
483 .with_column("avg_memory", SqlScalarType::String.nullable(true))
484 .with_column("total_memory", SqlScalarType::String.nullable(true))
485 .with_column("max_operator_records_ratio", numeric.clone())
486 .with_column("worker_records", numeric.clone())
487 .with_column("avg_records", numeric.clone())
488 .with_column("total_records", numeric);
489 }
490 ExplainAnalyzeComputationProperty::Memory => {
491 relation_desc = relation_desc
492 .with_column("total_memory", SqlScalarType::String.nullable(true))
493 .with_column(
494 "total_records",
495 SqlScalarType::Numeric { max_scale: None }.nullable(true),
496 );
497 }
498 ExplainAnalyzeComputationProperty::Cpu if skew => {
499 relation_desc = relation_desc
500 .with_column(
501 "max_operator_cpu_ratio",
502 SqlScalarType::Numeric { max_scale: None }.nullable(true),
503 )
504 .with_column("worker_elapsed", SqlScalarType::Interval.nullable(true))
505 .with_column("avg_elapsed", SqlScalarType::Interval.nullable(true))
506 .with_column("total_elapsed", SqlScalarType::Interval.nullable(true));
507 }
508 ExplainAnalyzeComputationProperty::Cpu => {
509 relation_desc = relation_desc
510 .with_column("total_elapsed", SqlScalarType::Interval.nullable(true));
511 }
512 }
513 }
514
515 Ok(StatementDesc::new(Some(relation_desc.finish())))
516}
517
518pub fn describe_explain_timestamp(
519 scx: &StatementContext,
520 ExplainTimestampStatement { select, .. }: ExplainTimestampStatement<Aug>,
521) -> Result<StatementDesc, PlanError> {
522 let relation_desc = RelationDesc::builder()
523 .with_column("Timestamp", SqlScalarType::String.nullable(false))
524 .finish();
525
526 Ok(StatementDesc::new(Some(relation_desc))
527 .with_params(describe_select(scx, select)?.param_types))
528}
529
530pub fn describe_explain_schema(
531 _: &StatementContext,
532 ExplainSinkSchemaStatement { .. }: ExplainSinkSchemaStatement<Aug>,
533) -> Result<StatementDesc, PlanError> {
534 let relation_desc = RelationDesc::builder()
535 .with_column("Schema", SqlScalarType::String.nullable(false))
536 .finish();
537 Ok(StatementDesc::new(Some(relation_desc)))
538}
539
540generate_extracted_config!(
549 ExplainPlanOption,
550 (Arity, Option<bool>, Default(None)),
551 (Cardinality, bool, Default(false)),
552 (ColumnNames, bool, Default(false)),
553 (FilterPushdown, Option<bool>, Default(None)),
554 (HumanizedExpressions, Option<bool>, Default(None)),
555 (JoinImplementations, bool, Default(false)),
556 (Keys, bool, Default(false)),
557 (LinearChains, bool, Default(false)),
558 (NoFastPath, bool, Default(false)),
559 (NonNegative, bool, Default(false)),
560 (NoNotices, bool, Default(false)),
561 (NodeIdentifiers, bool, Default(false)),
562 (Raw, bool, Default(false)),
563 (RawPlans, bool, Default(false)),
564 (RawSyntax, bool, Default(false)),
565 (Redacted, bool, Default(false)),
566 (SubtreeSize, bool, Default(false)),
567 (Timing, bool, Default(false)),
568 (Types, bool, Default(false)),
569 (Equivalences, bool, Default(false)),
570 (ReoptimizeImportedViews, Option<bool>, Default(None)),
571 (EnableNewOuterJoinLowering, Option<bool>, Default(None)),
572 (EnableEagerDeltaJoins, Option<bool>, Default(None)),
573 (EnableVariadicLeftJoinLowering, Option<bool>, Default(None)),
574 (EnableLetrecFixpointAnalysis, Option<bool>, Default(None)),
575 (EnableJoinPrioritizeArranged, Option<bool>, Default(None)),
576 (
577 EnableProjectionPushdownAfterRelationCse,
578 Option<bool>,
579 Default(None)
580 ),
581 (
582 EnableFixedCorrelatedCteLowering,
583 Option<bool>,
584 Default(None)
585 )
586);
587
588impl TryFrom<ExplainPlanOptionExtracted> for ExplainConfig {
589 type Error = PlanError;
590
591 fn try_from(mut v: ExplainPlanOptionExtracted) -> Result<Self, Self::Error> {
592 if v.raw {
595 v.raw_plans = true;
596 v.raw_syntax = true;
597 }
598
599 let enable_on_prod = !mz_ore::assert::soft_assertions_enabled();
602
603 Ok(ExplainConfig {
604 arity: v.arity.unwrap_or(enable_on_prod),
605 cardinality: v.cardinality,
606 column_names: v.column_names,
607 filter_pushdown: v.filter_pushdown.unwrap_or(enable_on_prod),
608 humanized_exprs: !v.raw_plans && (v.humanized_expressions.unwrap_or(enable_on_prod)),
609 join_impls: v.join_implementations,
610 keys: v.keys,
611 linear_chains: !v.raw_plans && v.linear_chains,
612 no_fast_path: v.no_fast_path,
613 no_notices: v.no_notices,
614 node_ids: v.node_identifiers,
615 non_negative: v.non_negative,
616 raw_plans: v.raw_plans,
617 raw_syntax: v.raw_syntax,
618 verbose_syntax: false,
619 redacted: v.redacted,
620 subtree_size: v.subtree_size,
621 equivalences: v.equivalences,
622 timing: v.timing,
623 types: v.types,
624 features: OptimizerFeatureOverrides {
626 enable_eager_delta_joins: v.enable_eager_delta_joins,
627 enable_new_outer_join_lowering: v.enable_new_outer_join_lowering,
628 enable_variadic_left_join_lowering: v.enable_variadic_left_join_lowering,
629 enable_letrec_fixpoint_analysis: v.enable_letrec_fixpoint_analysis,
630 enable_reduce_mfp_fusion: Default::default(),
631 enable_cardinality_estimates: Default::default(),
632 persist_fast_path_limit: Default::default(),
633 reoptimize_imported_views: v.reoptimize_imported_views,
634 enable_join_prioritize_arranged: v.enable_join_prioritize_arranged,
635 enable_projection_pushdown_after_relation_cse: v
636 .enable_projection_pushdown_after_relation_cse,
637 enable_less_reduce_in_eqprop: Default::default(),
638 enable_dequadratic_eqprop_map: Default::default(),
639 enable_eq_classes_withholding_errors: Default::default(),
640 enable_fast_path_plan_insights: Default::default(),
641 enable_cast_elimination: Default::default(),
642 enable_case_literal_transform: Default::default(),
643 enable_simplify_quantified_comparisons: Default::default(),
644 enable_coalesce_case_transform: Default::default(),
645 enable_will_distinct_propagation: Default::default(),
646 enable_fixed_correlated_cte_lowering: v.enable_fixed_correlated_cte_lowering,
647 },
648 })
649 }
650}
651
652fn plan_explainee(
653 scx: &StatementContext,
654 explainee: Explainee<Aug>,
655 params: &Params,
656) -> Result<plan::Explainee, PlanError> {
657 use crate::plan::ExplaineeStatement;
658
659 let is_replan = matches!(
660 explainee,
661 Explainee::ReplanView(_) | Explainee::ReplanMaterializedView(_) | Explainee::ReplanIndex(_)
662 );
663
664 let explainee = match explainee {
665 Explainee::View(name) | Explainee::ReplanView(name) => {
666 let item = scx.get_item_by_resolved_name(&name)?;
667 let item_type = item.item_type();
668 if item_type != CatalogItemType::View {
669 sql_bail!("Expected {name} to be a view, not a {item_type}");
670 }
671 match is_replan {
672 true => crate::plan::Explainee::ReplanView(item.id()),
673 false => crate::plan::Explainee::View(item.id()),
674 }
675 }
676 Explainee::MaterializedView(name) | Explainee::ReplanMaterializedView(name) => {
677 let item = scx.get_item_by_resolved_name(&name)?;
678 let item_type = item.item_type();
679 if item_type != CatalogItemType::MaterializedView {
680 sql_bail!("Expected {name} to be a materialized view, not a {item_type}");
681 }
682 match is_replan {
683 true => crate::plan::Explainee::ReplanMaterializedView(item.id()),
684 false => crate::plan::Explainee::MaterializedView(item.id()),
685 }
686 }
687 Explainee::Index(name) | Explainee::ReplanIndex(name) => {
688 let item = scx.get_item_by_resolved_name(&name)?;
689 let item_type = item.item_type();
690 if item_type != CatalogItemType::Index {
691 sql_bail!("Expected {name} to be an index, not a {item_type}");
692 }
693 match is_replan {
694 true => crate::plan::Explainee::ReplanIndex(item.id()),
695 false => crate::plan::Explainee::Index(item.id()),
696 }
697 }
698 Explainee::Select(select, broken) => {
699 let (plan, desc) = plan_select_inner(scx, *select, params, None)?;
700 crate::plan::Explainee::Statement(ExplaineeStatement::Select { broken, plan, desc })
701 }
702 Explainee::CreateView(mut stmt, broken) => {
703 if stmt.if_exists != IfExistsBehavior::Skip {
704 stmt.if_exists = IfExistsBehavior::Skip;
709 } else {
710 sql_bail!(
711 "Cannot EXPLAIN a CREATE VIEW that explictly sets IF NOT EXISTS \
712 (the behavior is implied within the scope of an enclosing EXPLAIN)"
713 );
714 }
715
716 let Plan::CreateView(plan) = ddl::plan_create_view(scx, *stmt)? else {
717 sql_bail!("expected CreateViewPlan plan");
718 };
719
720 crate::plan::Explainee::Statement(ExplaineeStatement::CreateView { broken, plan })
721 }
722 Explainee::CreateMaterializedView(mut stmt, broken) => {
723 if stmt.if_exists != IfExistsBehavior::Skip {
724 stmt.if_exists = IfExistsBehavior::Skip;
729 } else {
730 sql_bail!(
731 "Cannot EXPLAIN a CREATE MATERIALIZED VIEW that explictly sets IF NOT EXISTS \
732 (the behavior is implied within the scope of an enclosing EXPLAIN)"
733 );
734 }
735
736 let Plan::CreateMaterializedView(plan) =
737 ddl::plan_create_materialized_view(scx, *stmt)?
738 else {
739 sql_bail!("expected CreateMaterializedViewPlan plan");
740 };
741
742 crate::plan::Explainee::Statement(ExplaineeStatement::CreateMaterializedView {
743 broken,
744 plan,
745 })
746 }
747 Explainee::CreateIndex(mut stmt, broken) => {
748 if !stmt.if_not_exists {
749 stmt.if_not_exists = true;
752 } else {
753 sql_bail!(
754 "Cannot EXPLAIN a CREATE INDEX that explictly sets IF NOT EXISTS \
755 (the behavior is implied within the scope of an enclosing EXPLAIN)"
756 );
757 }
758
759 let Plan::CreateIndex(plan) = ddl::plan_create_index(scx, *stmt)? else {
760 sql_bail!("expected CreateIndexPlan plan");
761 };
762
763 crate::plan::Explainee::Statement(ExplaineeStatement::CreateIndex { broken, plan })
764 }
765 Explainee::Subscribe(stmt, broken) => {
766 let Plan::Subscribe(plan) = plan_subscribe(scx, *stmt, params, None)? else {
767 sql_bail!("expected SubscribePlan");
768 };
769 crate::plan::Explainee::Statement(ExplaineeStatement::Subscribe { broken, plan })
770 }
771 };
772
773 Ok(explainee)
774}
775
776pub fn plan_explain_plan(
777 scx: &StatementContext,
778 explain: ExplainPlanStatement<Aug>,
779 params: &Params,
780) -> Result<Plan, PlanError> {
781 let (format, verbose_syntax) = match explain.format() {
782 mz_sql_parser::ast::ExplainFormat::Text => (ExplainFormat::Text, false),
783 mz_sql_parser::ast::ExplainFormat::VerboseText => (ExplainFormat::Text, true),
784 mz_sql_parser::ast::ExplainFormat::Json => (ExplainFormat::Json, false),
785 mz_sql_parser::ast::ExplainFormat::Dot => (ExplainFormat::Dot, false),
786 };
787 let stage = explain.stage();
788
789 let mut config = {
791 let mut with_options = ExplainPlanOptionExtracted::try_from(explain.with_options)?;
792
793 if !scx.catalog.system_vars().persist_stats_filter_enabled() {
794 with_options.filter_pushdown = Some(false);
796 }
797
798 ExplainConfig::try_from(with_options)?
799 };
800 config.verbose_syntax = verbose_syntax;
801
802 let explainee = plan_explainee(scx, explain.explainee, params)?;
803
804 Ok(Plan::ExplainPlan(ExplainPlanPlan {
805 stage,
806 format,
807 config,
808 explainee,
809 }))
810}
811
812pub fn plan_explain_schema(
813 scx: &StatementContext,
814 explain_schema: ExplainSinkSchemaStatement<Aug>,
815) -> Result<Plan, PlanError> {
816 let ExplainSinkSchemaStatement {
817 schema_for,
818 format: _,
820 mut statement,
821 } = explain_schema;
822
823 statement.name = Some(UnresolvedItemName::qualified(&[
827 ident!("mz_catalog"),
828 ident!("mz_explain_schema"),
829 ]));
830
831 crate::pure::purify_create_sink_avro_doc_on_options(
832 scx.catalog,
833 *statement.from.item_id(),
834 &mut statement.format,
835 )?;
836
837 match ddl::plan_create_sink(scx, statement)? {
838 Plan::CreateSink(CreateSinkPlan { sink, .. }) => match sink.connection {
839 StorageSinkConnection::Kafka(KafkaSinkConnection {
840 format:
841 KafkaSinkFormat {
842 key_format,
843 value_format:
844 KafkaSinkFormatType::Avro {
845 schema: value_schema,
846 ..
847 },
848 ..
849 },
850 ..
851 }) => {
852 let schema = match schema_for {
853 ExplainSinkSchemaFor::Key => key_format
854 .and_then(|f| match f {
855 KafkaSinkFormatType::Avro { schema, .. } => Some(schema),
856 _ => None,
857 })
858 .ok_or_else(|| sql_err!("CREATE SINK does not have a key"))?,
859 ExplainSinkSchemaFor::Value => value_schema,
860 };
861
862 Ok(Plan::ExplainSinkSchema(ExplainSinkSchemaPlan {
863 sink_from: sink.from,
864 json_schema: schema,
865 }))
866 }
867 _ => bail_unsupported!(
868 "EXPLAIN SCHEMA is only available for Kafka sinks with Avro schemas"
869 ),
870 },
871 _ => bail_internal!("plan_sink did not produce a CreateSink plan"),
872 }
873}
874
875pub fn plan_explain_pushdown(
876 scx: &StatementContext,
877 statement: ExplainPushdownStatement<Aug>,
878 params: &Params,
879) -> Result<Plan, PlanError> {
880 scx.require_feature_flag(&vars::ENABLE_EXPLAIN_PUSHDOWN)?;
881 let explainee = plan_explainee(scx, statement.explainee, params)?;
882 Ok(Plan::ExplainPushdown(ExplainPushdownPlan { explainee }))
883}
884
885pub fn plan_explain_analyze_object(
886 scx: &StatementContext,
887 statement: ExplainAnalyzeObjectStatement<Aug>,
888 params: &Params,
889) -> Result<Plan, PlanError> {
890 let explainee_name = statement
891 .explainee
892 .name()
893 .ok_or_else(|| sql_err!("EXPLAIN ANALYZE on anonymous dataflows",))?
894 .full_name_str();
895 let explainee = plan_explainee(scx, statement.explainee, params)?;
896
897 let check_ownership = |item_id: &CatalogItemId, item_type: &str| -> Result<(), PlanError> {
898 if scx.catalog.restrict_to_user_objects() {
899 let item = scx.catalog.get_item(item_id);
900 if item.owner_id() != *scx.catalog.active_role_id() {
901 let full_name = scx.catalog.resolve_full_name(item.name());
902 return Err(sql_err!("must be owner of {item_type} {full_name}"));
903 }
904 }
905 Ok(())
906 };
907 match &explainee {
908 plan::Explainee::Index(item_id) => check_ownership(item_id, "INDEX")?,
909 plan::Explainee::MaterializedView(item_id) => {
910 check_ownership(item_id, "MATERIALIZED VIEW")?
911 }
912 _ => return Err(sql_err!("EXPLAIN ANALYZE queries for this explainee type",)),
913 };
914
915 let mut ctes = Vec::with_capacity(4); let mut columns = vec!["REPEAT(' ', nesting * 2) || operator AS operator"];
930 let mut from = vec!["mz_introspection.mz_lir_mapping mlm"];
931 let mut predicates = vec![format!(
932 "mo.name = {}",
933 escaped_string_literal(&explainee_name)
934 )];
935 let mut order_by = vec!["mlm.lir_id DESC"];
936
937 match statement.properties {
938 ExplainAnalyzeProperty::Computation(ExplainAnalyzeComputationProperties {
939 properties,
940 skew,
941 }) => {
942 let mut worker_id = None;
943 let mut seen_properties = BTreeSet::new();
944 for property in properties {
945 if !seen_properties.insert(property) {
947 continue;
948 }
949
950 match property {
951 ExplainAnalyzeComputationProperty::Memory => {
952 ctes.push((
953 "summary_memory",
954 r#"
955 SELECT mlm.global_id AS global_id,
956 mlm.lir_id AS lir_id,
957 SUM(mas.size) AS total_memory,
958 SUM(mas.records) AS total_records,
959 CASE WHEN COUNT(DISTINCT mas.worker_id) <> 0 THEN SUM(mas.size) / COUNT(DISTINCT mas.worker_id) ELSE NULL END AS avg_memory,
960 CASE WHEN COUNT(DISTINCT mas.worker_id) <> 0 THEN SUM(mas.records) / COUNT(DISTINCT mas.worker_id) ELSE NULL END AS avg_records
961 FROM mz_introspection.mz_lir_mapping mlm
962 CROSS JOIN generate_series((mlm.operator_id_start) :: int8, (mlm.operator_id_end - 1) :: int8) AS valid_id
963 JOIN mz_introspection.mz_arrangement_sizes_per_worker mas
964 ON (mas.operator_id = valid_id)
965GROUP BY mlm.global_id, mlm.lir_id"#,
966 ));
967 from.push("LEFT JOIN summary_memory sm USING (global_id, lir_id)");
968
969 if skew {
970 ctes.push((
971 "per_worker_memory",
972 r#"
973 SELECT mlm.global_id AS global_id,
974 mlm.lir_id AS lir_id,
975 mas.worker_id AS worker_id,
976 SUM(mas.size) AS worker_memory,
977 SUM(mas.records) AS worker_records
978 FROM mz_introspection.mz_lir_mapping mlm
979 CROSS JOIN generate_series((mlm.operator_id_start) :: int8, (mlm.operator_id_end - 1) :: int8) AS valid_id
980 JOIN mz_introspection.mz_arrangement_sizes_per_worker mas
981 ON (mas.operator_id = valid_id)
982GROUP BY mlm.global_id, mlm.lir_id, mas.worker_id"#,
983 ));
984 from.push("LEFT JOIN per_worker_memory pwm USING (global_id, lir_id)");
985
986 if let Some(worker_id) = worker_id {
987 predicates.push(format!(
988 "(pwm.worker_id = {worker_id} OR pwm.worker_id IS NULL OR {worker_id} IS NULL)"
989 ));
990 } else {
991 worker_id = Some("pwm.worker_id");
992 columns.push("pwm.worker_id AS worker_id");
993 order_by.push("worker_id");
994 }
995
996 columns.extend([
997 "CASE WHEN pwm.worker_id IS NOT NULL AND sm.avg_memory <> 0 THEN ROUND(pwm.worker_memory / sm.avg_memory, 2) ELSE NULL END AS memory_ratio",
998 "pg_size_pretty(pwm.worker_memory) AS worker_memory",
999 "pg_size_pretty(sm.avg_memory) AS avg_memory",
1000 "pg_size_pretty(sm.total_memory) AS total_memory",
1001 "CASE WHEN pwm.worker_id IS NOT NULL AND sm.avg_records <> 0 THEN ROUND(pwm.worker_records / sm.avg_records, 2) ELSE NULL END AS records_ratio",
1002 "pwm.worker_records AS worker_records",
1003 "sm.avg_records AS avg_records",
1004 "sm.total_records AS total_records",
1005 ]);
1006 } else {
1007 columns.extend([
1008 "pg_size_pretty(sm.total_memory) AS total_memory",
1009 "sm.total_records AS total_records",
1010 ]);
1011 }
1012 }
1013 ExplainAnalyzeComputationProperty::Cpu => {
1014 ctes.push((
1015 "summary_cpu",
1016 r#"
1017 SELECT mlm.global_id AS global_id,
1018 mlm.lir_id AS lir_id,
1019 SUM(mse.elapsed_ns) AS total_ns,
1020 CASE WHEN COUNT(DISTINCT mse.worker_id) <> 0 THEN SUM(mse.elapsed_ns) / COUNT(DISTINCT mse.worker_id) ELSE NULL END AS avg_ns
1021 FROM mz_introspection.mz_lir_mapping mlm
1022 CROSS JOIN generate_series((mlm.operator_id_start) :: int8, (mlm.operator_id_end - 1) :: int8) AS valid_id
1023 JOIN mz_introspection.mz_scheduling_elapsed_per_worker mse
1024 ON (mse.id = valid_id)
1025GROUP BY mlm.global_id, mlm.lir_id"#,
1026 ));
1027 from.push("LEFT JOIN summary_cpu sc USING (global_id, lir_id)");
1028
1029 if skew {
1030 ctes.push((
1031 "per_worker_cpu",
1032 r#"
1033 SELECT mlm.global_id AS global_id,
1034 mlm.lir_id AS lir_id,
1035 mse.worker_id AS worker_id,
1036 SUM(mse.elapsed_ns) AS worker_ns
1037 FROM mz_introspection.mz_lir_mapping mlm
1038 CROSS JOIN generate_series((mlm.operator_id_start) :: int8, (mlm.operator_id_end - 1) :: int8) AS valid_id
1039 JOIN mz_introspection.mz_scheduling_elapsed_per_worker mse
1040 ON (mse.id = valid_id)
1041GROUP BY mlm.global_id, mlm.lir_id, mse.worker_id"#,
1042 ));
1043 from.push("LEFT JOIN per_worker_cpu pwc USING (global_id, lir_id)");
1044
1045 if let Some(worker_id) = worker_id {
1046 predicates.push(format!(
1047 "(pwc.worker_id = {worker_id} OR pwc.worker_id IS NULL OR {worker_id} IS NULL)"
1048 ));
1049 } else {
1050 worker_id = Some("pwc.worker_id");
1051 columns.push("pwc.worker_id AS worker_id");
1052 order_by.push("worker_id");
1053 }
1054
1055 columns.extend([
1056 "CASE WHEN pwc.worker_id IS NOT NULL AND sc.avg_ns <> 0 THEN ROUND(pwc.worker_ns / sc.avg_ns, 2) ELSE NULL END AS cpu_ratio",
1057 "pwc.worker_ns / 1000 * '1 microsecond'::INTERVAL AS worker_elapsed",
1058 "sc.avg_ns / 1000 * '1 microsecond'::INTERVAL AS avg_elapsed",
1059 ]);
1060 }
1061 columns.push(
1062 "sc.total_ns / 1000 * '1 microsecond'::INTERVAL AS total_elapsed",
1063 );
1064 }
1065 }
1066 }
1067 }
1068 ExplainAnalyzeProperty::Hints => {
1069 columns.extend([
1070 "megsa.levels AS levels",
1071 "megsa.to_cut AS to_cut",
1072 "megsa.hint AS hint",
1073 "pg_size_pretty(megsa.savings) AS savings",
1074 ]);
1075 from.extend(["JOIN mz_introspection.mz_dataflow_global_ids mdgi ON (mlm.global_id = mdgi.global_id)",
1076 "LEFT JOIN (generate_series((mlm.operator_id_start) :: int8, (mlm.operator_id_end - 1) :: int8) AS valid_id JOIN \
1077 mz_introspection.mz_expected_group_size_advice megsa ON (megsa.region_id = valid_id)) ON (megsa.dataflow_id = mdgi.id)"]);
1078 }
1079 }
1080
1081 from.push("JOIN mz_introspection.mz_mappable_objects mo ON (mlm.global_id = mo.global_id)");
1082
1083 let ctes = if !ctes.is_empty() {
1084 format!(
1085 "WITH {}",
1086 separated(
1087 ",\n",
1088 ctes.iter()
1089 .map(|(name, defn)| format!("{name} AS ({defn})"))
1090 )
1091 )
1092 } else {
1093 String::new()
1094 };
1095 let columns = separated(", ", columns);
1096 let from = separated(" ", from);
1097 let predicates = separated(" AND ", predicates);
1098 let order_by = separated(", ", order_by);
1099 let query = format!(
1100 r#"{ctes}
1101SELECT {columns}
1102FROM {from}
1103WHERE {predicates}
1104ORDER BY {order_by}"#
1105 );
1106
1107 if statement.as_sql {
1108 let rows = vec![Row::pack_slice(&[Datum::String(
1109 &mz_sql_pretty::pretty_str_simple(&query, 80).map_err(|e| {
1110 PlanError::Unstructured(format!("internal error parsing our own SQL: {e}"))
1111 })?,
1112 )])];
1113 let typ = SqlRelationType::new(vec![SqlScalarType::String.nullable(false)]);
1114
1115 Ok(Plan::Select(SelectPlan::immediate(rows, typ)))
1116 } else {
1117 let (show_select, resolved_ids) = ShowSelect::new_from_bare_query(scx, query)?;
1118 scx.record_sql_impl_ids(&resolved_ids);
1119 show_select.plan()
1120 }
1121}
1122
1123pub fn plan_explain_analyze_cluster(
1124 scx: &StatementContext,
1125 statement: ExplainAnalyzeClusterStatement,
1126 _params: &Params,
1127) -> Result<Plan, PlanError> {
1128 let mut ctes = Vec::with_capacity(4); let mut columns = vec!["mo.name AS object", "mo.global_id AS global_id"];
1153 let mut from = vec!["mz_introspection.mz_mappable_objects mo"];
1154 let mut predicates = vec![];
1155 let mut order_by = vec![];
1156
1157 let ExplainAnalyzeComputationProperties { properties, skew } = statement.properties;
1158 let mut worker_id = None;
1159 let mut seen_properties = BTreeSet::new();
1160 for property in properties {
1161 if !seen_properties.insert(property) {
1163 continue;
1164 }
1165
1166 match property {
1167 ExplainAnalyzeComputationProperty::Memory => {
1168 if skew {
1169 let mut set_worker_id = false;
1170 if let Some(worker_id) = worker_id {
1171 predicates.push(format!(
1173 "(om.worker_id = {worker_id} OR om.worker_id IS NULL OR {worker_id} IS NULL)"
1174 ));
1175 } else {
1176 worker_id = Some("om.worker_id");
1177 columns.push("om.worker_id AS worker_id");
1178 set_worker_id = true; };
1180
1181 ctes.push((
1183 "per_operator_memory_summary",
1184 r#"
1185SELECT mlm.global_id AS global_id,
1186 mlm.lir_id AS lir_id,
1187 SUM(mas.size) AS total_memory,
1188 SUM(mas.records) AS total_records,
1189 CASE WHEN COUNT(DISTINCT mas.worker_id) <> 0 THEN SUM(mas.size) / COUNT(DISTINCT mas.worker_id) ELSE NULL END AS avg_memory,
1190 CASE WHEN COUNT(DISTINCT mas.worker_id) <> 0 THEN SUM(mas.records) / COUNT(DISTINCT mas.worker_id) ELSE NULL END AS avg_records
1191FROM mz_introspection.mz_lir_mapping mlm
1192 CROSS JOIN generate_series((mlm.operator_id_start) :: int8, (mlm.operator_id_end - 1) :: int8) AS valid_id
1193 JOIN mz_introspection.mz_arrangement_sizes_per_worker mas
1194 ON (mas.operator_id = valid_id)
1195GROUP BY mlm.global_id, mlm.lir_id"#,
1196 ));
1197
1198 ctes.push((
1200 "per_operator_memory_per_worker",
1201 r#"
1202SELECT mlm.global_id AS global_id,
1203 mlm.lir_id AS lir_id,
1204 mas.worker_id AS worker_id,
1205 SUM(mas.size) AS worker_memory,
1206 SUM(mas.records) AS worker_records
1207FROM mz_introspection.mz_lir_mapping mlm
1208 CROSS JOIN generate_series((mlm.operator_id_start) :: int8, (mlm.operator_id_end - 1) :: int8) AS valid_id
1209 JOIN mz_introspection.mz_arrangement_sizes_per_worker mas
1210 ON (mas.operator_id = valid_id)
1211GROUP BY mlm.global_id, mlm.lir_id, mas.worker_id"#,
1212 ));
1213
1214 ctes.push((
1216 "per_operator_memory_ratios",
1217 r#"
1218SELECT pompw.global_id AS global_id,
1219 pompw.lir_id AS lir_id,
1220 pompw.worker_id AS worker_id,
1221 CASE WHEN pompw.worker_id IS NOT NULL AND poms.avg_memory <> 0 THEN ROUND(pompw.worker_memory / poms.avg_memory, 2) ELSE NULL END AS memory_ratio,
1222 CASE WHEN pompw.worker_id IS NOT NULL AND poms.avg_records <> 0 THEN ROUND(pompw.worker_records / poms.avg_records, 2) ELSE NULL END AS records_ratio
1223 FROM per_operator_memory_per_worker pompw
1224 JOIN per_operator_memory_summary poms
1225 USING (global_id, lir_id)
1226"#,
1227 ));
1228
1229 ctes.push((
1231 "object_memory",
1232 r#"
1233SELECT pompw.global_id AS global_id,
1234 pompw.worker_id AS worker_id,
1235 MAX(pomr.memory_ratio) AS max_operator_memory_ratio,
1236 MAX(pomr.records_ratio) AS max_operator_records_ratio,
1237 SUM(pompw.worker_memory) AS worker_memory,
1238 SUM(pompw.worker_records) AS worker_records
1239FROM per_operator_memory_per_worker pompw
1240 JOIN per_operator_memory_ratios pomr
1241 USING (global_id, worker_id, lir_id)
1242GROUP BY pompw.global_id, pompw.worker_id
1243"#,
1244 ));
1245
1246 ctes.push(("object_average_memory", r#"
1248SELECT om.global_id AS global_id,
1249 SUM(om.worker_memory) AS total_memory,
1250 CASE WHEN COUNT(DISTINCT om.worker_id) <> 0 THEN SUM(om.worker_memory) / COUNT(DISTINCT om.worker_id) ELSE NULL END AS avg_memory,
1251 SUM(om.worker_records) AS total_records,
1252 CASE WHEN COUNT(DISTINCT om.worker_id) <> 0 THEN SUM(om.worker_records) / COUNT(DISTINCT om.worker_id) ELSE NULL END AS avg_records
1253 FROM object_memory om
1254GROUP BY om.global_id"#));
1255
1256 from.push("LEFT JOIN object_memory om USING (global_id)");
1257 from.push("LEFT JOIN object_average_memory oam USING (global_id)");
1258
1259 columns.extend([
1260 "om.max_operator_memory_ratio AS max_operator_memory_ratio",
1261 "pg_size_pretty(om.worker_memory) AS worker_memory",
1262 "pg_size_pretty(oam.avg_memory) AS avg_memory",
1263 "pg_size_pretty(oam.total_memory) AS total_memory",
1264 "om.max_operator_records_ratio AS max_operator_records_ratio",
1265 "om.worker_records AS worker_records",
1266 "oam.avg_records AS avg_records",
1267 "oam.total_records AS total_records",
1268 ]);
1269
1270 order_by.extend([
1271 "max_operator_memory_ratio DESC NULLS LAST",
1272 "max_operator_records_ratio DESC NULLS LAST",
1273 "om.worker_memory DESC NULLS LAST",
1274 "worker_records DESC NULLS LAST",
1275 ]);
1276
1277 if set_worker_id {
1278 order_by.push("worker_id");
1279 }
1280 } else {
1281 ctes.push((
1283 "per_operator_memory_totals",
1284 r#"
1285 SELECT mlm.global_id AS global_id,
1286 mlm.lir_id AS lir_id,
1287 SUM(mas.size) AS total_memory,
1288 SUM(mas.records) AS total_records
1289 FROM mz_introspection.mz_lir_mapping mlm
1290 CROSS JOIN generate_series((mlm.operator_id_start) :: int8, (mlm.operator_id_end - 1) :: int8) AS valid_id
1291 JOIN mz_introspection.mz_arrangement_sizes_per_worker mas
1292 ON (mas.operator_id = valid_id)
1293 GROUP BY mlm.global_id, mlm.lir_id"#,
1294 ));
1295
1296 ctes.push((
1297 "object_memory_totals",
1298 r#"
1299SELECT pomt.global_id AS global_id,
1300 SUM(pomt.total_memory) AS total_memory,
1301 SUM(pomt.total_records) AS total_records
1302FROM per_operator_memory_totals pomt
1303GROUP BY pomt.global_id
1304"#,
1305 ));
1306
1307 from.push("LEFT JOIN object_memory_totals omt USING (global_id)");
1308 columns.extend([
1309 "pg_size_pretty(omt.total_memory) AS total_memory",
1310 "omt.total_records AS total_records",
1311 ]);
1312 order_by.extend([
1313 "omt.total_memory DESC NULLS LAST",
1314 "total_records DESC NULLS LAST",
1315 ]);
1316 }
1317 }
1318 ExplainAnalyzeComputationProperty::Cpu => {
1319 if skew {
1320 let mut set_worker_id = false;
1321 if let Some(worker_id) = worker_id {
1322 predicates.push(format!(
1324 "(oc.worker_id = {worker_id} OR oc.worker_id IS NULL OR {worker_id} IS NULL)"
1325 ));
1326 } else {
1327 worker_id = Some("oc.worker_id");
1328 columns.push("oc.worker_id AS worker_id");
1329 set_worker_id = true; };
1331
1332 ctes.push((
1334 "per_operator_cpu_summary",
1335 r#"
1336SELECT mlm.global_id AS global_id,
1337 mlm.lir_id AS lir_id,
1338 SUM(mse.elapsed_ns) AS total_ns,
1339 CASE WHEN COUNT(DISTINCT mse.worker_id) <> 0 THEN SUM(mse.elapsed_ns) / COUNT(DISTINCT mse.worker_id) ELSE NULL END AS avg_ns
1340FROM mz_introspection.mz_lir_mapping mlm
1341CROSS JOIN generate_series((mlm.operator_id_start) :: int8, (mlm.operator_id_end - 1) :: int8) AS valid_id
1342 JOIN mz_introspection.mz_scheduling_elapsed_per_worker mse
1343 ON (mse.id = valid_id)
1344GROUP BY mlm.global_id, mlm.lir_id"#,
1345));
1346
1347 ctes.push((
1349 "per_operator_cpu_per_worker",
1350 r#"
1351SELECT mlm.global_id AS global_id,
1352 mlm.lir_id AS lir_id,
1353 mse.worker_id AS worker_id,
1354 SUM(mse.elapsed_ns) AS worker_ns
1355FROM mz_introspection.mz_lir_mapping mlm
1356CROSS JOIN generate_series((mlm.operator_id_start) :: int8, (mlm.operator_id_end - 1) :: int8) AS valid_id
1357 JOIN mz_introspection.mz_scheduling_elapsed_per_worker mse
1358 ON (mse.id = valid_id)
1359GROUP BY mlm.global_id, mlm.lir_id, mse.worker_id"#,
1360 ));
1361
1362 ctes.push((
1364 "per_operator_cpu_ratios",
1365 r#"
1366SELECT pocpw.global_id AS global_id,
1367 pocpw.lir_id AS lir_id,
1368 pocpw.worker_id AS worker_id,
1369 CASE WHEN pocpw.worker_id IS NOT NULL AND pocs.avg_ns <> 0 THEN ROUND(pocpw.worker_ns / pocs.avg_ns, 2) ELSE NULL END AS cpu_ratio
1370FROM per_operator_cpu_per_worker pocpw
1371 JOIN per_operator_cpu_summary pocs
1372 USING (global_id, lir_id)
1373"#,
1374 ));
1375
1376 ctes.push((
1378 "object_cpu",
1379 r#"
1380SELECT pocpw.global_id AS global_id,
1381 pocpw.worker_id AS worker_id,
1382 MAX(pomr.cpu_ratio) AS max_operator_cpu_ratio,
1383 SUM(pocpw.worker_ns) AS worker_ns
1384FROM per_operator_cpu_per_worker pocpw
1385 JOIN per_operator_cpu_ratios pomr
1386 USING (global_id, worker_id, lir_id)
1387GROUP BY pocpw.global_id, pocpw.worker_id
1388"#,
1389 ));
1390
1391 ctes.push((
1393 "object_average_cpu",
1394 r#"
1395SELECT oc.global_id AS global_id,
1396 SUM(oc.worker_ns) AS total_ns,
1397 CASE WHEN COUNT(DISTINCT oc.worker_id) <> 0 THEN SUM(oc.worker_ns) / COUNT(DISTINCT oc.worker_id) ELSE NULL END AS avg_ns
1398 FROM object_cpu oc
1399GROUP BY oc.global_id"#,));
1400
1401 from.push("LEFT JOIN object_cpu oc USING (global_id)");
1402 from.push("LEFT JOIN object_average_cpu oac USING (global_id)");
1403
1404 columns.extend([
1405 "oc.max_operator_cpu_ratio AS max_operator_cpu_ratio",
1406 "oc.worker_ns / 1000 * '1 microsecond'::interval AS worker_elapsed",
1407 "oac.avg_ns / 1000 * '1 microsecond'::interval AS avg_elapsed",
1408 "oac.total_ns / 1000 * '1 microsecond'::interval AS total_elapsed",
1409 ]);
1410
1411 order_by.extend([
1412 "max_operator_cpu_ratio DESC NULLS LAST",
1413 "worker_elapsed DESC NULLS LAST",
1414 ]);
1415
1416 if set_worker_id {
1417 order_by.push("worker_id");
1418 }
1419 } else {
1420 ctes.push((
1422 "per_operator_cpu_totals",
1423 r#"
1424 SELECT mlm.global_id AS global_id,
1425 mlm.lir_id AS lir_id,
1426 SUM(mse.elapsed_ns) AS total_ns
1427 FROM mz_introspection.mz_lir_mapping mlm
1428 CROSS JOIN generate_series((mlm.operator_id_start) :: int8, (mlm.operator_id_end - 1) :: int8) AS valid_id
1429 JOIN mz_introspection.mz_scheduling_elapsed_per_worker mse
1430 ON (mse.id = valid_id)
1431 GROUP BY mlm.global_id, mlm.lir_id"#,
1432 ));
1433
1434 ctes.push((
1435 "object_cpu_totals",
1436 r#"
1437SELECT poct.global_id AS global_id,
1438 SUM(poct.total_ns) AS total_ns
1439FROM per_operator_cpu_totals poct
1440GROUP BY poct.global_id
1441"#,
1442 ));
1443
1444 from.push("LEFT JOIN object_cpu_totals oct USING (global_id)");
1445 columns
1446 .push("oct.total_ns / 1000 * '1 microsecond'::interval AS total_elapsed");
1447 order_by.extend(["total_elapsed DESC NULLS LAST"]);
1448 }
1449 }
1450 }
1451 }
1452
1453 let ctes = if !ctes.is_empty() {
1455 format!(
1456 "WITH {}",
1457 separated(
1458 ",\n",
1459 ctes.iter()
1460 .map(|(name, defn)| format!("{name} AS ({defn})"))
1461 )
1462 )
1463 } else {
1464 String::new()
1465 };
1466 let columns = separated(", ", columns);
1467 let from = separated(" ", from);
1468 let predicates = if !predicates.is_empty() {
1469 format!("WHERE {}", separated(" AND ", predicates))
1470 } else {
1471 String::new()
1472 };
1473 order_by.push("mo.name DESC");
1475 let order_by = separated(", ", order_by);
1476 let query = format!(
1477 r#"{ctes}
1478SELECT {columns}
1479FROM {from}
1480{predicates}
1481ORDER BY {order_by}"#
1482 );
1483
1484 if statement.as_sql {
1485 let rows = vec![Row::pack_slice(&[Datum::String(
1486 &mz_sql_pretty::pretty_str_simple(&query, 80).map_err(|e| {
1487 PlanError::Unstructured(format!("internal error parsing our own SQL: {e}"))
1488 })?,
1489 )])];
1490 let typ = SqlRelationType::new(vec![SqlScalarType::String.nullable(false)]);
1491
1492 Ok(Plan::Select(SelectPlan::immediate(rows, typ)))
1493 } else {
1494 let (show_select, resolved_ids) = ShowSelect::new_from_bare_query(scx, query)?;
1495 scx.record_sql_impl_ids(&resolved_ids);
1496 show_select.plan()
1497 }
1498}
1499
1500pub fn plan_explain_timestamp(
1501 scx: &StatementContext,
1502 explain: ExplainTimestampStatement<Aug>,
1503) -> Result<Plan, PlanError> {
1504 let (format, _verbose_syntax) = match explain.format() {
1505 mz_sql_parser::ast::ExplainFormat::Text => (ExplainFormat::Text, false),
1506 mz_sql_parser::ast::ExplainFormat::VerboseText => (ExplainFormat::Text, true),
1507 mz_sql_parser::ast::ExplainFormat::Json => (ExplainFormat::Json, false),
1508 mz_sql_parser::ast::ExplainFormat::Dot => (ExplainFormat::Dot, false),
1509 };
1510
1511 let raw_plan = {
1512 let query::PlannedRootQuery {
1513 expr: raw_plan,
1514 desc: _,
1515 finishing: _,
1516 scope: _,
1517 } = query::plan_root_query(scx, explain.select.query, QueryLifetime::OneShot)?;
1518 if raw_plan.contains_parameters()? {
1519 return Err(PlanError::ParameterNotAllowed(
1520 "EXPLAIN TIMESTAMP".to_string(),
1521 ));
1522 }
1523
1524 raw_plan
1525 };
1526 let when = query::plan_as_of(scx, explain.select.as_of)?;
1527
1528 Ok(Plan::ExplainTimestamp(ExplainTimestampPlan {
1529 format,
1530 raw_plan,
1531 when,
1532 }))
1533}
1534
1535generate_extracted_config!(SubscribeOption, (Snapshot, bool), (Progress, bool));
1536
1537pub fn describe_subscribe(
1538 scx: &StatementContext,
1539 stmt: SubscribeStatement<Aug>,
1540) -> Result<StatementDesc, PlanError> {
1541 let relation_desc = match stmt.relation {
1542 SubscribeRelation::Name(name) => {
1543 let item = scx.get_item_by_resolved_name(&name)?;
1544 match item.relation_desc() {
1545 Some(desc) => desc.into_owned(),
1546 None => sql_bail!(
1547 "'{}' cannot be subscribed to because it is a {}",
1548 name.full_name_str(),
1549 item.item_type(),
1550 ),
1551 }
1552 }
1553 SubscribeRelation::Query(query) => {
1554 let query::PlannedRootQuery { desc, .. } =
1555 query::plan_root_query(scx, query, QueryLifetime::Subscribe)?;
1556 desc
1557 }
1558 };
1559 let SubscribeOptionExtracted { progress, .. } = stmt.options.try_into()?;
1560 let progress = progress.unwrap_or(false);
1561 let mut desc = RelationDesc::builder().with_column(
1562 "mz_timestamp",
1563 SqlScalarType::Numeric {
1564 max_scale: Some(NumericMaxScale::ZERO),
1565 }
1566 .nullable(false),
1567 );
1568 if progress {
1569 desc = desc.with_column("mz_progressed", SqlScalarType::Bool.nullable(false));
1570 }
1571
1572 let debezium = matches!(stmt.output, SubscribeOutput::EnvelopeDebezium { .. });
1573 match stmt.output {
1574 SubscribeOutput::Diffs | SubscribeOutput::WithinTimestampOrderBy { .. } => {
1575 desc = desc.with_column("mz_diff", SqlScalarType::Int64.nullable(true));
1576 for (name, mut ty) in relation_desc.into_iter() {
1577 if progress {
1578 ty.nullable = true;
1579 }
1580 desc = desc.with_column(name, ty);
1581 }
1582 }
1583 SubscribeOutput::EnvelopeUpsert { key_columns }
1584 | SubscribeOutput::EnvelopeDebezium { key_columns } => {
1585 desc = desc.with_column("mz_state", SqlScalarType::String.nullable(true));
1586 let key_columns = key_columns
1587 .into_iter()
1588 .map(normalize::column_name)
1589 .collect_vec();
1590 let mut before_values_desc = RelationDesc::builder();
1591 let mut after_values_desc = RelationDesc::builder();
1592
1593 for column_name in &key_columns {
1595 let mut column_ty = relation_desc
1596 .get_by_name(column_name)
1597 .map(|(_pos, ty)| ty.clone())
1598 .ok_or_else(|| PlanError::UnknownColumn {
1599 table: None,
1600 column: column_name.clone(),
1601 similar: Box::new([]),
1602 })?;
1603 if progress {
1604 column_ty.nullable = true;
1605 }
1606 desc = desc.with_column(column_name, column_ty);
1607 }
1608
1609 for (mut name, mut ty) in relation_desc
1612 .into_iter()
1613 .filter(|(name, _ty)| !key_columns.contains(name))
1614 {
1615 ty.nullable = true;
1616 before_values_desc =
1617 before_values_desc.with_column(format!("before_{}", name), ty.clone());
1618 if debezium {
1619 name = format!("after_{}", name).into();
1620 }
1621 after_values_desc = after_values_desc.with_column(name, ty);
1622 }
1623
1624 if debezium {
1625 desc = desc.concat(before_values_desc);
1626 }
1627 desc = desc.concat(after_values_desc);
1628 }
1629 }
1630 Ok(StatementDesc::new(Some(desc.finish())))
1631}
1632
1633pub fn plan_subscribe(
1634 scx: &StatementContext,
1635 SubscribeStatement {
1636 relation,
1637 options,
1638 as_of,
1639 up_to,
1640 output,
1641 }: SubscribeStatement<Aug>,
1642 params: &Params,
1643 copy_to: Option<CopyFormat>,
1644) -> Result<Plan, PlanError> {
1645 let (from, desc, scope) = match relation {
1646 SubscribeRelation::Name(name) => {
1647 let item = scx.get_item_by_resolved_name(&name)?;
1648 let Some(desc) = item.relation_desc() else {
1649 sql_bail!(
1650 "'{}' cannot be subscribed to because it is a {}",
1651 name.full_name_str(),
1652 item.item_type(),
1653 );
1654 };
1655 let item_name = match name {
1656 ResolvedItemName::Item { full_name, .. } => Some(full_name.into()),
1657 _ => None,
1658 };
1659 let scope = Scope::from_source(item_name, desc.iter().map(|(name, _type)| name));
1660 (
1661 SubscribeFrom::Id(item.global_id()),
1662 desc.into_owned(),
1663 scope,
1664 )
1665 }
1666 SubscribeRelation::Query(query) => {
1667 #[allow(deprecated)] let query::PlannedRootQuery {
1669 mut expr,
1670 desc,
1671 finishing,
1672 scope,
1673 } = query::plan_root_query(scx, query, QueryLifetime::Subscribe)?;
1674 expr.bind_parameters_and_simplify_offset(scx, QueryLifetime::Subscribe, params)?;
1675 let query = query::PlannedRootQuery {
1676 expr,
1677 desc,
1678 finishing,
1679 scope,
1680 };
1681 assert!(HirRelationExpr::is_trivial_row_set_finishing_hir(
1685 &query.finishing,
1686 query.desc.arity()
1687 ));
1688 let desc = query.desc.clone();
1689 (
1690 SubscribeFrom::Query {
1691 expr: query.expr,
1692 desc: query.desc,
1693 },
1694 desc,
1695 query.scope,
1696 )
1697 }
1698 };
1699
1700 let when = query::plan_as_of(scx, as_of)?;
1701 let up_to = up_to
1702 .map(|up_to| plan_as_of_or_up_to(scx, up_to))
1703 .transpose()?;
1704
1705 let qcx = QueryContext::root(scx, QueryLifetime::Subscribe);
1706 let ecx = ExprContext {
1707 qcx: &qcx,
1708 name: "",
1709 scope: &scope,
1710 relation_type: desc.typ(),
1711 allow_aggregates: false,
1712 allow_subqueries: true,
1713 allow_parameters: true,
1714 allow_windows: false,
1715 };
1716
1717 let output_columns: Vec<_> = scope.column_names().enumerate().collect();
1718 let output = match output {
1719 SubscribeOutput::Diffs => plan::SubscribeOutput::Diffs,
1720 SubscribeOutput::EnvelopeUpsert { key_columns } => {
1721 let order_by = key_columns
1722 .iter()
1723 .map(|ident| OrderByExpr {
1724 expr: Expr::Identifier(vec![ident.clone()]),
1725 asc: None,
1726 nulls_last: None,
1727 })
1728 .collect_vec();
1729 let (order_by, map_exprs) = query::plan_order_by_exprs(
1730 &ExprContext {
1731 name: "ENVELOPE UPSERT KEY clause",
1732 ..ecx
1733 },
1734 &order_by[..],
1735 &output_columns[..],
1736 )?;
1737 if !map_exprs.is_empty() {
1738 return Err(PlanError::InvalidKeysInSubscribeEnvelopeUpsert);
1739 }
1740 check_distinct_key_columns(&order_by, &output_columns)?;
1741 plan::SubscribeOutput::EnvelopeUpsert {
1742 order_by_keys: order_by,
1743 }
1744 }
1745 SubscribeOutput::EnvelopeDebezium { key_columns } => {
1746 scx.require_feature_flag(&vars::ENABLE_ENVELOPE_DEBEZIUM_IN_SUBSCRIBE)?;
1747 let order_by = key_columns
1748 .iter()
1749 .map(|ident| OrderByExpr {
1750 expr: Expr::Identifier(vec![ident.clone()]),
1751 asc: None,
1752 nulls_last: None,
1753 })
1754 .collect_vec();
1755 let (order_by, map_exprs) = query::plan_order_by_exprs(
1756 &ExprContext {
1757 name: "ENVELOPE DEBEZIUM KEY clause",
1758 ..ecx
1759 },
1760 &order_by[..],
1761 &output_columns[..],
1762 )?;
1763 if !map_exprs.is_empty() {
1764 return Err(PlanError::InvalidKeysInSubscribeEnvelopeDebezium);
1765 }
1766 check_distinct_key_columns(&order_by, &output_columns)?;
1767 plan::SubscribeOutput::EnvelopeDebezium {
1768 order_by_keys: order_by,
1769 }
1770 }
1771 SubscribeOutput::WithinTimestampOrderBy { order_by } => {
1772 scx.require_feature_flag(&vars::ENABLE_WITHIN_TIMESTAMP_ORDER_BY_IN_SUBSCRIBE)?;
1773 let mz_diff = "mz_diff".into();
1774 let output_columns = std::iter::once((0, &mz_diff))
1775 .chain(output_columns.into_iter().map(|(i, c)| (i + 1, c)))
1776 .collect_vec();
1777 match query::plan_order_by_exprs(
1778 &ExprContext {
1779 name: "WITHIN TIMESTAMP ORDER BY clause",
1780 ..ecx
1781 },
1782 &order_by[..],
1783 &output_columns[..],
1784 ) {
1785 Err(PlanError::UnknownColumn {
1786 table: None,
1787 column,
1788 similar: _,
1789 }) if &column == &mz_diff => {
1790 return Err(PlanError::InvalidOrderByInSubscribeWithinTimestampOrderBy);
1793 }
1794 Err(e) => return Err(e),
1795 Ok((order_by, map_exprs)) => {
1796 if !map_exprs.is_empty() {
1797 return Err(PlanError::InvalidOrderByInSubscribeWithinTimestampOrderBy);
1798 }
1799
1800 plan::SubscribeOutput::WithinTimestampOrderBy { order_by }
1801 }
1802 }
1803 }
1804 };
1805
1806 let SubscribeOptionExtracted {
1807 progress, snapshot, ..
1808 } = options.try_into()?;
1809 Ok(Plan::Subscribe(SubscribePlan {
1810 from,
1811 when,
1812 up_to,
1813 with_snapshot: snapshot.unwrap_or(true),
1814 copy_to,
1815 emit_progress: progress.unwrap_or(false),
1816 output,
1817 }))
1818}
1819
1820fn check_distinct_key_columns(
1823 order_by: &[ColumnOrder],
1824 output_columns: &[(usize, &mz_repr::ColumnName)],
1825) -> Result<(), PlanError> {
1826 let mut seen = BTreeSet::new();
1827 for co in order_by {
1828 if !seen.insert(co.column) {
1829 return Err(PlanError::DuplicateKeyColumnInSubscribeEnvelope {
1830 column_name: output_columns[co.column].1.to_string(),
1831 });
1832 }
1833 }
1834 Ok(())
1835}
1836
1837pub fn describe_copy_from_table(
1838 scx: &StatementContext,
1839 table_name: <Aug as AstInfo>::ItemName,
1840 columns: Vec<Ident>,
1841) -> Result<StatementDesc, PlanError> {
1842 let (_, desc, _, _) = query::plan_copy_from(scx, table_name, columns)?;
1843 Ok(StatementDesc::new(Some(desc)))
1844}
1845
1846pub fn describe_copy_item(
1847 scx: &StatementContext,
1848 object_name: <Aug as AstInfo>::ItemName,
1849 columns: Vec<Ident>,
1850) -> Result<StatementDesc, PlanError> {
1851 let (_, desc, _, _) = query::plan_copy_item(scx, object_name, columns)?;
1852 Ok(StatementDesc::new(Some(desc)))
1853}
1854
1855pub fn describe_copy(
1856 scx: &StatementContext,
1857 CopyStatement {
1858 relation,
1859 direction,
1860 ..
1861 }: CopyStatement<Aug>,
1862) -> Result<StatementDesc, PlanError> {
1863 Ok(match (relation, direction) {
1864 (CopyRelation::Named { name, columns }, CopyDirection::To) => {
1865 describe_copy_item(scx, name, columns)?
1866 }
1867 (CopyRelation::Named { name, columns }, CopyDirection::From) => {
1868 describe_copy_from_table(scx, name, columns)?
1869 }
1870 (CopyRelation::Select(stmt), _) => describe_select(scx, stmt)?,
1871 (CopyRelation::Subscribe(stmt), _) => describe_subscribe(scx, stmt)?,
1872 }
1873 .with_is_copy())
1874}
1875
1876fn plan_copy_to_expr(
1877 scx: &StatementContext,
1878 select_plan: SelectPlan,
1879 desc: RelationDesc,
1880 to: &Expr<Aug>,
1881 format: CopyFormat,
1882 options: CopyOptionExtracted,
1883) -> Result<Plan, PlanError> {
1884 let conn_id = match options.aws_connection {
1885 Some(conn_id) => CatalogItemId::from(conn_id),
1886 None => sql_bail!("AWS CONNECTION is required for COPY ... TO <expr>"),
1887 };
1888 let connection = scx.get_item(&conn_id).connection()?;
1889
1890 match connection {
1891 mz_storage_types::connections::Connection::Aws(_) => {}
1892 _ => sql_bail!("only AWS CONNECTION is supported for COPY ... TO <expr>"),
1893 }
1894
1895 let format = match format {
1896 CopyFormat::Csv => {
1897 let quote = extract_byte_param_value(options.quote, "quote")?;
1898 let escape = extract_byte_param_value(options.escape, "escape")?;
1899 let delimiter = extract_byte_param_value(options.delimiter, "delimiter")?;
1900 S3SinkFormat::PgCopy(CopyFormatParams::Csv(
1901 CopyCsvFormatParams::try_new(
1902 delimiter,
1903 quote,
1904 escape,
1905 options.header,
1906 options.null,
1907 )
1908 .map_err(|e| sql_err!("{}", e))?,
1909 ))
1910 }
1911 CopyFormat::Parquet => {
1912 ArrowBuilder::validate_desc_for_parquet(&desc, |_| None)
1915 .map_err(|e| sql_err!("{}", e))?;
1916 S3SinkFormat::Parquet
1917 }
1918 CopyFormat::Binary => bail_unsupported!("FORMAT BINARY"),
1919 CopyFormat::Text => bail_unsupported!("FORMAT TEXT"),
1920 };
1921
1922 let mut to_expr = to.clone();
1924 transform_ast::transform(scx, &mut to_expr)?;
1925 let relation_type = RelationDesc::empty();
1926 let ecx = &ExprContext {
1927 qcx: &QueryContext::root(scx, QueryLifetime::OneShot),
1928 name: "COPY TO target",
1929 scope: &Scope::empty(),
1930 relation_type: relation_type.typ(),
1931 allow_aggregates: false,
1932 allow_subqueries: false,
1933 allow_parameters: false,
1934 allow_windows: false,
1935 };
1936
1937 let to = plan_expr(ecx, &to_expr)?.type_as(ecx, &SqlScalarType::String)?;
1938
1939 if options.max_file_size.as_bytes() < MIN_S3_SINK_FILE_SIZE.as_bytes() {
1940 sql_bail!(
1941 "MAX FILE SIZE cannot be less than {}",
1942 MIN_S3_SINK_FILE_SIZE
1943 );
1944 }
1945 if options.max_file_size.as_bytes() > MAX_S3_SINK_FILE_SIZE.as_bytes() {
1946 sql_bail!(
1947 "MAX FILE SIZE cannot be greater than {}",
1948 MAX_S3_SINK_FILE_SIZE
1949 );
1950 }
1951
1952 Ok(Plan::CopyTo(CopyToPlan {
1953 select_plan,
1954 desc,
1955 to,
1956 connection: connection.to_owned(),
1957 connection_id: conn_id,
1958 format,
1959 max_file_size: options.max_file_size.as_bytes(),
1960 }))
1961}
1962
1963fn plan_copy_from(
1964 scx: &StatementContext,
1965 target: &CopyTarget<Aug>,
1966 table_name: ResolvedItemName,
1967 columns: Vec<Ident>,
1968 format: Option<CopyFormat>,
1969 options: CopyOptionExtracted,
1970) -> Result<Plan, PlanError> {
1971 fn only_available_with_csv<T>(option: Option<T>, param: &str) -> Result<(), PlanError> {
1972 match option {
1973 Some(_) => sql_bail!("COPY {} available only in CSV mode", param),
1974 None => Ok(()),
1975 }
1976 }
1977
1978 let source = match target {
1979 CopyTarget::Stdin => CopyFromSource::Stdin,
1980 CopyTarget::Expr(from) => {
1981 let mut from_expr = from.clone();
1983 transform_ast::transform(scx, &mut from_expr)?;
1984 let relation_type = RelationDesc::empty();
1985 let ecx = &ExprContext {
1986 qcx: &QueryContext::root(scx, QueryLifetime::OneShot),
1987 name: "COPY FROM target",
1988 scope: &Scope::empty(),
1989 relation_type: relation_type.typ(),
1990 allow_aggregates: false,
1991 allow_subqueries: false,
1992 allow_parameters: false,
1993 allow_windows: false,
1994 };
1995 let from = plan_expr(ecx, &from_expr)?.type_as(ecx, &SqlScalarType::String)?;
1996
1997 match options.aws_connection {
1998 Some(conn_id) => {
1999 let conn_id = CatalogItemId::from(conn_id);
2000
2001 let connection = match scx.get_item(&conn_id).connection()? {
2003 mz_storage_types::connections::Connection::Aws(conn) => conn,
2004 _ => sql_bail!("only AWS CONNECTION is supported in COPY ... FROM"),
2005 };
2006
2007 CopyFromSource::AwsS3 {
2008 uri: from,
2009 connection,
2010 connection_id: conn_id,
2011 }
2012 }
2013 None => CopyFromSource::Url(from),
2014 }
2015 }
2016 CopyTarget::Stdout => bail_never_supported!("COPY FROM {} not supported", target),
2017 };
2018
2019 let format = match &source {
2024 CopyFromSource::Stdin => format.unwrap_or(CopyFormat::Text),
2025 CopyFromSource::Url(_) | CopyFromSource::AwsS3 { .. } => match format {
2026 None => sql_bail!("COPY FROM <expr> requires a FORMAT option"),
2027 Some(CopyFormat::Text) => bail_unsupported!("FORMAT TEXT"),
2028 Some(CopyFormat::Binary) => bail_unsupported!("FORMAT BINARY"),
2029 Some(format @ (CopyFormat::Csv | CopyFormat::Parquet)) => format,
2030 },
2031 };
2032
2033 let params = match format {
2034 CopyFormat::Text => {
2035 only_available_with_csv(options.quote, "quote")?;
2036 only_available_with_csv(options.escape, "escape")?;
2037 only_available_with_csv(options.header, "HEADER")?;
2038 let delimiter =
2039 extract_byte_param_value(options.delimiter, "delimiter")?.unwrap_or(b'\t');
2040 let null = match options.null {
2041 Some(null) => Cow::from(null),
2042 None => Cow::from("\\N"),
2043 };
2044 CopyFormatParams::Text(CopyTextFormatParams { null, delimiter })
2045 }
2046 CopyFormat::Csv => {
2047 let quote = extract_byte_param_value(options.quote, "quote")?;
2048 let escape = extract_byte_param_value(options.escape, "escape")?;
2049 let delimiter = extract_byte_param_value(options.delimiter, "delimiter")?;
2050 CopyFormatParams::Csv(
2051 CopyCsvFormatParams::try_new(
2052 delimiter,
2053 quote,
2054 escape,
2055 options.header,
2056 options.null,
2057 )
2058 .map_err(|e| sql_err!("{}", e))?,
2059 )
2060 }
2061 CopyFormat::Binary => bail_unsupported!("FORMAT BINARY"),
2062 CopyFormat::Parquet => CopyFormatParams::Parquet,
2063 };
2064
2065 let filter = match (options.files, options.pattern) {
2066 (Some(_), Some(_)) => bail_unsupported!("must specify one of FILES or PATTERN"),
2067 (Some(files), None) => Some(CopyFromFilter::Files(files)),
2068 (None, Some(pattern)) => Some(CopyFromFilter::Pattern(pattern)),
2069 (None, None) => None,
2070 };
2071
2072 if filter.is_some() && matches!(source, CopyFromSource::Stdin) {
2073 bail_unsupported!("COPY FROM ... WITH (FILES ...) only supported from a URL")
2074 }
2075
2076 let table_name_string = table_name.full_name_str();
2077
2078 let (id, source_desc, columns, maybe_mfp) = query::plan_copy_from(scx, table_name, columns)?;
2079
2080 let Some(mfp) = maybe_mfp else {
2081 sql_bail!("[internal error] COPY FROM ... expects an MFP to be produced");
2082 };
2083
2084 Ok(Plan::CopyFrom(CopyFromPlan {
2085 target_id: id,
2086 target_name: table_name_string,
2087 source,
2088 columns,
2089 source_desc,
2090 mfp,
2091 params,
2092 filter,
2093 }))
2094}
2095
2096fn extract_byte_param_value(v: Option<String>, param_name: &str) -> Result<Option<u8>, PlanError> {
2097 match v {
2098 Some(v) if v.len() == 1 => Ok(Some(v.as_bytes()[0])),
2099 Some(..) => sql_bail!("COPY {} must be a single one-byte character", param_name),
2100 None => Ok(None),
2101 }
2102}
2103
2104generate_extracted_config!(
2105 CopyOption,
2106 (Format, String),
2107 (Delimiter, String),
2108 (Null, String),
2109 (Escape, String),
2110 (Quote, String),
2111 (Header, bool),
2112 (AwsConnection, with_options::Object),
2113 (MaxFileSize, ByteSize, Default(ByteSize::mb(256))),
2114 (Files, Vec<String>),
2115 (Pattern, String)
2116);
2117
2118pub fn plan_copy(
2119 scx: &StatementContext,
2120 CopyStatement {
2121 relation,
2122 direction,
2123 target,
2124 options,
2125 }: CopyStatement<Aug>,
2126) -> Result<Plan, PlanError> {
2127 let options = CopyOptionExtracted::try_from(options)?;
2128 let format = options
2131 .format
2132 .as_ref()
2133 .map(|format| match format.to_lowercase().as_str() {
2134 "text" => Ok(CopyFormat::Text),
2135 "csv" => Ok(CopyFormat::Csv),
2136 "binary" => Ok(CopyFormat::Binary),
2137 "parquet" => Ok(CopyFormat::Parquet),
2138 _ => sql_bail!("unknown FORMAT: {}", format),
2139 })
2140 .transpose()?;
2141
2142 match (&direction, &target) {
2143 (CopyDirection::To, CopyTarget::Stdout) => {
2144 if options.delimiter.is_some() {
2145 sql_bail!("COPY TO does not support DELIMITER option yet");
2146 }
2147 if options.quote.is_some() {
2148 sql_bail!("COPY TO does not support QUOTE option yet");
2149 }
2150 if options.escape.is_some() {
2151 sql_bail!("COPY TO does not support ESCAPE option yet");
2152 }
2153 if options.null.is_some() {
2154 sql_bail!("COPY TO does not support NULL option yet");
2155 }
2156 if options.header == Some(true) {
2160 sql_bail!("COPY TO does not support HEADER option yet");
2161 }
2162 match relation {
2163 CopyRelation::Named { .. } => sql_bail!("named with COPY TO STDOUT unsupported"),
2164 CopyRelation::Select(stmt) => Ok(plan_select(
2165 scx,
2166 stmt,
2167 &Params::empty(),
2168 Some(format.unwrap_or(CopyFormat::Text)),
2169 )?),
2170 CopyRelation::Subscribe(stmt) => Ok(plan_subscribe(
2171 scx,
2172 stmt,
2173 &Params::empty(),
2174 Some(format.unwrap_or(CopyFormat::Text)),
2175 )?),
2176 }
2177 }
2178 (CopyDirection::From, target) => match relation {
2179 CopyRelation::Named { name, columns } => {
2180 plan_copy_from(scx, target, name, columns, format, options)
2181 }
2182 _ => sql_bail!("COPY FROM {} not supported", target),
2183 },
2184 (CopyDirection::To, CopyTarget::Expr(to_expr)) => {
2185 let format = match format {
2186 Some(inner) => inner,
2187 _ => sql_bail!("COPY TO <expr> requires a FORMAT option"),
2188 };
2189
2190 let stmt = match relation {
2191 CopyRelation::Named { name, columns } => {
2192 if !columns.is_empty() {
2193 sql_bail!(
2195 "specifying columns for COPY <table_name> TO commands not yet supported; use COPY (SELECT...) TO ... instead"
2196 );
2197 }
2198 let query = Query {
2200 ctes: CteBlock::empty(),
2201 body: SetExpr::Table(name),
2202 order_by: vec![],
2203 limit: None,
2204 offset: None,
2205 };
2206 SelectStatement { query, as_of: None }
2207 }
2208 CopyRelation::Select(stmt) => {
2209 if !stmt.query.order_by.is_empty() {
2210 sql_bail!("ORDER BY is not supported in SELECT query for COPY statements")
2211 }
2212 stmt
2213 }
2214 CopyRelation::Subscribe(_) => {
2215 sql_bail!("COPY {} {} not supported", direction, target)
2216 }
2217 };
2218
2219 let (plan, desc) = plan_select_inner(scx, stmt, &Params::empty(), None)?;
2220 plan_copy_to_expr(scx, plan, desc, to_expr, format, options)
2221 }
2222 _ => sql_bail!("COPY {} {} not supported", direction, target),
2223 }
2224}