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