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