Skip to main content

mz_sql/plan/statement/
dml.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Data manipulation language (DML).
11//!
12//! This module houses the handlers for statements that manipulate data, like
13//! `INSERT`, `SELECT`, `SUBSCRIBE`, and `COPY`.
14
15use 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
71// TODO(benesch): currently, describing a `SELECT` or `INSERT` query
72// plans the whole query to determine its shape and parameter types,
73// and then throws away that plan. If we were smarter, we'd stash that
74// plan somewhere so we don't have to recompute it when the query is
75// executed.
76
77pub 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    // We need to concretize the `limit` and `offset` of the RowSetFinishing, so that we go from
231    // `RowSetFinishing<HirScalarExpr, HirScalarExpr>` to `RowSetFinishing`.
232    // This involves binding parameters and evaluating each expression to a number.
233    // (This should be possible even for `limit` here, because we are at the top level of a SELECT,
234    // so this `limit` has to be a constant.)
235    let limit = match finishing.limit {
236        None => None,
237        Some(mut limit) => {
238            limit.bind_parameters_and_simplify_offset(scx, lifetime, params)?;
239            // TODO: Call `try_into_literal_int64` instead of `as_literal`.
240            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            // We already checked in bind_parameters_and_simplify_offset / offset_into_value.
262            soft_panic_or_log!("unexpectedly negative OFFSET");
263            negative_offset_error(offset)
264        })?
265    };
266
267    // Unmaterializable functions are evaluated based on various information (e.g., the catalog)
268    // during sequencing, without taking AS OF into account. This would be hard to fix, so for now
269    // we just disallow AS OF when there is an unmaterializable function in a query (except mz_now).
270    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                // handle each property only once (belt and suspenders)
388                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        // handle each property only once (belt and suspenders)
473        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
540// Currently, there are two reasons for why a flag should be `Option<bool>` instead of simply
541// `bool`:
542// - When it's an override of a global feature flag, for example optimizer feature flags. In this
543//   case, we need not just false and true, but also None to say "take the value of the global
544//   flag".
545// - When it's an override of whether SOFT_ASSERTIONS are enabled. For example, when `Arity` is not
546//   explicitly given in the EXPLAIN command, then we'd like staging and prod to default to true,
547//   but otherwise we'd like to default to false.
548generate_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 `WITH(raw)` is specified, ensure that the config will be as
593        // representative for the original plan as possible.
594        if v.raw {
595            v.raw_plans = true;
596            v.raw_syntax = true;
597        }
598
599        // Certain config should default to be enabled in release builds running on
600        // staging or prod (where SOFT_ASSERTIONS are turned off).
601        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            // The ones that are initialized with `Default::default()` are not wired up to EXPLAIN.
625            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                // If we don't force this parameter to Skip planning will
705                // fail for names that already exist in the catalog. This
706                // can happen even in `Replace` mode if the existing item
707                // has dependencies.
708                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                // If we don't force this parameter to Skip planning will
725                // fail for names that already exist in the catalog. This
726                // can happen even in `Replace` mode if the existing item
727                // has dependencies.
728                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                // If we don't force this parameter to true planning will
750                // fail for index items that already exist in the catalog.
751                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    // Plan ExplainConfig.
790    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            // If filtering is disabled, explain plans should not include pushdown info.
795            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        // Parser limits to JSON.
819        format: _,
820        mut statement,
821    } = explain_schema;
822
823    // Force the sink's name to one that's guaranteed not to exist, by virtue of
824    // being a non-existent item in a schema under the system's control, so that
825    // `plan_create_sink` doesn't complain about the name already existing.
826    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    // generate SQL query
916
917    /* WITH {CTEs}
918       SELECT REPEAT(' ', nesting * 2) || operator AS operator
919             {columns}
920        FROM      mz_introspection.mz_lir_mapping mlm
921             JOIN {from} USING (lir_id)
922             JOIN mz_introspection.mz_mappable_objects mo
923               ON (mlm.global_id = mo.global_id)
924       WHERE     mo.name = {escaped explainee_name}
925             AND {predicates}
926       ORDER BY lir_id DESC
927    */
928    let mut ctes = Vec::with_capacity(4); // max 2 per ExplainAnalyzeComputationProperty
929    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                // handle each property only once (belt and suspenders)
946                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    // object string
1129    // worker_id uint64        (if           skew)
1130    // memory_ratio numeric    (if memory && skew)
1131    // worker_memory string    (if memory && skew)
1132    // avg_memory string       (if memory && skew)
1133    // total_memory string     (if memory)
1134    // records_ratio numeric   (if memory && skew)
1135    // worker_records          (if memory && skew)
1136    // avg_records numeric     (if memory && skew)
1137    // total_records numeric   (if memory)
1138    // cpu_ratio numeric       (if cpu    && skew)
1139    // worker_elapsed interval (if cpu    && skew)
1140    // avg_elapsed interval    (if cpu    && skew)
1141    // total_elapsed interval  (if cpu)
1142
1143    /* WITH {CTEs}
1144       SELECT mo.name AS object
1145             {columns}
1146        FROM mz_introspection.mz_mappable_objects mo
1147             {from}
1148       WHERE {predicates}
1149       ORDER BY {order_by}, mo.name DESC
1150    */
1151    let mut ctes = Vec::with_capacity(4); // max 2 per ExplainAnalyzeComputationProperty
1152    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        // handle each property only once (belt and suspenders)
1162        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                        // join condition if we're showing skew for more than one property
1172                        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; // we'll add ourselves to `order_by` later
1179                    };
1180
1181                    // computes the average memory per LIR operator (for per operator ratios)
1182                    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                    // computes the memory per worker in a per operator way
1199                    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                    // computes memory ratios per worker per operator
1215                    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                    // summarizes each object, per worker
1230                    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                    // summarizes each worker
1247                    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                    // no skew, so just compute totals
1282                    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                        // join condition if we're showing skew for more than one property
1323                        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; // we'll add ourselves to `order_by` later
1330                    };
1331
1332                    // computes the average memory per LIR operator (for per operator ratios)
1333                    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                    // computes the CPU per worker in a per operator way
1348                    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                    // computes CPU ratios per worker per operator
1363                    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                    // summarizes each object, per worker
1377                    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                    // summarizes each worker
1392                    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                    // no skew, so just compute totals
1421                    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    // generate SQL query text
1454    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    // add mo.name last, to break ties only
1474    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            // Add the key columns in the order that they're specified.
1594            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            // Then add the remaining columns in the order from the original
1610            // table, filtering out the key columns since we added those above.
1611            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)] // TODO(aalexandrov): Use HirRelationExpr in Subscribe
1668            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            // There's no way to apply finishing operations to a `SUBSCRIBE` directly, so the
1682            // finishing should have already been turned into a `TopK` by
1683            // `plan_query` / `plan_root_query`, upon seeing the `QueryLifetime::Subscribe`.
1684            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                    // mz_diff is being used in an expression. Since mz_diff isn't part of the table
1791                    // it looks like an unknown column. Instead, return a better error
1792                    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
1820/// Ensures each `ColumnOrder` in `order_by` references a distinct column,
1821/// returning `DuplicateKeyColumnInSubscribeEnvelope` on the first repeat.
1822fn 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            // Validate that the output desc can be formatted as parquet.
1913            // COPY TO does not apply any type overrides, so pass `|_| None`.
1914            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    // Converting the to expr to a HirScalarExpr
1923    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            // Converting the expr to an HirScalarExpr
1982            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                    // Validate the connection type is one we expect.
2002                    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    // COPY FROM a URL or S3 bucket only supports CSV and Parquet. Unlike COPY
2020    // FROM STDIN there's no sensible default format, so one must be specified
2021    // explicitly. Reject unsupported formats here in planning; the coordinator
2022    // relies on this and would otherwise soft-panic.
2023    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    // Parse any user-provided FORMAT option. If not provided, will default to
2129    // Text for COPY TO STDOUT and COPY FROM STDIN, but will error for COPY TO <expr>.
2130    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            // `HEADER false` is the default and already honored; only an
2157            // enabled header is unimplemented. Silently accepting it would
2158            // make clients strip the first data row as a presumed header.
2159            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                        // TODO(mouli): Add support for this
2194                        sql_bail!(
2195                            "specifying columns for COPY <table_name> TO commands not yet supported; use COPY (SELECT...) TO ... instead"
2196                        );
2197                    }
2198                    // Generate a synthetic SELECT query that just gets the table
2199                    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}