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    scx.record_sql_impl_ids(&new_resolved_ids);
745    Ok(ShowColumnsSelect {
746        id: entry.id(),
747        show_select,
748        new_resolved_ids,
749    })
750}
751
752// The rationale for which fields to include in the tuples are those
753// that are mandatory when creating a replica as part of the CREATE
754// CLUSTER command, i.e., name and size.
755pub fn show_clusters<'a>(
756    scx: &'a StatementContext<'a>,
757    filter: Option<ShowStatementFilter<Aug>>,
758) -> Result<ShowSelect<'a>, PlanError> {
759    let query = "SELECT name, replicas, comment FROM mz_internal.mz_show_clusters".to_string();
760    ShowSelect::new(
761        scx,
762        query,
763        filter,
764        None,
765        Some(&["name", "replicas", "comment"]),
766    )
767}
768
769pub fn show_cluster_replicas<'a>(
770    scx: &'a StatementContext<'a>,
771    filter: Option<ShowStatementFilter<Aug>>,
772) -> Result<ShowSelect<'a>, PlanError> {
773    let query = "
774    SELECT cluster, replica, size, ready, comment
775    FROM mz_internal.mz_show_cluster_replicas
776    "
777    .to_string();
778
779    ShowSelect::new(
780        scx,
781        query,
782        filter,
783        None,
784        Some(&["cluster", "replica", "size", "ready", "comment"]),
785    )
786}
787
788pub fn show_secrets<'a>(
789    scx: &'a StatementContext<'a>,
790    from: Option<ResolvedSchemaName>,
791    filter: Option<ShowStatementFilter<Aug>>,
792) -> Result<ShowSelect<'a>, PlanError> {
793    let schema_spec = scx.resolve_optional_schema(&from)?;
794
795    let query = format!(
796        "SELECT name, comment
797        FROM mz_internal.mz_show_secrets
798        WHERE schema_id = '{schema_spec}'",
799    );
800
801    ShowSelect::new(scx, query, filter, None, Some(&["name", "comment"]))
802}
803
804pub fn show_privileges<'a>(
805    scx: &'a StatementContext<'a>,
806    object_type: Option<SystemObjectType>,
807    role: Option<ResolvedRoleName>,
808    filter: Option<ShowStatementFilter<Aug>>,
809) -> Result<ShowSelect<'a>, PlanError> {
810    let mut query_filter = Vec::new();
811    if let Some(object_type) = object_type {
812        query_filter.push(format!(
813            "object_type = '{}'",
814            object_type.to_string().to_lowercase()
815        ));
816    }
817    if let Some(role) = role {
818        let name = role.name;
819        query_filter.push(format!("CASE WHEN grantee = 'PUBLIC' THEN true ELSE pg_has_role('{name}', grantee, 'USAGE') END"));
820    }
821    let query_filter = if query_filter.len() > 0 {
822        format!("WHERE {}", itertools::join(query_filter, " AND "))
823    } else {
824        "".to_string()
825    };
826
827    let query = format!(
828        "SELECT grantor, grantee, database, schema, name, object_type, privilege_type
829        FROM mz_internal.mz_show_all_privileges
830        {query_filter}",
831    );
832
833    ShowSelect::new(
834        scx,
835        query,
836        filter,
837        None,
838        Some(&[
839            "grantor",
840            "grantee",
841            "database",
842            "schema",
843            "name",
844            "object_type",
845            "privilege_type",
846        ]),
847    )
848}
849
850pub fn show_default_privileges<'a>(
851    scx: &'a StatementContext<'a>,
852    object_type: Option<ObjectType>,
853    role: Option<ResolvedRoleName>,
854    filter: Option<ShowStatementFilter<Aug>>,
855) -> Result<ShowSelect<'a>, PlanError> {
856    let mut query_filter = Vec::new();
857    if let Some(object_type) = object_type {
858        query_filter.push(format!(
859            "object_type = '{}'",
860            object_type.to_string().to_lowercase()
861        ));
862    }
863    if let Some(role) = role {
864        let name = role.name;
865        query_filter.push(format!("CASE WHEN grantee = 'PUBLIC' THEN true ELSE pg_has_role('{name}', grantee, 'USAGE') END"));
866    }
867    let query_filter = if query_filter.len() > 0 {
868        format!("WHERE {}", itertools::join(query_filter, " AND "))
869    } else {
870        "".to_string()
871    };
872
873    let query = format!(
874        "SELECT object_owner, database, schema, object_type, grantee, privilege_type
875        FROM mz_internal.mz_show_default_privileges
876        {query_filter}",
877    );
878
879    ShowSelect::new(
880        scx,
881        query,
882        filter,
883        None,
884        Some(&[
885            "object_owner",
886            "database",
887            "schema",
888            "object_type",
889            "grantee",
890            "privilege_type",
891        ]),
892    )
893}
894
895pub fn show_role_membership<'a>(
896    scx: &'a StatementContext<'a>,
897    role: Option<ResolvedRoleName>,
898    filter: Option<ShowStatementFilter<Aug>>,
899) -> Result<ShowSelect<'a>, PlanError> {
900    let mut query_filter = Vec::new();
901    if let Some(role) = role {
902        let name = role.name;
903        query_filter.push(format!("pg_has_role('{name}', member, 'USAGE')"));
904    }
905    let query_filter = if query_filter.len() > 0 {
906        format!("WHERE {}", itertools::join(query_filter, " AND "))
907    } else {
908        "".to_string()
909    };
910
911    let query = format!(
912        "SELECT role, member, grantor
913        FROM mz_internal.mz_show_role_members
914        {query_filter}",
915    );
916
917    ShowSelect::new(
918        scx,
919        query,
920        filter,
921        None,
922        Some(&["role", "member", "grantor"]),
923    )
924}
925
926/// An intermediate result when planning a `SHOW` query.
927///
928/// Can be interrogated for its columns, or converted into a proper [`Plan`].
929pub struct ShowSelect<'a> {
930    scx: &'a StatementContext<'a>,
931    pub(crate) stmt: SelectStatement<Aug>,
932}
933
934impl<'a> ShowSelect<'a> {
935    /// Constructs a new [`ShowSelect`] from a query that provides the base
936    /// data and an optional user-supplied filter, order column, and
937    /// projection on that data.
938    ///
939    /// Note that the query must return a column named `name`, as the filter
940    /// may implicitly reference this column. Any `ORDER BY` in the query is
941    /// ignored. `ShowSelects`s are always ordered in ascending order by all
942    /// columns from left to right unless an order field is supplied.
943    fn new(
944        scx: &'a StatementContext,
945        query: String,
946        filter: Option<ShowStatementFilter<Aug>>,
947        order: Option<&str>,
948        projection: Option<&[&str]>,
949    ) -> Result<ShowSelect<'a>, PlanError> {
950        let (show_select, new_resolved_ids) =
951            Self::new_with_resolved_ids(scx, query, filter, order, projection)?;
952        scx.sql_impl_resolved_ids
953            .lock()
954            .expect("planning is single-threaded")
955            .extend_from(&new_resolved_ids);
956        Ok(show_select)
957    }
958
959    fn new_with_resolved_ids(
960        scx: &'a StatementContext,
961        query: String,
962        filter: Option<ShowStatementFilter<Aug>>,
963        order: Option<&str>,
964        projection: Option<&[&str]>,
965    ) -> Result<(ShowSelect<'a>, ResolvedIds), PlanError> {
966        let filter = match filter {
967            Some(ShowStatementFilter::Like(like)) => format!("name LIKE {}", Value::String(like)),
968            Some(ShowStatementFilter::Where(expr)) => expr.to_string(),
969            None => "true".to_string(),
970        };
971        let query = format!(
972            "SELECT {} FROM ({}) q WHERE {} ORDER BY {}",
973            projection
974                .map(|ps| ps.join(", "))
975                .unwrap_or_else(|| "*".into()),
976            query,
977            filter,
978            order.unwrap_or("q.*")
979        );
980
981        Self::new_from_bare_query(scx, query)
982    }
983
984    pub fn new_from_bare_query(
985        scx: &'a StatementContext,
986        query: String,
987    ) -> Result<(ShowSelect<'a>, ResolvedIds), PlanError> {
988        let stmts = parse::parse(&query)
989            .map_err(|e| internal_err!("failed to parse generated SHOW query: {}", e))?;
990        let stmt = match stmts.into_element().ast {
991            Statement::Select(select) => select,
992            _ => bail_internal!("generated SHOW query was not a SELECT statement"),
993        };
994        let (mut stmt, new_resolved_ids) = names::resolve(scx.catalog, stmt)?;
995        transform_ast::transform(scx, &mut stmt)?;
996        Ok((ShowSelect { scx, stmt }, new_resolved_ids))
997    }
998
999    /// Computes the shape of this `ShowSelect`.
1000    pub fn describe(self) -> Result<StatementDesc, PlanError> {
1001        dml::describe_select(self.scx, self.stmt)
1002    }
1003
1004    /// Converts this `ShowSelect` into a [`Plan`].
1005    pub fn plan(self) -> Result<Plan, PlanError> {
1006        dml::plan_select(self.scx, self.stmt, &Params::empty(), None)
1007    }
1008
1009    /// Converts this `ShowSelect` into a [`(HirRelationExpr, Scope)`].
1010    pub fn plan_hir(self, qcx: &QueryContext) -> Result<(HirRelationExpr, Scope), PlanError> {
1011        query::plan_nested_query(&mut qcx.clone(), &self.stmt.query)
1012    }
1013}
1014
1015pub struct ShowColumnsSelect<'a> {
1016    id: CatalogItemId,
1017    new_resolved_ids: ResolvedIds,
1018    show_select: ShowSelect<'a>,
1019}
1020
1021impl<'a> ShowColumnsSelect<'a> {
1022    pub fn describe(self) -> Result<StatementDesc, PlanError> {
1023        self.show_select.describe()
1024    }
1025
1026    pub fn plan(self) -> Result<Plan, PlanError> {
1027        let select_plan = self.show_select.plan()?;
1028        match select_plan {
1029            Plan::Select(select_plan) => Ok(Plan::ShowColumns(ShowColumnsPlan {
1030                id: self.id,
1031                select_plan,
1032                new_resolved_ids: self.new_resolved_ids,
1033            })),
1034            _ => {
1035                tracing::error!(
1036                    "SHOW COLUMNS produced a non select plan. plan: {:?}",
1037                    select_plan
1038                );
1039                Err(PlanError::Unstructured(
1040                    "SHOW COLUMNS produced an unexpected plan. Please file a bug.".to_string(),
1041                ))
1042            }
1043        }
1044    }
1045
1046    pub fn plan_hir(self, qcx: &QueryContext) -> Result<(HirRelationExpr, Scope), PlanError> {
1047        self.show_select.plan_hir(qcx)
1048    }
1049}
1050
1051/// Convert a SQL statement into a form that could be used as input, as well as
1052/// is more amenable to human consumption.
1053fn humanize_sql_for_show_create(
1054    catalog: &dyn SessionCatalog,
1055    id: CatalogItemId,
1056    sql: &str,
1057    redacted: bool,
1058) -> Result<String, PlanError> {
1059    use mz_sql_parser::ast::{CreateSourceConnection, MySqlConfigOptionName, PgConfigOptionName};
1060
1061    let parsed = parse::parse(sql)?.into_element().ast;
1062    let (mut resolved, _) = names::resolve(catalog, parsed)?;
1063
1064    // Simplify names.
1065    let mut simplifier = NameSimplifier { catalog };
1066    simplifier.visit_statement_mut(&mut resolved);
1067
1068    match &mut resolved {
1069        // Strip internal `AS OF` syntax.
1070        Statement::CreateMaterializedView(stmt) => stmt.as_of = None,
1071        // `CREATE SOURCE` statements should roundtrip. However, sources and
1072        // their subsources have a complex relationship, so we need to do a lot
1073        // of work to reconstruct the statement for multi-output sources.
1074        //
1075        // For instance, `DROP SOURCE` statements can leave dangling references
1076        // to subsources that must be filtered out here, that, due to catalog
1077        // transaction limitations, can only be cleaned up when a top-level
1078        // source is altered.
1079        Statement::CreateSource(stmt) => {
1080            // Collect all current subsource references.
1081            let mut curr_references: BTreeMap<UnresolvedItemName, Vec<UnresolvedItemName>> =
1082                catalog
1083                    .get_item(&id)
1084                    .used_by()
1085                    .into_iter()
1086                    .filter_map(|subsource| {
1087                        let item = catalog.get_item(subsource);
1088                        item.subsource_details().map(|(_id, reference, _details)| {
1089                            let name = item.name();
1090                            let subsource_name = catalog.resolve_full_name(name);
1091                            let subsource_name = UnresolvedItemName::from(subsource_name);
1092                            (reference.clone(), subsource_name)
1093                        })
1094                    })
1095                    .fold(BTreeMap::new(), |mut map, (reference, subsource_name)| {
1096                        map.entry(reference)
1097                            .or_insert_with(Vec::new)
1098                            .push(subsource_name);
1099                        map
1100                    });
1101
1102            match &mut stmt.connection {
1103                CreateSourceConnection::Postgres { options, .. } => {
1104                    options.retain_mut(|o| {
1105                        match o.name {
1106                            // Dropping a subsource does not remove any `TEXT
1107                            // COLUMNS` values that refer to the table it
1108                            // ingests, which we'll handle below.
1109                            PgConfigOptionName::TextColumns => {}
1110                            // Drop details, which does not roundtrip.
1111                            PgConfigOptionName::Details => return false,
1112                            _ => return true,
1113                        };
1114                        match &mut o.value {
1115                            Some(WithOptionValue::Sequence(text_cols)) => {
1116                                text_cols.retain(|v| match v {
1117                                    WithOptionValue::UnresolvedItemName(n) => {
1118                                        let mut name = n.clone();
1119                                        // Remove the column reference.
1120                                        name.0.truncate(3);
1121                                        curr_references.contains_key(&name)
1122                                    }
1123                                    _ => unreachable!(
1124                                        "TEXT COLUMNS must be sequence of unresolved item names"
1125                                    ),
1126                                });
1127                                !text_cols.is_empty()
1128                            }
1129                            _ => unreachable!(
1130                                "TEXT COLUMNS must be sequence of unresolved item names"
1131                            ),
1132                        }
1133                    });
1134                }
1135                CreateSourceConnection::SqlServer { options, .. } => {
1136                    // TODO(sql_server2): TEXT and EXCLUDE columns are represented by
1137                    // `schema.table.column` whereas our external table references are
1138                    // `database.schema.table`. We handle the mismatch here but should
1139                    // probably fully qualify our TEXT and EXCLUDE column references.
1140                    let adjusted_references: BTreeSet<_> = curr_references
1141                        .keys()
1142                        .map(|name| {
1143                            if name.0.len() == 3 {
1144                                // Strip the database component of the name.
1145                                let adjusted_name = name.0[1..].to_vec();
1146                                UnresolvedItemName(adjusted_name)
1147                            } else {
1148                                name.clone()
1149                            }
1150                        })
1151                        .collect();
1152
1153                    options.retain_mut(|o| {
1154                        match o.name {
1155                            // Dropping a subsource does not remove any `TEXT COLUMNS`
1156                            // values that refer to the table it ingests, which we'll
1157                            // handle below.
1158                            SqlServerConfigOptionName::TextColumns
1159                            | SqlServerConfigOptionName::ExcludeColumns => {}
1160                            // Drop details, which does not roundtrip.
1161                            SqlServerConfigOptionName::Details => return false,
1162                        };
1163
1164                        match &mut o.value {
1165                            Some(WithOptionValue::Sequence(seq_unresolved_item_names)) => {
1166                                seq_unresolved_item_names.retain(|v| match v {
1167                                    WithOptionValue::UnresolvedItemName(n) => {
1168                                        let mut name = n.clone();
1169                                        // Remove column reference.
1170                                        name.0.truncate(2);
1171                                        adjusted_references.contains(&name)
1172                                    }
1173                                    _ => unreachable!(
1174                                        "TEXT COLUMNS + EXCLUDE COLUMNS must be sequence of unresolved item names"
1175                                    ),
1176                                });
1177                                !seq_unresolved_item_names.is_empty()
1178                            }
1179                            _ => unreachable!(
1180                                "TEXT COLUMNS + EXCLUDE COLUMNS must be sequence of unresolved item names"
1181                            ),
1182                        }
1183                    });
1184                }
1185                CreateSourceConnection::MySql { options, .. } => {
1186                    options.retain_mut(|o| {
1187                        match o.name {
1188                            // Dropping a subsource does not remove any `TEXT
1189                            // COLUMNS` values that refer to the table it
1190                            // ingests, which we'll handle below.
1191                            MySqlConfigOptionName::TextColumns
1192                            | MySqlConfigOptionName::ExcludeColumns => {}
1193                            // Drop details, which does not roundtrip.
1194                            MySqlConfigOptionName::Details => return false,
1195                        };
1196
1197                        match &mut o.value {
1198                            Some(WithOptionValue::Sequence(seq_unresolved_item_names)) => {
1199                                seq_unresolved_item_names.retain(|v| match v {
1200                                    WithOptionValue::UnresolvedItemName(n) => {
1201                                        let mut name = n.clone();
1202                                        // Remove column reference.
1203                                        name.0.truncate(2);
1204                                        curr_references.contains_key(&name)
1205                                    }
1206                                    _ => unreachable!(
1207                                        "TEXT COLUMNS + EXCLUDE COLUMNS must be sequence of unresolved item names"
1208                                    ),
1209                                });
1210                                !seq_unresolved_item_names.is_empty()
1211                            }
1212                            _ => unreachable!(
1213                                "TEXT COLUMNS + EXCLUDE COLUMNS must be sequence of unresolved item names"
1214                            ),
1215                        }
1216                    });
1217                }
1218                CreateSourceConnection::LoadGenerator { .. } if !curr_references.is_empty() => {
1219                    // Load generator sources with any references only support
1220                    // `FOR ALL TABLES`. However, this would change if database-issues#7911
1221                    // landed.
1222                    curr_references.clear();
1223                    stmt.external_references = Some(ExternalReferences::All);
1224                }
1225                CreateSourceConnection::Kafka { .. }
1226                | CreateSourceConnection::LoadGenerator { .. } => {}
1227            }
1228
1229            // If this source has any references, reconstruct them.
1230            if !curr_references.is_empty() {
1231                let mut subsources: Vec<_> = curr_references
1232                    .into_iter()
1233                    .flat_map(|(reference, names)| {
1234                        names.into_iter().map(move |name| ExternalReferenceExport {
1235                            reference: reference.clone(),
1236                            alias: Some(name),
1237                        })
1238                    })
1239                    .collect();
1240                subsources.sort();
1241                stmt.external_references = Some(ExternalReferences::SubsetTables(subsources));
1242            }
1243        }
1244        Statement::CreateSubsource(stmt) => {
1245            stmt.with_options.retain_mut(|o| {
1246                match o.name {
1247                    CreateSubsourceOptionName::TextColumns => true,
1248                    CreateSubsourceOptionName::RetainHistory => true,
1249                    CreateSubsourceOptionName::ExcludeColumns => true,
1250                    // Drop details, which does not roundtrip.
1251                    CreateSubsourceOptionName::Details => false,
1252                    CreateSubsourceOptionName::ExternalReference => true,
1253                    CreateSubsourceOptionName::Progress => true,
1254                }
1255            });
1256        }
1257
1258        _ => (),
1259    }
1260
1261    Ok(mz_sql_pretty::to_pretty(
1262        &resolved,
1263        PrettyConfig {
1264            width: mz_sql_pretty::DEFAULT_WIDTH,
1265            format_mode: if redacted {
1266                FormatMode::SimpleRedacted
1267            } else {
1268                FormatMode::Simple
1269            },
1270        },
1271    ))
1272}