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