Skip to main content

mz_sql/plan/statement/
show.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//! Queries that show the state of the database system.
11//!
12//! This module houses the handlers for the `SHOW` suite of statements, like
13//! `SHOW CREATE TABLE` and `SHOW VIEWS`. Note that `SHOW <var>` is considered
14//! an SCL statement.
15
16use std::collections::{BTreeMap, BTreeSet};
17use std::fmt::Write;
18
19use mz_ore::collections::CollectionExt;
20use mz_repr::{CatalogItemId, Datum, RelationDesc, Row, SqlScalarType};
21use mz_sql_parser::ast::display::{AstDisplay, FormatMode};
22use mz_sql_parser::ast::{
23    CreateSinkOptionName, CreateSubsourceOptionName, ExternalReferenceExport, ExternalReferences,
24    ObjectType, ShowCreateClusterStatement, ShowCreateConnectionStatement,
25    ShowCreateMaterializedViewStatement, ShowCreateTypeStatement, ShowObjectType,
26    SqlServerConfigOptionName, SystemObjectType, UnresolvedItemName, WithOptionValue,
27};
28use mz_sql_pretty::PrettyConfig;
29use query::QueryContext;
30
31use crate::ast::display::escaped_string_literal;
32use crate::ast::visit_mut::VisitMut;
33use crate::ast::{
34    SelectStatement, ShowColumnsStatement, ShowCreateIndexStatement, ShowCreateSinkStatement,
35    ShowCreateSourceStatement, ShowCreateTableStatement, ShowCreateViewStatement,
36    ShowObjectsStatement, ShowStatementFilter, Statement, Value,
37};
38use crate::catalog::{CatalogItemType, SessionCatalog};
39use crate::names::{
40    self, Aug, NameSimplifier, ObjectId, ResolvedClusterName, ResolvedDataType,
41    ResolvedDatabaseName, ResolvedIds, ResolvedItemName, ResolvedRoleName, ResolvedSchemaName,
42};
43use crate::parse;
44use crate::plan::scope::Scope;
45use crate::plan::statement::ddl::unplan_create_cluster;
46use crate::plan::statement::{StatementContext, StatementDesc, dml};
47use crate::plan::{
48    HirRelationExpr, Params, Plan, PlanError, ShowColumnsPlan, ShowCreatePlan, query, transform_ast,
49};
50
51pub fn describe_show_create_view(
52    _: &StatementContext,
53    _: ShowCreateViewStatement<Aug>,
54) -> Result<StatementDesc, PlanError> {
55    Ok(StatementDesc::new(Some(
56        RelationDesc::builder()
57            .with_column("name", SqlScalarType::String.nullable(false))
58            .with_column("create_sql", SqlScalarType::String.nullable(false))
59            .finish(),
60    )))
61}
62
63pub fn plan_show_create_view(
64    scx: &StatementContext,
65    ShowCreateViewStatement {
66        view_name,
67        redacted,
68    }: ShowCreateViewStatement<Aug>,
69) -> Result<ShowCreatePlan, PlanError> {
70    plan_show_create_item(scx, &view_name, CatalogItemType::View, redacted)
71}
72
73pub fn describe_show_create_materialized_view(
74    _: &StatementContext,
75    _: ShowCreateMaterializedViewStatement<Aug>,
76) -> Result<StatementDesc, PlanError> {
77    Ok(StatementDesc::new(Some(
78        RelationDesc::builder()
79            .with_column("name", SqlScalarType::String.nullable(false))
80            .with_column("create_sql", SqlScalarType::String.nullable(false))
81            .finish(),
82    )))
83}
84
85pub fn plan_show_create_materialized_view(
86    scx: &StatementContext,
87    ShowCreateMaterializedViewStatement {
88        materialized_view_name,
89        redacted,
90    }: ShowCreateMaterializedViewStatement<Aug>,
91) -> Result<ShowCreatePlan, PlanError> {
92    plan_show_create_item(
93        scx,
94        &materialized_view_name,
95        CatalogItemType::MaterializedView,
96        redacted,
97    )
98}
99
100pub fn describe_show_create_table(
101    _: &StatementContext,
102    _: ShowCreateTableStatement<Aug>,
103) -> Result<StatementDesc, PlanError> {
104    Ok(StatementDesc::new(Some(
105        RelationDesc::builder()
106            .with_column("name", SqlScalarType::String.nullable(false))
107            .with_column("create_sql", SqlScalarType::String.nullable(false))
108            .finish(),
109    )))
110}
111
112fn plan_show_create_item(
113    scx: &StatementContext,
114    name: &ResolvedItemName,
115    expect_type: CatalogItemType,
116    redacted: bool,
117) -> Result<ShowCreatePlan, PlanError> {
118    let item = scx.get_item_by_resolved_name(name)?;
119    let name = name.full_name_str();
120    if item.id().is_system()
121        && matches!(
122            expect_type,
123            CatalogItemType::Table | CatalogItemType::Source
124        )
125    {
126        sql_bail!("cannot show create for system object {name}");
127    }
128    if item.item_type() == CatalogItemType::MaterializedView && expect_type == CatalogItemType::View
129    {
130        return Err(PlanError::ShowCreateViewOnMaterializedView(name));
131    }
132    if item.item_type() != expect_type {
133        sql_bail!("{name} is not a {expect_type}");
134    }
135    let create_sql =
136        humanize_sql_for_show_create(scx.catalog, item.id(), item.create_sql(), redacted)?;
137    Ok(ShowCreatePlan {
138        id: ObjectId::Item(item.id()),
139        row: Row::pack_slice(&[Datum::String(&name), Datum::String(&create_sql)]),
140    })
141}
142
143pub fn plan_show_create_table(
144    scx: &StatementContext,
145    ShowCreateTableStatement {
146        table_name,
147        redacted,
148    }: ShowCreateTableStatement<Aug>,
149) -> Result<ShowCreatePlan, PlanError> {
150    plan_show_create_item(scx, &table_name, CatalogItemType::Table, redacted)
151}
152
153pub fn describe_show_create_source(
154    _: &StatementContext,
155    _: ShowCreateSourceStatement<Aug>,
156) -> Result<StatementDesc, PlanError> {
157    Ok(StatementDesc::new(Some(
158        RelationDesc::builder()
159            .with_column("name", SqlScalarType::String.nullable(false))
160            .with_column("create_sql", SqlScalarType::String.nullable(false))
161            .finish(),
162    )))
163}
164
165pub fn plan_show_create_source(
166    scx: &StatementContext,
167    ShowCreateSourceStatement {
168        source_name,
169        redacted,
170    }: ShowCreateSourceStatement<Aug>,
171) -> Result<ShowCreatePlan, PlanError> {
172    plan_show_create_item(scx, &source_name, CatalogItemType::Source, redacted)
173}
174
175pub fn describe_show_create_sink(
176    _: &StatementContext,
177    _: ShowCreateSinkStatement<Aug>,
178) -> Result<StatementDesc, PlanError> {
179    Ok(StatementDesc::new(Some(
180        RelationDesc::builder()
181            .with_column("name", SqlScalarType::String.nullable(false))
182            .with_column("create_sql", SqlScalarType::String.nullable(false))
183            .finish(),
184    )))
185}
186
187pub fn plan_show_create_sink(
188    scx: &StatementContext,
189    ShowCreateSinkStatement {
190        sink_name,
191        redacted,
192    }: ShowCreateSinkStatement<Aug>,
193) -> Result<ShowCreatePlan, PlanError> {
194    plan_show_create_item(scx, &sink_name, CatalogItemType::Sink, redacted)
195}
196
197pub fn describe_show_create_index(
198    _: &StatementContext,
199    _: ShowCreateIndexStatement<Aug>,
200) -> Result<StatementDesc, PlanError> {
201    Ok(StatementDesc::new(Some(
202        RelationDesc::builder()
203            .with_column("name", SqlScalarType::String.nullable(false))
204            .with_column("create_sql", SqlScalarType::String.nullable(false))
205            .finish(),
206    )))
207}
208
209pub fn plan_show_create_index(
210    scx: &StatementContext,
211    ShowCreateIndexStatement {
212        index_name,
213        redacted,
214    }: ShowCreateIndexStatement<Aug>,
215) -> Result<ShowCreatePlan, PlanError> {
216    plan_show_create_item(scx, &index_name, CatalogItemType::Index, redacted)
217}
218
219pub fn describe_show_create_connection(
220    _: &StatementContext,
221    _: ShowCreateConnectionStatement<Aug>,
222) -> Result<StatementDesc, PlanError> {
223    Ok(StatementDesc::new(Some(
224        RelationDesc::builder()
225            .with_column("name", SqlScalarType::String.nullable(false))
226            .with_column("create_sql", SqlScalarType::String.nullable(false))
227            .finish(),
228    )))
229}
230
231pub fn plan_show_create_cluster(
232    scx: &StatementContext,
233    ShowCreateClusterStatement { cluster_name }: ShowCreateClusterStatement<Aug>,
234) -> Result<ShowCreatePlan, PlanError> {
235    let cluster = scx.get_cluster(&cluster_name.id);
236    let name = cluster.name().to_string();
237    let plan = cluster.try_to_plan()?;
238    let stmt = unplan_create_cluster(scx, plan)?;
239    let create_sql = stmt.to_ast_string_stable();
240    Ok(ShowCreatePlan {
241        id: ObjectId::Cluster(cluster_name.id),
242        row: Row::pack_slice(&[Datum::String(&name), Datum::String(&create_sql)]),
243    })
244}
245
246pub fn describe_show_create_cluster(
247    _: &StatementContext,
248    _: ShowCreateClusterStatement<Aug>,
249) -> Result<StatementDesc, PlanError> {
250    Ok(StatementDesc::new(Some(
251        RelationDesc::builder()
252            .with_column("name", SqlScalarType::String.nullable(false))
253            .with_column("create_sql", SqlScalarType::String.nullable(false))
254            .finish(),
255    )))
256}
257
258pub fn plan_show_create_type(
259    scx: &StatementContext,
260    ShowCreateTypeStatement {
261        type_name,
262        redacted,
263    }: ShowCreateTypeStatement<Aug>,
264) -> Result<ShowCreatePlan, PlanError> {
265    let ResolvedDataType::Named { id, full_name, .. } = type_name else {
266        sql_bail!("{type_name} is not a named type");
267    };
268
269    let type_item = scx.get_item(&id);
270
271    if id.is_system() {
272        sql_bail!("cannot show create for system type {full_name}");
273    }
274
275    let name = full_name.to_string();
276
277    let create_sql = humanize_sql_for_show_create(
278        scx.catalog,
279        type_item.id(),
280        type_item.create_sql(),
281        redacted,
282    )?;
283
284    Ok(ShowCreatePlan {
285        id: ObjectId::Item(id),
286        row: Row::pack_slice(&[Datum::String(&name), Datum::String(&create_sql)]),
287    })
288}
289
290pub fn describe_show_create_type(
291    _: &StatementContext,
292    _: ShowCreateTypeStatement<Aug>,
293) -> Result<StatementDesc, PlanError> {
294    Ok(StatementDesc::new(Some(
295        RelationDesc::builder()
296            .with_column("name", SqlScalarType::String.nullable(false))
297            .with_column("create_sql", SqlScalarType::String.nullable(false))
298            .finish(),
299    )))
300}
301
302pub fn plan_show_create_connection(
303    scx: &StatementContext,
304    ShowCreateConnectionStatement {
305        connection_name,
306        redacted,
307    }: ShowCreateConnectionStatement<Aug>,
308) -> Result<ShowCreatePlan, PlanError> {
309    plan_show_create_item(scx, &connection_name, CatalogItemType::Connection, redacted)
310}
311
312pub fn show_databases<'a>(
313    scx: &'a StatementContext<'a>,
314    filter: Option<ShowStatementFilter<Aug>>,
315) -> Result<ShowSelect<'a>, PlanError> {
316    let query = "SELECT name, comment FROM mz_internal.mz_show_databases".to_string();
317    ShowSelect::new(scx, query, filter, None, Some(&["name", "comment"]))
318}
319
320pub fn show_schemas<'a>(
321    scx: &'a StatementContext<'a>,
322    from: Option<ResolvedDatabaseName>,
323    filter: Option<ShowStatementFilter<Aug>>,
324) -> Result<ShowSelect<'a>, PlanError> {
325    let database_id = match from {
326        Some(ResolvedDatabaseName::Database { id, .. }) => id.to_string(),
327        None => match scx.active_database() {
328            Some(id) => id.to_string(),
329            None => sql_bail!("no database specified and no active database"),
330        },
331        Some(ResolvedDatabaseName::Error) => {
332            bail_internal!("unresolved database name")
333        }
334    };
335    let query = format!(
336        "SELECT name, comment
337        FROM mz_internal.mz_show_schemas
338        WHERE database_id IS NULL OR database_id = '{database_id}'",
339    );
340    ShowSelect::new(scx, query, filter, None, Some(&["name", "comment"]))
341}
342
343pub fn show_roles<'a>(
344    scx: &'a StatementContext<'a>,
345    filter: Option<ShowStatementFilter<Aug>>,
346) -> Result<ShowSelect<'a>, PlanError> {
347    let query = "SELECT name, comment FROM mz_internal.mz_show_roles".to_string();
348    ShowSelect::new(scx, query, filter, None, Some(&["name", "comment"]))
349}
350
351pub fn show_network_policies<'a>(
352    scx: &'a StatementContext<'a>,
353    filter: Option<ShowStatementFilter<Aug>>,
354) -> Result<ShowSelect<'a>, PlanError> {
355    let query = "SELECT name, rules, comment FROM mz_internal.mz_show_network_policies".to_string();
356    ShowSelect::new(
357        scx,
358        query,
359        filter,
360        None,
361        Some(&["name", "rules", "comment"]),
362    )
363}
364
365/// Ensures that the `FROM` clause was not provided for `SHOW` commands that
366/// don't accept it. The parser is supposed to reject such cases, so this is an
367/// internal-only invariant.
368fn ensure_no_from<T>(from: Option<T>) -> Result<(), PlanError> {
369    if from.is_some() {
370        bail_internal!("FROM not supported for this SHOW command");
371    }
372    Ok(())
373}
374
375pub fn show_objects<'a>(
376    scx: &'a StatementContext<'a>,
377    ShowObjectsStatement {
378        object_type,
379        from,
380        filter,
381    }: ShowObjectsStatement<Aug>,
382) -> Result<ShowSelect<'a>, PlanError> {
383    match object_type {
384        ShowObjectType::Table { on_source } => show_tables(scx, from, on_source, filter),
385        ShowObjectType::Source { in_cluster } => show_sources(scx, from, in_cluster, filter),
386        ShowObjectType::Subsource { on_source } => show_subsources(scx, from, on_source, filter),
387        ShowObjectType::View => show_views(scx, from, filter),
388        ShowObjectType::Sink { in_cluster } => show_sinks(scx, from, in_cluster, filter),
389        ShowObjectType::Type => show_types(scx, from, filter),
390        ShowObjectType::Object => show_all_objects(scx, from, filter),
391        ShowObjectType::Role => {
392            ensure_no_from(from)?;
393            show_roles(scx, filter)
394        }
395        ShowObjectType::Cluster => {
396            ensure_no_from(from)?;
397            show_clusters(scx, filter)
398        }
399        ShowObjectType::ClusterReplica => {
400            ensure_no_from(from)?;
401            show_cluster_replicas(scx, filter)
402        }
403        ShowObjectType::Secret => show_secrets(scx, from, filter),
404        ShowObjectType::Connection => show_connections(scx, from, filter),
405        ShowObjectType::MaterializedView { in_cluster } => {
406            show_materialized_views(scx, from, in_cluster, filter)
407        }
408        ShowObjectType::Index {
409            in_cluster,
410            on_object,
411        } => show_indexes(scx, from, on_object, in_cluster, filter),
412        ShowObjectType::Database => {
413            ensure_no_from(from)?;
414            show_databases(scx, filter)
415        }
416        ShowObjectType::Schema { from: db_from } => {
417            ensure_no_from(from)?;
418            show_schemas(scx, db_from, filter)
419        }
420        ShowObjectType::Privileges { object_type, role } => {
421            ensure_no_from(from)?;
422            show_privileges(scx, object_type, role, filter)
423        }
424        ShowObjectType::DefaultPrivileges { object_type, role } => {
425            ensure_no_from(from)?;
426            show_default_privileges(scx, object_type, role, filter)
427        }
428        ShowObjectType::RoleMembership { role } => {
429            ensure_no_from(from)?;
430            show_role_membership(scx, role, filter)
431        }
432        ShowObjectType::NetworkPolicy => {
433            ensure_no_from(from)?;
434            show_network_policies(scx, filter)
435        }
436    }
437}
438
439fn show_connections<'a>(
440    scx: &'a StatementContext<'a>,
441    from: Option<ResolvedSchemaName>,
442    filter: Option<ShowStatementFilter<Aug>>,
443) -> Result<ShowSelect<'a>, PlanError> {
444    let schema_spec = scx.resolve_optional_schema(&from)?;
445    let query = format!(
446        "SELECT name, type, comment
447        FROM mz_internal.mz_show_connections connections
448        WHERE schema_id = '{schema_spec}'",
449    );
450    ShowSelect::new(scx, query, filter, None, Some(&["name", "type", "comment"]))
451}
452
453fn show_tables<'a>(
454    scx: &'a StatementContext<'a>,
455    from: Option<ResolvedSchemaName>,
456    on_source: Option<ResolvedItemName>,
457    filter: Option<ShowStatementFilter<Aug>>,
458) -> Result<ShowSelect<'a>, PlanError> {
459    let schema_spec = scx.resolve_optional_schema(&from)?;
460    let mut query = format!(
461        "SELECT name, comment
462        FROM mz_internal.mz_show_tables tables
463        WHERE tables.schema_id = '{schema_spec}'",
464    );
465    if let Some(on_source) = &on_source {
466        let on_item = scx.get_item_by_resolved_name(on_source)?;
467        if on_item.item_type() != CatalogItemType::Source {
468            sql_bail!(
469                "cannot show tables on {} because it is a {}",
470                on_source.full_name_str(),
471                on_item.item_type(),
472            );
473        }
474        query += &format!(" AND tables.source_id = '{}'", on_item.id());
475    }
476    ShowSelect::new(scx, query, filter, None, Some(&["name", "comment"]))
477}
478
479fn show_sources<'a>(
480    scx: &'a StatementContext<'a>,
481    from: Option<ResolvedSchemaName>,
482    in_cluster: Option<ResolvedClusterName>,
483    filter: Option<ShowStatementFilter<Aug>>,
484) -> Result<ShowSelect<'a>, PlanError> {
485    let schema_spec = scx.resolve_optional_schema(&from)?;
486    let mut where_clause = format!("schema_id = '{schema_spec}'");
487
488    if let Some(cluster) = in_cluster {
489        write!(where_clause, " AND cluster_id = '{}'", cluster.id)
490            .expect("write on string cannot fail");
491    }
492
493    let query = format!(
494        "SELECT name, type, cluster, comment
495        FROM mz_internal.mz_show_sources
496        WHERE {where_clause}"
497    );
498    ShowSelect::new(
499        scx,
500        query,
501        filter,
502        None,
503        Some(&["name", "type", "cluster", "comment"]),
504    )
505}
506
507fn show_subsources<'a>(
508    scx: &'a StatementContext<'a>,
509    from_schema: Option<ResolvedSchemaName>,
510    on_source: Option<ResolvedItemName>,
511    filter: Option<ShowStatementFilter<Aug>>,
512) -> Result<ShowSelect<'a>, PlanError> {
513    let mut query_filter = Vec::new();
514
515    if on_source.is_none() && from_schema.is_none() {
516        query_filter.push("subsources.id NOT LIKE 's%'".into());
517        let schema_spec = scx.resolve_active_schema().map(|spec| spec.clone())?;
518        query_filter.push(format!("subsources.schema_id = '{schema_spec}'"));
519    }
520
521    if let Some(on_source) = &on_source {
522        let on_item = scx.get_item_by_resolved_name(on_source)?;
523        if on_item.item_type() != CatalogItemType::Source {
524            sql_bail!(
525                "cannot show subsources on {} because it is a {}",
526                on_source.full_name_str(),
527                on_item.item_type(),
528            );
529        }
530        query_filter.push(format!("sources.id = '{}'", on_item.id()));
531    }
532
533    if let Some(schema) = from_schema {
534        let schema_spec = schema.schema_spec();
535        query_filter.push(format!("subsources.schema_id = '{schema_spec}'"));
536    }
537
538    // TODO(database-issues#8322): this looks in both directions for subsources as long as
539    // progress collections still exist
540    let query = format!(
541        "SELECT DISTINCT
542            subsources.name AS name,
543            subsources.type AS type
544        FROM
545            mz_sources AS subsources
546            JOIN mz_internal.mz_object_dependencies deps ON (subsources.id = deps.object_id OR subsources.id = deps.referenced_object_id)
547            JOIN mz_sources AS sources ON (sources.id = deps.object_id OR sources.id = deps.referenced_object_id)
548        WHERE (subsources.type = 'subsource' OR subsources.type = 'progress') AND {}",
549        itertools::join(query_filter, " AND "),
550    );
551    ShowSelect::new(scx, query, filter, None, None)
552}
553
554fn show_views<'a>(
555    scx: &'a StatementContext<'a>,
556    from: Option<ResolvedSchemaName>,
557    filter: Option<ShowStatementFilter<Aug>>,
558) -> Result<ShowSelect<'a>, PlanError> {
559    let schema_spec = scx.resolve_optional_schema(&from)?;
560    let query = format!(
561        "SELECT name, comment
562        FROM mz_internal.mz_show_views
563        WHERE schema_id = '{schema_spec}'"
564    );
565    ShowSelect::new(scx, query, filter, None, Some(&["name", "comment"]))
566}
567
568fn show_materialized_views<'a>(
569    scx: &'a StatementContext<'a>,
570    from: Option<ResolvedSchemaName>,
571    in_cluster: Option<ResolvedClusterName>,
572    filter: Option<ShowStatementFilter<Aug>>,
573) -> Result<ShowSelect<'a>, PlanError> {
574    let schema_spec = scx.resolve_optional_schema(&from)?;
575    let mut where_clause = format!("schema_id = '{schema_spec}'");
576
577    if let Some(cluster) = in_cluster {
578        write!(where_clause, " AND cluster_id = '{}'", cluster.id)
579            .expect("write on string cannot fail");
580    }
581
582    let query = format!(
583        "SELECT name, cluster, comment
584            FROM mz_internal.mz_show_materialized_views
585            WHERE {where_clause}"
586    );
587
588    let projection = vec!["name", "cluster", "comment"];
589
590    ShowSelect::new(scx, query, filter, None, Some(&projection))
591}
592
593fn show_sinks<'a>(
594    scx: &'a StatementContext<'a>,
595    from: Option<ResolvedSchemaName>,
596    in_cluster: Option<ResolvedClusterName>,
597    filter: Option<ShowStatementFilter<Aug>>,
598) -> Result<ShowSelect<'a>, PlanError> {
599    let schema_spec = if let Some(ResolvedSchemaName::Schema { schema_spec, .. }) = from {
600        schema_spec.to_string()
601    } else {
602        scx.resolve_active_schema()?.to_string()
603    };
604
605    let mut where_clause = format!("schema_id = '{schema_spec}'");
606
607    if let Some(cluster) = in_cluster {
608        write!(where_clause, " AND cluster_id = '{}'", cluster.id)
609            .expect("write on string cannot fail");
610    }
611
612    let query = format!(
613        "SELECT name, type, cluster, comment
614        FROM mz_internal.mz_show_sinks sinks
615        WHERE {where_clause}"
616    );
617    ShowSelect::new(
618        scx,
619        query,
620        filter,
621        None,
622        Some(&["name", "type", "cluster", "comment"]),
623    )
624}
625
626fn show_types<'a>(
627    scx: &'a StatementContext<'a>,
628    from: Option<ResolvedSchemaName>,
629    filter: Option<ShowStatementFilter<Aug>>,
630) -> Result<ShowSelect<'a>, PlanError> {
631    let schema_spec = scx.resolve_optional_schema(&from)?;
632    let query = format!(
633        "SELECT name, comment
634        FROM mz_internal.mz_show_types
635        WHERE schema_id = '{schema_spec}'"
636    );
637    ShowSelect::new(scx, query, filter, None, Some(&["name", "comment"]))
638}
639
640fn show_all_objects<'a>(
641    scx: &'a StatementContext<'a>,
642    from: Option<ResolvedSchemaName>,
643    filter: Option<ShowStatementFilter<Aug>>,
644) -> Result<ShowSelect<'a>, PlanError> {
645    let schema_spec = scx.resolve_optional_schema(&from)?;
646    let query = format!(
647        "SELECT name, type, comment
648         FROM mz_internal.mz_show_all_objects
649         WHERE schema_id = '{schema_spec}'",
650    );
651    ShowSelect::new(scx, query, filter, None, Some(&["name", "type", "comment"]))
652}
653
654pub fn show_indexes<'a>(
655    scx: &'a StatementContext<'a>,
656    from_schema: Option<ResolvedSchemaName>,
657    on_object: Option<ResolvedItemName>,
658    in_cluster: Option<ResolvedClusterName>,
659    filter: Option<ShowStatementFilter<Aug>>,
660) -> Result<ShowSelect<'a>, PlanError> {
661    let mut query_filter = Vec::new();
662
663    if on_object.is_none() && from_schema.is_none() && in_cluster.is_none() {
664        query_filter.push("on_id NOT LIKE 's%'".into());
665        let schema_spec = scx.resolve_active_schema().map(|spec| spec.clone())?;
666        query_filter.push(format!("schema_id = '{schema_spec}'"));
667    }
668
669    if let Some(on_object) = &on_object {
670        let on_item = scx.get_item_by_resolved_name(on_object)?;
671        if on_item.item_type() != CatalogItemType::View
672            && on_item.item_type() != CatalogItemType::MaterializedView
673            && on_item.item_type() != CatalogItemType::Source
674            && on_item.item_type() != CatalogItemType::Table
675        {
676            sql_bail!(
677                "cannot show indexes on {} because it is a {}",
678                on_object.full_name_str(),
679                on_item.item_type(),
680            );
681        }
682        query_filter.push(format!("on_id = '{}'", on_item.id()));
683    }
684
685    if let Some(schema) = from_schema {
686        let schema_spec = schema.schema_spec();
687        query_filter.push(format!("schema_id = '{schema_spec}'"));
688    }
689
690    if let Some(cluster) = in_cluster {
691        query_filter.push(format!("cluster_id = '{}'", cluster.id))
692    };
693
694    let query = format!(
695        "SELECT name, on, cluster, key, comment
696        FROM mz_internal.mz_show_indexes
697        WHERE {}",
698        itertools::join(query_filter.iter(), " AND ")
699    );
700
701    ShowSelect::new(
702        scx,
703        query,
704        filter,
705        None,
706        Some(&["name", "on", "cluster", "key", "comment"]),
707    )
708}
709
710pub fn show_columns<'a>(
711    scx: &'a StatementContext<'a>,
712    ShowColumnsStatement { table_name, filter }: ShowColumnsStatement<Aug>,
713) -> Result<ShowColumnsSelect<'a>, PlanError> {
714    let entry = scx.get_item_by_resolved_name(&table_name)?;
715    let full_name = scx.catalog.resolve_full_name(entry.name());
716
717    match entry.item_type() {
718        CatalogItemType::Source
719        | CatalogItemType::Table
720        | CatalogItemType::View
721        | CatalogItemType::MaterializedView => (),
722        ty @ CatalogItemType::Connection
723        | ty @ CatalogItemType::Index
724        | ty @ CatalogItemType::Func
725        | ty @ CatalogItemType::Secret
726        | ty @ CatalogItemType::Type
727        | ty @ CatalogItemType::Sink
728        | ty @ CatalogItemType::MetricSink => {
729            sql_bail!("{full_name} is a {ty} and so does not have columns");
730        }
731    }
732
733    let query = format!(
734        "SELECT name, nullable, type, position, comment
735         FROM mz_internal.mz_show_columns columns
736         WHERE columns.id = '{}'",
737        entry.id(),
738    );
739    let (show_select, new_resolved_ids) = ShowSelect::new_with_resolved_ids(
740        scx,
741        query,
742        filter,
743        Some("position"),
744        Some(&["name", "nullable", "type", "comment"]),
745    )?;
746    scx.record_sql_impl_ids(&new_resolved_ids);
747    Ok(ShowColumnsSelect {
748        id: entry.id(),
749        show_select,
750        new_resolved_ids,
751    })
752}
753
754// The rationale for which fields to include in the tuples are those
755// that are mandatory when creating a replica as part of the CREATE
756// CLUSTER command, i.e., name and size.
757pub fn show_clusters<'a>(
758    scx: &'a StatementContext<'a>,
759    filter: Option<ShowStatementFilter<Aug>>,
760) -> Result<ShowSelect<'a>, PlanError> {
761    let query =
762        "SELECT name, replicas, activity, comment FROM mz_internal.mz_show_clusters".to_string();
763    ShowSelect::new(
764        scx,
765        query,
766        filter,
767        None,
768        Some(&["name", "replicas", "activity", "comment"]),
769    )
770}
771
772pub fn show_cluster_replicas<'a>(
773    scx: &'a StatementContext<'a>,
774    filter: Option<ShowStatementFilter<Aug>>,
775) -> Result<ShowSelect<'a>, PlanError> {
776    let query = "
777    SELECT cluster, replica, size, ready, comment
778    FROM mz_internal.mz_show_cluster_replicas
779    "
780    .to_string();
781
782    ShowSelect::new(
783        scx,
784        query,
785        filter,
786        None,
787        Some(&["cluster", "replica", "size", "ready", "comment"]),
788    )
789}
790
791pub fn show_secrets<'a>(
792    scx: &'a StatementContext<'a>,
793    from: Option<ResolvedSchemaName>,
794    filter: Option<ShowStatementFilter<Aug>>,
795) -> Result<ShowSelect<'a>, PlanError> {
796    let schema_spec = scx.resolve_optional_schema(&from)?;
797
798    let query = format!(
799        "SELECT name, comment
800        FROM mz_internal.mz_show_secrets
801        WHERE schema_id = '{schema_spec}'",
802    );
803
804    ShowSelect::new(scx, query, filter, None, Some(&["name", "comment"]))
805}
806
807pub fn show_privileges<'a>(
808    scx: &'a StatementContext<'a>,
809    object_type: Option<SystemObjectType>,
810    role: Option<ResolvedRoleName>,
811    filter: Option<ShowStatementFilter<Aug>>,
812) -> Result<ShowSelect<'a>, PlanError> {
813    let mut query_filter = Vec::new();
814    if let Some(object_type) = object_type {
815        query_filter.push(format!(
816            "object_type = '{}'",
817            object_type.to_string().to_lowercase()
818        ));
819    }
820    if let Some(role) = role {
821        let name = escaped_string_literal(&role.name);
822        query_filter.push(format!(
823            "CASE WHEN grantee = 'PUBLIC' THEN true ELSE pg_has_role({name}, grantee, 'USAGE') END"
824        ));
825    }
826    let query_filter = if query_filter.len() > 0 {
827        format!("WHERE {}", itertools::join(query_filter, " AND "))
828    } else {
829        "".to_string()
830    };
831
832    let query = format!(
833        "SELECT grantor, grantee, database, schema, name, object_type, privilege_type
834        FROM mz_internal.mz_show_all_privileges
835        {query_filter}",
836    );
837
838    ShowSelect::new(
839        scx,
840        query,
841        filter,
842        None,
843        Some(&[
844            "grantor",
845            "grantee",
846            "database",
847            "schema",
848            "name",
849            "object_type",
850            "privilege_type",
851        ]),
852    )
853}
854
855pub fn show_default_privileges<'a>(
856    scx: &'a StatementContext<'a>,
857    object_type: Option<ObjectType>,
858    role: Option<ResolvedRoleName>,
859    filter: Option<ShowStatementFilter<Aug>>,
860) -> Result<ShowSelect<'a>, PlanError> {
861    let mut query_filter = Vec::new();
862    if let Some(object_type) = object_type {
863        query_filter.push(format!(
864            "object_type = '{}'",
865            object_type.to_string().to_lowercase()
866        ));
867    }
868    if let Some(role) = role {
869        let name = escaped_string_literal(&role.name);
870        query_filter.push(format!(
871            "CASE WHEN grantee = 'PUBLIC' THEN true ELSE pg_has_role({name}, grantee, 'USAGE') END"
872        ));
873    }
874    let query_filter = if query_filter.len() > 0 {
875        format!("WHERE {}", itertools::join(query_filter, " AND "))
876    } else {
877        "".to_string()
878    };
879
880    let query = format!(
881        "SELECT object_owner, database, schema, object_type, grantee, privilege_type
882        FROM mz_internal.mz_show_default_privileges
883        {query_filter}",
884    );
885
886    ShowSelect::new(
887        scx,
888        query,
889        filter,
890        None,
891        Some(&[
892            "object_owner",
893            "database",
894            "schema",
895            "object_type",
896            "grantee",
897            "privilege_type",
898        ]),
899    )
900}
901
902pub fn show_role_membership<'a>(
903    scx: &'a StatementContext<'a>,
904    role: Option<ResolvedRoleName>,
905    filter: Option<ShowStatementFilter<Aug>>,
906) -> Result<ShowSelect<'a>, PlanError> {
907    let mut query_filter = Vec::new();
908    if let Some(role) = role {
909        let name = escaped_string_literal(&role.name);
910        query_filter.push(format!("pg_has_role({name}, member, 'USAGE')"));
911    }
912    let query_filter = if query_filter.len() > 0 {
913        format!("WHERE {}", itertools::join(query_filter, " AND "))
914    } else {
915        "".to_string()
916    };
917
918    let query = format!(
919        "SELECT role, member, grantor
920        FROM mz_internal.mz_show_role_members
921        {query_filter}",
922    );
923
924    ShowSelect::new(
925        scx,
926        query,
927        filter,
928        None,
929        Some(&["role", "member", "grantor"]),
930    )
931}
932
933/// An intermediate result when planning a `SHOW` query.
934///
935/// Can be interrogated for its columns, or converted into a proper [`Plan`].
936pub struct ShowSelect<'a> {
937    scx: &'a StatementContext<'a>,
938    pub(crate) stmt: SelectStatement<Aug>,
939}
940
941impl<'a> ShowSelect<'a> {
942    /// Constructs a new [`ShowSelect`] from a query that provides the base
943    /// data and an optional user-supplied filter, order column, and
944    /// projection on that data.
945    ///
946    /// Note that the query must return a column named `name`, as the filter
947    /// may implicitly reference this column. Any `ORDER BY` in the query is
948    /// ignored. `ShowSelects`s are always ordered in ascending order by all
949    /// columns from left to right unless an order field is supplied.
950    fn new(
951        scx: &'a StatementContext,
952        query: String,
953        filter: Option<ShowStatementFilter<Aug>>,
954        order: Option<&str>,
955        projection: Option<&[&str]>,
956    ) -> Result<ShowSelect<'a>, PlanError> {
957        let (show_select, new_resolved_ids) =
958            Self::new_with_resolved_ids(scx, query, filter, order, projection)?;
959        scx.sql_impl_resolved_ids
960            .lock()
961            .expect("planning is single-threaded")
962            .extend_from(&new_resolved_ids);
963        Ok(show_select)
964    }
965
966    fn new_with_resolved_ids(
967        scx: &'a StatementContext,
968        query: String,
969        filter: Option<ShowStatementFilter<Aug>>,
970        order: Option<&str>,
971        projection: Option<&[&str]>,
972    ) -> Result<(ShowSelect<'a>, ResolvedIds), PlanError> {
973        let filter = match filter {
974            Some(ShowStatementFilter::Like(like)) => format!("name LIKE {}", Value::String(like)),
975            Some(ShowStatementFilter::Where(expr)) => expr.to_string(),
976            None => "true".to_string(),
977        };
978        let query = format!(
979            "SELECT {} FROM ({}) q WHERE {} ORDER BY {}",
980            projection
981                .map(|ps| ps.join(", "))
982                .unwrap_or_else(|| "*".into()),
983            query,
984            filter,
985            order.unwrap_or("q.*")
986        );
987
988        Self::new_from_bare_query(scx, query)
989    }
990
991    pub fn new_from_bare_query(
992        scx: &'a StatementContext,
993        query: String,
994    ) -> Result<(ShowSelect<'a>, ResolvedIds), PlanError> {
995        let stmts = parse::parse(&query)
996            .map_err(|e| internal_err!("failed to parse generated SHOW query: {}", e))?;
997        let stmt = match stmts.into_element().ast {
998            Statement::Select(select) => select,
999            _ => bail_internal!("generated SHOW query was not a SELECT statement"),
1000        };
1001        let (mut stmt, new_resolved_ids) = names::resolve(scx.catalog, stmt)?;
1002        transform_ast::transform(scx, &mut stmt)?;
1003        Ok((ShowSelect { scx, stmt }, new_resolved_ids))
1004    }
1005
1006    /// Computes the shape of this `ShowSelect`.
1007    pub fn describe(self) -> Result<StatementDesc, PlanError> {
1008        dml::describe_select(self.scx, self.stmt)
1009    }
1010
1011    /// Converts this `ShowSelect` into a [`Plan`].
1012    pub fn plan(self) -> Result<Plan, PlanError> {
1013        dml::plan_select(self.scx, self.stmt, &Params::empty(), None)
1014    }
1015
1016    /// Converts this `ShowSelect` into a [`(HirRelationExpr, Scope)`].
1017    pub fn plan_hir(self, qcx: &QueryContext) -> Result<(HirRelationExpr, Scope), PlanError> {
1018        query::plan_nested_query(&mut qcx.clone(), &self.stmt.query)
1019    }
1020}
1021
1022pub struct ShowColumnsSelect<'a> {
1023    id: CatalogItemId,
1024    new_resolved_ids: ResolvedIds,
1025    show_select: ShowSelect<'a>,
1026}
1027
1028impl<'a> ShowColumnsSelect<'a> {
1029    pub fn describe(self) -> Result<StatementDesc, PlanError> {
1030        self.show_select.describe()
1031    }
1032
1033    pub fn plan(self) -> Result<Plan, PlanError> {
1034        let select_plan = self.show_select.plan()?;
1035        match select_plan {
1036            Plan::Select(select_plan) => Ok(Plan::ShowColumns(ShowColumnsPlan {
1037                id: self.id,
1038                select_plan,
1039                new_resolved_ids: self.new_resolved_ids,
1040            })),
1041            _ => {
1042                tracing::error!(
1043                    "SHOW COLUMNS produced a non select plan. plan: {:?}",
1044                    select_plan
1045                );
1046                Err(PlanError::Unstructured(
1047                    "SHOW COLUMNS produced an unexpected plan. Please file a bug.".to_string(),
1048                ))
1049            }
1050        }
1051    }
1052
1053    pub fn plan_hir(self, qcx: &QueryContext) -> Result<(HirRelationExpr, Scope), PlanError> {
1054        self.show_select.plan_hir(qcx)
1055    }
1056}
1057
1058/// Convert a SQL statement into a form that could be used as input, as well as
1059/// is more amenable to human consumption.
1060///
1061/// Note that the bits we omit here (e.g. the internal `AS OF` of a materialized
1062/// view, or the `DETAILS` of a `CREATE TABLE ... FROM SOURCE`) are not
1063/// user-typeable, but remain accessible for debugging in the raw `create_sql`
1064/// (and `redacted_create_sql`) column of catalog builtins like
1065/// `mz_catalog.mz_materialized_views` and `mz_catalog.mz_tables`. They are not
1066/// in the `definition` column, which is parsed back out of `create_sql` and only
1067/// retains the inner query.
1068fn humanize_sql_for_show_create(
1069    catalog: &dyn SessionCatalog,
1070    id: CatalogItemId,
1071    sql: &str,
1072    redacted: bool,
1073) -> Result<String, PlanError> {
1074    use mz_sql_parser::ast::{
1075        CreateSourceConnection, MySqlConfigOptionName, PgConfigOptionName, TableFromSourceColumns,
1076        TableFromSourceOptionName,
1077    };
1078
1079    let parsed = parse::parse(sql)?.into_element().ast;
1080    let (mut resolved, _) = names::resolve(catalog, parsed)?;
1081
1082    // Simplify names.
1083    let mut simplifier = NameSimplifier { catalog };
1084    simplifier.visit_statement_mut(&mut resolved);
1085
1086    match &mut resolved {
1087        // Strip internal `AS OF` syntax.
1088        Statement::CreateMaterializedView(stmt) => stmt.as_of = None,
1089        // Strip the internal `DETAILS` option, which is not user-typeable and
1090        // does not roundtrip.
1091        Statement::CreateTableFromSource(stmt) => {
1092            stmt.with_options.retain_mut(|o| match o.name {
1093                TableFromSourceOptionName::TextColumns => true,
1094                TableFromSourceOptionName::ExcludeColumns => true,
1095                // Drop details, which does not roundtrip.
1096                TableFromSourceOptionName::Details => false,
1097                TableFromSourceOptionName::PartitionBy => true,
1098                TableFromSourceOptionName::RetainHistory => true,
1099            });
1100            // The `Defined` column list and constraints are populated during
1101            // purification (from the upstream schema), and `CREATE TABLE ... FROM
1102            // SOURCE` rejects them as input. Omit them so the statement
1103            // roundtrips; purification re-derives them from the source on replay,
1104            // and the schema-affecting `TEXT COLUMNS` / `EXCLUDE COLUMNS` options
1105            // are retained above so the re-derived schema matches. A user-typed
1106            // `Named` column list is left intact, since it does roundtrip.
1107            if matches!(stmt.columns, TableFromSourceColumns::Defined(_)) {
1108                stmt.columns = TableFromSourceColumns::NotSpecified;
1109            }
1110            // Constraints are never valid input here (purification populates them
1111            // alongside `Defined` columns), so always drop them.
1112            stmt.constraints = Vec::new();
1113        }
1114        // `CREATE SOURCE` statements should roundtrip. However, sources and
1115        // their subsources have a complex relationship, so we need to do a lot
1116        // of work to reconstruct the statement for multi-output sources.
1117        //
1118        // For instance, `DROP SOURCE` statements can leave dangling references
1119        // to subsources that must be filtered out here, that, due to catalog
1120        // transaction limitations, can only be cleaned up when a top-level
1121        // source is altered.
1122        Statement::CreateSource(stmt) => {
1123            // Collect all current subsource references.
1124            let mut curr_references: BTreeMap<UnresolvedItemName, Vec<UnresolvedItemName>> =
1125                catalog
1126                    .get_item(&id)
1127                    .used_by()
1128                    .into_iter()
1129                    .filter_map(|subsource| {
1130                        let item = catalog.get_item(subsource);
1131                        item.subsource_details().map(|(_id, reference, _details)| {
1132                            let name = item.name();
1133                            let subsource_name = catalog.resolve_full_name(name);
1134                            let subsource_name = UnresolvedItemName::from(subsource_name);
1135                            (reference.clone(), subsource_name)
1136                        })
1137                    })
1138                    .fold(BTreeMap::new(), |mut map, (reference, subsource_name)| {
1139                        map.entry(reference)
1140                            .or_insert_with(Vec::new)
1141                            .push(subsource_name);
1142                        map
1143                    });
1144
1145            match &mut stmt.connection {
1146                CreateSourceConnection::Postgres { options, .. } => {
1147                    options.retain_mut(|o| {
1148                        match o.name {
1149                            // Dropping a subsource does not remove any `TEXT
1150                            // COLUMNS` values that refer to the table it
1151                            // ingests, which we'll handle below.
1152                            PgConfigOptionName::TextColumns => {}
1153                            // Drop details, which does not roundtrip.
1154                            PgConfigOptionName::Details => return false,
1155                            _ => return true,
1156                        };
1157                        match &mut o.value {
1158                            Some(WithOptionValue::Sequence(text_cols)) => {
1159                                text_cols.retain(|v| match v {
1160                                    WithOptionValue::UnresolvedItemName(n) => {
1161                                        let mut name = n.clone();
1162                                        // Remove the column reference.
1163                                        name.0.truncate(3);
1164                                        curr_references.contains_key(&name)
1165                                    }
1166                                    _ => unreachable!(
1167                                        "TEXT COLUMNS must be sequence of unresolved item names"
1168                                    ),
1169                                });
1170                                !text_cols.is_empty()
1171                            }
1172                            _ => unreachable!(
1173                                "TEXT COLUMNS must be sequence of unresolved item names"
1174                            ),
1175                        }
1176                    });
1177                }
1178                CreateSourceConnection::SqlServer { options, .. } => {
1179                    // TODO(sql_server2): TEXT and EXCLUDE columns are represented by
1180                    // `schema.table.column` whereas our external table references are
1181                    // `database.schema.table`. We handle the mismatch here but should
1182                    // probably fully qualify our TEXT and EXCLUDE column references.
1183                    let adjusted_references: BTreeSet<_> = curr_references
1184                        .keys()
1185                        .map(|name| {
1186                            if name.0.len() == 3 {
1187                                // Strip the database component of the name.
1188                                let adjusted_name = name.0[1..].to_vec();
1189                                UnresolvedItemName(adjusted_name)
1190                            } else {
1191                                name.clone()
1192                            }
1193                        })
1194                        .collect();
1195
1196                    options.retain_mut(|o| {
1197                        match o.name {
1198                            // Dropping a subsource does not remove any `TEXT COLUMNS`
1199                            // values that refer to the table it ingests, which we'll
1200                            // handle below.
1201                            SqlServerConfigOptionName::TextColumns
1202                            | SqlServerConfigOptionName::ExcludeColumns => {}
1203                            // Drop details, which does not roundtrip.
1204                            SqlServerConfigOptionName::Details => return false,
1205                        };
1206
1207                        match &mut o.value {
1208                            Some(WithOptionValue::Sequence(seq_unresolved_item_names)) => {
1209                                seq_unresolved_item_names.retain(|v| match v {
1210                                    WithOptionValue::UnresolvedItemName(n) => {
1211                                        let mut name = n.clone();
1212                                        // Remove column reference.
1213                                        name.0.truncate(2);
1214                                        adjusted_references.contains(&name)
1215                                    }
1216                                    _ => unreachable!(
1217                                        "TEXT COLUMNS + EXCLUDE COLUMNS must be sequence of unresolved item names"
1218                                    ),
1219                                });
1220                                !seq_unresolved_item_names.is_empty()
1221                            }
1222                            _ => unreachable!(
1223                                "TEXT COLUMNS + EXCLUDE COLUMNS must be sequence of unresolved item names"
1224                            ),
1225                        }
1226                    });
1227                }
1228                CreateSourceConnection::MySql { options, .. } => {
1229                    options.retain_mut(|o| {
1230                        match o.name {
1231                            // Dropping a subsource does not remove any `TEXT
1232                            // COLUMNS` values that refer to the table it
1233                            // ingests, which we'll handle below.
1234                            MySqlConfigOptionName::TextColumns
1235                            | MySqlConfigOptionName::ExcludeColumns => {}
1236                            // Drop details, which does not roundtrip.
1237                            MySqlConfigOptionName::Details => return false,
1238                        };
1239
1240                        match &mut o.value {
1241                            Some(WithOptionValue::Sequence(seq_unresolved_item_names)) => {
1242                                seq_unresolved_item_names.retain(|v| match v {
1243                                    WithOptionValue::UnresolvedItemName(n) => {
1244                                        let mut name = n.clone();
1245                                        // Remove column reference.
1246                                        name.0.truncate(2);
1247                                        curr_references.contains_key(&name)
1248                                    }
1249                                    _ => unreachable!(
1250                                        "TEXT COLUMNS + EXCLUDE COLUMNS must be sequence of unresolved item names"
1251                                    ),
1252                                });
1253                                !seq_unresolved_item_names.is_empty()
1254                            }
1255                            _ => unreachable!(
1256                                "TEXT COLUMNS + EXCLUDE COLUMNS must be sequence of unresolved item names"
1257                            ),
1258                        }
1259                    });
1260                }
1261                CreateSourceConnection::LoadGenerator { .. } if !curr_references.is_empty() => {
1262                    // Load generator sources with any references only support
1263                    // `FOR ALL TABLES`. However, this would change if database-issues#7911
1264                    // landed.
1265                    curr_references.clear();
1266                    stmt.external_references = Some(ExternalReferences::All);
1267                }
1268                CreateSourceConnection::Kafka { .. }
1269                | CreateSourceConnection::LoadGenerator { .. } => {}
1270            }
1271
1272            // If this source has any references, reconstruct them.
1273            if !curr_references.is_empty() {
1274                let mut subsources: Vec<_> = curr_references
1275                    .into_iter()
1276                    .flat_map(|(reference, names)| {
1277                        names.into_iter().map(move |name| ExternalReferenceExport {
1278                            reference: reference.clone(),
1279                            alias: Some(name),
1280                        })
1281                    })
1282                    .collect();
1283                subsources.sort();
1284                stmt.external_references = Some(ExternalReferences::SubsetTables(subsources));
1285            }
1286        }
1287        Statement::CreateSubsource(stmt) => {
1288            stmt.with_options.retain_mut(|o| {
1289                match o.name {
1290                    CreateSubsourceOptionName::TextColumns => true,
1291                    CreateSubsourceOptionName::RetainHistory => true,
1292                    CreateSubsourceOptionName::ExcludeColumns => true,
1293                    // Drop details, which does not roundtrip.
1294                    CreateSubsourceOptionName::Details => false,
1295                    CreateSubsourceOptionName::ExternalReference => true,
1296                    CreateSubsourceOptionName::Progress => true,
1297                }
1298            });
1299        }
1300        Statement::CreateSink(stmt) => {
1301            stmt.with_options.retain_mut(|o| {
1302                match o.name {
1303                    CreateSinkOptionName::CommitInterval => true,
1304                    CreateSinkOptionName::PartitionStrategy => true,
1305                    CreateSinkOptionName::Snapshot => true,
1306                    // Drop version, which does not roundtrip.
1307                    CreateSinkOptionName::Version => false,
1308                }
1309            });
1310        }
1311        _ => (),
1312    }
1313
1314    Ok(mz_sql_pretty::to_pretty(
1315        &resolved,
1316        PrettyConfig {
1317            width: mz_sql_pretty::DEFAULT_WIDTH,
1318            format_mode: if redacted {
1319                FormatMode::SimpleRedacted
1320            } else {
1321                FormatMode::Simple
1322            },
1323        },
1324    ))
1325}