Skip to main content

mz_deploy/client/
introspection.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//! Read-only catalog introspection queries.
11//!
12//! Methods on [`IntrospectionClient`] query the `mz_catalog` and
13//! `information_schema` to inspect the live environment without modifying it.
14//! Provides batch existence checks for schemas, clusters, and objects, as well
15//! as dependency lookups used during deployment planning and sink repointing.
16
17use crate::client::connection::{Client, IntrospectionClient};
18use crate::client::errors::ConnectionError;
19use crate::client::models::{Cluster, ClusterConfig, ClusterOptions, ClusterReplica, ObjectGrant};
20use crate::client::quote_identifier;
21use crate::client::sql_placeholders;
22use crate::client::staging_suffix_like_pattern;
23use crate::project::SchemaQualifier;
24use crate::project::ir::object_id::ObjectId;
25use itertools::Itertools;
26use std::collections::{BTreeMap, BTreeSet};
27use tokio_postgres::types::ToSql;
28
29/// A sink that depends on an object in a schema being dropped.
30///
31/// Used during apply to identify sinks that need to be repointed to new
32/// upstream objects before the old schemas are dropped with CASCADE.
33#[derive(Debug, Clone, serde::Serialize)]
34pub struct DependentSink {
35    pub sink_database: String,
36    pub sink_schema: String,
37    pub sink_name: String,
38    pub dependency_database: String,
39    pub dependency_schema: String,
40    pub dependency_name: String,
41    pub dependency_type: String,
42}
43
44/// Check if a schema exists in the specified database.
45pub(super) async fn schema_exists(
46    client: &Client,
47    database: &str,
48    schema: &str,
49) -> Result<bool, ConnectionError> {
50    let query = r#"
51        SELECT EXISTS(
52            SELECT 1
53            FROM mz_catalog.mz_schemas s
54            JOIN mz_catalog.mz_databases d ON s.database_id = d.id
55            WHERE s.name = $1 AND d.name = $2
56        ) AS exists
57    "#;
58
59    let row = client.query_one(query, &[&schema, &database]).await?;
60
61    Ok(row.get("exists"))
62}
63
64/// Check if a cluster exists.
65pub(super) async fn cluster_exists(client: &Client, name: &str) -> Result<bool, ConnectionError> {
66    let query = r#"
67        SELECT EXISTS(
68            SELECT 1 FROM mz_catalog.mz_clusters WHERE name = $1
69        ) AS exists
70    "#;
71
72    let row = client.query_one(query, &[&name]).await?;
73
74    Ok(row.get("exists"))
75}
76
77/// The cluster's configured autoscaling policy, read from the
78/// `auto_scaling_strategy` column of a cluster query row. A `NULL` column means
79/// "no policy": the cluster has none, or the region predates the feature.
80fn parse_auto_scaling_strategy(
81    row: &tokio_postgres::Row,
82    cluster_name: &str,
83) -> Result<Option<mz_sql::plan::AutoScalingStrategy>, ConnectionError> {
84    let json: Option<String> = row.get("auto_scaling_strategy");
85    match json {
86        None => Ok(None),
87        Some(json) => crate::client::auto_scaling::strategy_from_catalog_json(&json).map_err(|e| {
88            ConnectionError::Message(format!(
89                "invalid autoscaling strategy for cluster '{}': {}",
90                cluster_name, e
91            ))
92        }),
93    }
94}
95
96/// SQL fragments (a select expression and a join) that add the configured
97/// autoscaling policy to a cluster query. On regions that predate the feature,
98/// they yield a `NULL` policy column instead.
99async fn auto_scaling_query_parts(
100    client: &Client,
101) -> Result<(&'static str, &'static str), ConnectionError> {
102    if client.supports_auto_scaling_strategies().await? {
103        Ok((
104            "scaling.strategy::text AS auto_scaling_strategy",
105            "LEFT JOIN mz_internal.mz_cluster_auto_scaling_strategies scaling \
106             ON scaling.cluster_id = c.id",
107        ))
108    } else {
109        Ok(("NULL::text AS auto_scaling_strategy", ""))
110    }
111}
112
113/// Get a cluster by name.
114pub(super) async fn get_cluster(
115    client: &Client,
116    name: &str,
117) -> Result<Option<Cluster>, ConnectionError> {
118    let (strategy_col, strategy_join) = auto_scaling_query_parts(client).await?;
119    let query = format!(
120        r#"
121        SELECT
122            c.id,
123            c.name,
124            c.size,
125            c.replication_factor::bigint AS replication_factor,
126            {strategy_col}
127        FROM mz_catalog.mz_clusters c
128        {strategy_join}
129        WHERE c.name = $1
130    "#
131    );
132
133    let rows = client.query(&query, &[&name]).await?;
134
135    if rows.is_empty() {
136        return Ok(None);
137    }
138
139    let row = &rows[0];
140    Ok(Some(Cluster {
141        id: row.get("id"),
142        name: row.get("name"),
143        size: row.get("size"),
144        replication_factor: row.get("replication_factor"),
145        auto_scaling_strategy: parse_auto_scaling_strategy(row, name)?,
146    }))
147}
148
149/// List all clusters.
150pub(super) async fn list_clusters(client: &Client) -> Result<Vec<Cluster>, ConnectionError> {
151    let (strategy_col, strategy_join) = auto_scaling_query_parts(client).await?;
152    let query = format!(
153        r#"
154        SELECT
155            c.id,
156            c.name,
157            c.size,
158            c.replication_factor::bigint AS replication_factor,
159            {strategy_col}
160        FROM mz_catalog.mz_clusters c
161        {strategy_join}
162        ORDER BY c.name
163    "#
164    );
165
166    let rows = client.query(&query, &[]).await?;
167
168    rows.iter()
169        .map(|row| {
170            let name: String = row.get("name");
171            let auto_scaling_strategy = parse_auto_scaling_strategy(row, &name)?;
172            Ok(Cluster {
173                id: row.get("id"),
174                name,
175                size: row.get("size"),
176                replication_factor: row.get("replication_factor"),
177                auto_scaling_strategy,
178            })
179        })
180        .collect()
181}
182
183/// Get cluster configuration including replicas and grants.
184///
185/// This fetches all information needed to clone a cluster's configuration:
186/// - For managed clusters: size and replication factor
187/// - For unmanaged clusters: replica configurations
188/// - For both: privilege grants
189pub(super) async fn get_cluster_config(
190    client: &Client,
191    name: &str,
192) -> Result<Option<ClusterConfig>, ConnectionError> {
193    // Query 1: Get cluster info and replicas with LEFT JOIN
194    let (strategy_col, strategy_join) = auto_scaling_query_parts(client).await?;
195    let cluster_query = format!(
196        r#"
197        SELECT
198            c.id,
199            c.name,
200            c.managed,
201            c.size,
202            c.replication_factor::bigint AS replication_factor,
203            {strategy_col},
204            r.name AS replica_name,
205            r.size AS replica_size,
206            r.availability_zone
207        FROM mz_catalog.mz_clusters c
208        {strategy_join}
209        LEFT JOIN mz_catalog.mz_cluster_replicas r ON c.id = r.cluster_id
210        WHERE c.name = $1
211        ORDER BY r.name
212    "#
213    );
214
215    let cluster_rows = client.query(&cluster_query, &[&name]).await?;
216
217    if cluster_rows.is_empty() {
218        return Ok(None);
219    }
220
221    // Extract cluster-level info from first row
222    let first_row = &cluster_rows[0];
223    let managed: bool = first_row.get("managed");
224    let size: Option<String> = first_row.get("size");
225    let replication_factor: Option<i64> = first_row.get("replication_factor");
226    let auto_scaling_strategy = parse_auto_scaling_strategy(first_row, name)?;
227
228    // Query 2: Get grants (excluding owner's implicit privileges)
229    let grants_query = r#"
230        WITH cluster_privilege AS (
231            SELECT mz_internal.mz_aclexplode(privileges).*, owner_id
232            FROM mz_clusters
233            WHERE name = $1
234        )
235        SELECT
236            grantee.name AS grantee,
237            c.privilege_type
238        FROM cluster_privilege AS c
239        JOIN mz_roles AS grantee ON c.grantee = grantee.id
240        WHERE grantee.name NOT IN ('none', 'mz_system', 'mz_support')
241          AND c.grantee != c.owner_id
242    "#;
243
244    let grant_rows = client.query(grants_query, &[&name]).await?;
245
246    let grants: Vec<ObjectGrant> = grant_rows
247        .iter()
248        .map(|row| ObjectGrant {
249            grantee: row.get("grantee"),
250            privilege_type: row.get("privilege_type"),
251        })
252        .collect();
253
254    if managed {
255        // Managed cluster
256        let size = size.ok_or_else(|| {
257            ConnectionError::Message(format!(
258                "Managed cluster '{}' has no size (unexpected)",
259                name
260            ))
261        })?;
262
263        let replication_factor = replication_factor.unwrap_or(1).try_into().map_err(|_| {
264            ConnectionError::Message(format!("Invalid replication_factor for cluster '{}'", name))
265        })?;
266
267        Ok(Some(ClusterConfig::Managed {
268            options: ClusterOptions {
269                size,
270                replication_factor,
271                auto_scaling_strategy,
272            },
273            grants,
274        }))
275    } else {
276        // Unmanaged cluster - collect replicas
277        let mut replicas = Vec::new();
278        for row in &cluster_rows {
279            let replica_name: Option<String> = row.get("replica_name");
280            if let Some(replica_name) = replica_name {
281                replicas.push(ClusterReplica {
282                    name: replica_name,
283                    size: row.get("replica_size"),
284                    availability_zone: row.get("availability_zone"),
285                });
286            }
287        }
288
289        Ok(Some(ClusterConfig::Unmanaged { replicas, grants }))
290    }
291}
292
293/// Check if a network policy exists.
294pub(super) async fn network_policy_exists(
295    client: &Client,
296    name: &str,
297) -> Result<bool, ConnectionError> {
298    let query = r#"
299        SELECT EXISTS(
300            SELECT 1 FROM mz_catalog.mz_network_policies WHERE name = $1
301        ) AS exists
302    "#;
303
304    let row = client.query_one(query, &[&name]).await?;
305
306    Ok(row.get("exists"))
307}
308
309/// Check if a role exists.
310pub(super) async fn role_exists(client: &Client, name: &str) -> Result<bool, ConnectionError> {
311    let query = r#"
312        SELECT EXISTS(
313            SELECT 1 FROM mz_catalog.mz_roles WHERE name = $1
314        ) AS exists
315    "#;
316
317    let row = client.query_one(query, &[&name]).await?;
318
319    Ok(row.get("exists"))
320}
321
322/// Get the members granted to a role.
323pub(super) async fn get_role_members(
324    client: &Client,
325    role_name: &str,
326) -> Result<Vec<String>, ConnectionError> {
327    let query = r#"
328        SELECT m.name AS member
329        FROM mz_catalog.mz_role_members rm
330        JOIN mz_catalog.mz_roles r ON r.id = rm.role_id
331        JOIN mz_catalog.mz_roles m ON m.id = rm.member
332        WHERE r.name = $1
333        ORDER BY m.name
334    "#;
335
336    let rows = client.query(query, &[&role_name]).await?;
337
338    Ok(rows.iter().map(|row| row.get("member")).collect())
339}
340
341/// Get session default parameter names for a role.
342pub(super) async fn get_role_parameters(
343    client: &Client,
344    role_name: &str,
345) -> Result<Vec<String>, ConnectionError> {
346    let query = r#"
347        SELECT rp.parameter_name
348        FROM mz_catalog.mz_role_parameters rp
349        JOIN mz_catalog.mz_roles r ON r.id = rp.role_id
350        WHERE r.name = $1
351        ORDER BY rp.parameter_name
352    "#;
353
354    let rows = client.query(query, &[&role_name]).await?;
355
356    Ok(rows.iter().map(|row| row.get("parameter_name")).collect())
357}
358
359/// Get the current Materialize user/role.
360pub(super) async fn get_current_user(client: &Client) -> Result<String, ConnectionError> {
361    let row = client.query_one("SELECT current_user()", &[]).await?;
362
363    Ok(row.get(0))
364}
365
366/// Check which schemas from a set of (database, schema) pairs exist.
367///
368/// Returns a BTreeSet of (database, schema) tuples that exist.
369pub(super) async fn check_schemas_exist(
370    client: &Client,
371    schemas: &[(String, String)],
372) -> Result<BTreeSet<(String, String)>, ConnectionError> {
373    if schemas.is_empty() {
374        return Ok(BTreeSet::new());
375    }
376
377    // Build FQN strings and a lookup map from FQN -> original tuple (reusing the same strings)
378    let fqns: Vec<String> = schemas
379        .iter()
380        .map(|(db, schema)| format!("{}.{}", db, schema))
381        .collect();
382
383    let fqn_map: BTreeMap<&str, &(String, String)> = fqns
384        .iter()
385        .zip_eq(schemas.iter())
386        .map(|(fqn, pair)| (fqn.as_str(), pair))
387        .collect();
388
389    let placeholders_str = sql_placeholders(fqns.len());
390
391    let query = format!(
392        r#"
393        SELECT d.name || '.' || s.name as fqn
394        FROM mz_catalog.mz_schemas s
395        JOIN mz_catalog.mz_databases d ON s.database_id = d.id
396        WHERE d.name || '.' || s.name IN ({})
397        ORDER BY fqn
398    "#,
399        placeholders_str
400    );
401
402    let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new();
403    for fqn in &fqns {
404        params.push(fqn);
405    }
406
407    let rows = client.query(&query, &params).await?;
408
409    let mut existing = BTreeSet::new();
410    for row in rows {
411        let fqn: String = row.get("fqn");
412        if let Some(pair) = fqn_map.get(fqn.as_str()) {
413            existing.insert((*pair).clone());
414        }
415    }
416
417    Ok(existing)
418}
419
420/// Check which clusters from a set of names exist.
421///
422/// Returns a BTreeSet of cluster names that exist.
423pub(super) async fn check_clusters_exist(
424    client: &Client,
425    clusters: &[String],
426) -> Result<BTreeSet<String>, ConnectionError> {
427    if clusters.is_empty() {
428        return Ok(BTreeSet::new());
429    }
430
431    let placeholders_str = sql_placeholders(clusters.len());
432
433    let query = format!(
434        r#"
435        SELECT name FROM mz_catalog.mz_clusters
436        WHERE name IN ({})
437        ORDER BY name
438    "#,
439        placeholders_str
440    );
441
442    let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new();
443    for name in clusters {
444        params.push(name);
445    }
446
447    let rows = client.query(&query, &params).await?;
448
449    Ok(rows.iter().map(|row| row.get("name")).collect())
450}
451
452/// Check which objects from a set exist in the production database.
453pub(super) async fn check_objects_exist(
454    client: &Client,
455    objects: &BTreeSet<ObjectId>,
456) -> Result<BTreeSet<ObjectId>, ConnectionError> {
457    if objects.is_empty() {
458        return Ok(BTreeSet::new());
459    }
460
461    let fqn_map: BTreeMap<String, &ObjectId> = objects.iter().map(|o| (o.to_string(), o)).collect();
462    let fqns: Vec<&String> = fqn_map.keys().collect();
463
464    let placeholders_str = sql_placeholders(fqns.len());
465
466    let query = format!(
467        r#"
468        SELECT d.name || '.' || s.name || '.' || mo.name as fqn
469        FROM mz_objects mo
470        JOIN mz_schemas s ON mo.schema_id = s.id
471        JOIN mz_databases d ON s.database_id = d.id
472        WHERE d.name || '.' || s.name || '.' || mo.name IN ({})
473        AND mo.type IN ('table', 'view', 'materialized-view', 'source', 'sink')
474        ORDER BY fqn
475    "#,
476        placeholders_str
477    );
478
479    let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new();
480    for fqn in &fqns {
481        params.push(fqn);
482    }
483
484    let rows = client.query(&query, &params).await?;
485
486    Ok(rows
487        .iter()
488        .filter_map(|row| {
489            let fqn: String = row.get("fqn");
490            fqn_map.get(&fqn).map(|id| (*id).clone())
491        })
492        .collect())
493}
494
495/// Check which objects from the given set exist in a specific catalog table.
496///
497/// Returns a BTreeSet of ObjectIds for objects that already exist.
498async fn check_catalog_objects_exist(
499    client: &Client,
500    objects: &BTreeSet<ObjectId>,
501    catalog_table: &str,
502) -> Result<BTreeSet<ObjectId>, ConnectionError> {
503    if objects.is_empty() {
504        return Ok(BTreeSet::new());
505    }
506
507    // Build a lookup map from FQN string -> ObjectId for O(1) matching
508    let fqn_map: BTreeMap<String, &ObjectId> = objects.iter().map(|o| (o.to_string(), o)).collect();
509    let fqns: Vec<&String> = fqn_map.keys().collect();
510
511    let placeholders_str = sql_placeholders(fqns.len());
512
513    let query = format!(
514        r#"
515        SELECT d.name || '.' || s.name || '.' || t.name as fqn
516        FROM {} t
517        JOIN mz_schemas s ON t.schema_id = s.id
518        JOIN mz_databases d ON s.database_id = d.id
519        WHERE d.name || '.' || s.name || '.' || t.name IN ({})
520        ORDER BY fqn
521    "#,
522        catalog_table, placeholders_str
523    );
524
525    let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new();
526    for fqn in &fqns {
527        params.push(*fqn);
528    }
529
530    let rows = client.query(&query, &params).await?;
531
532    let mut existing = BTreeSet::new();
533    for row in rows {
534        let fqn: String = row.get("fqn");
535        if let Some(obj_id) = fqn_map.get(&fqn) {
536            existing.insert((*obj_id).clone());
537        }
538    }
539
540    Ok(existing)
541}
542
543/// Check which tables from the given set exist in the database.
544///
545/// Returns a BTreeSet of ObjectIds for tables that already exist.
546pub(super) async fn check_tables_exist(
547    client: &Client,
548    tables: &BTreeSet<ObjectId>,
549) -> Result<BTreeSet<ObjectId>, ConnectionError> {
550    check_catalog_objects_exist(client, tables, "mz_tables").await
551}
552
553/// Check which sources from the given set exist in the database.
554///
555/// Returns a BTreeSet of ObjectIds for sources that already exist.
556pub(super) async fn check_sources_exist(
557    client: &Client,
558    sources: &BTreeSet<ObjectId>,
559) -> Result<BTreeSet<ObjectId>, ConnectionError> {
560    check_catalog_objects_exist(client, sources, "mz_sources").await
561}
562
563/// Check which secrets from the given set exist in the database.
564///
565/// Returns a BTreeSet of ObjectIds for secrets that already exist.
566pub(super) async fn check_secrets_exist(
567    client: &Client,
568    secrets: &BTreeSet<ObjectId>,
569) -> Result<BTreeSet<ObjectId>, ConnectionError> {
570    check_catalog_objects_exist(client, secrets, "mz_secrets").await
571}
572
573/// Check which connections from the given set exist in the database.
574///
575/// Returns a BTreeSet of ObjectIds for connections that already exist.
576pub(super) async fn check_connections_exist(
577    client: &Client,
578    connections: &BTreeSet<ObjectId>,
579) -> Result<BTreeSet<ObjectId>, ConnectionError> {
580    check_catalog_objects_exist(client, connections, "mz_connections").await
581}
582
583/// Check which sinks from the given set exist in the database.
584///
585/// Returns a BTreeSet of ObjectIds for sinks that already exist.
586/// Used during apply to skip creating sinks that already exist (like tables).
587pub(super) async fn check_sinks_exist(
588    client: &Client,
589    sinks: &BTreeSet<ObjectId>,
590) -> Result<BTreeSet<ObjectId>, ConnectionError> {
591    check_catalog_objects_exist(client, sinks, "mz_sinks").await
592}
593
594/// Find sinks that depend on objects in the specified schemas.
595///
596/// This is used during apply to identify sinks that need to be repointed
597/// before old schemas are dropped with CASCADE. Only returns sinks whose
598/// upstream object (FROM clause) is in one of the specified schemas.
599pub(super) async fn find_sinks_depending_on_schemas(
600    client: &Client,
601    schemas: &[SchemaQualifier],
602) -> Result<Vec<DependentSink>, ConnectionError> {
603    if schemas.is_empty() {
604        return Ok(Vec::new());
605    }
606
607    // Build WHERE clause for (database, schema) pairs
608    let mut conditions = Vec::new();
609    let mut param_idx = 1;
610
611    for _ in schemas {
612        conditions.push(format!(
613            "(dep_db.name = ${} AND dep_schema.name = ${})",
614            param_idx,
615            param_idx + 1
616        ));
617        param_idx += 2;
618    }
619
620    let where_clause = conditions.join(" OR ");
621
622    let query = format!(
623        r#"
624        SELECT
625            sink_db.name as sink_database,
626            sink_schema.name as sink_schema,
627            sinks.name as sink_name,
628            dep_db.name as dependency_database,
629            dep_schema.name as dependency_schema,
630            dep_obj.name as dependency_name,
631            dep_obj.type as dependency_type
632        FROM mz_sinks sinks
633        JOIN mz_schemas sink_schema ON sinks.schema_id = sink_schema.id
634        JOIN mz_databases sink_db ON sink_schema.database_id = sink_db.id
635        JOIN mz_internal.mz_object_dependencies deps ON sinks.id = deps.object_id
636        JOIN mz_objects dep_obj ON deps.referenced_object_id = dep_obj.id
637        JOIN mz_schemas dep_schema ON dep_obj.schema_id = dep_schema.id
638        JOIN mz_databases dep_db ON dep_schema.database_id = dep_db.id
639        WHERE ({})
640          AND dep_obj.type IN ('materialized-view', 'table', 'source')
641        ORDER BY sink_db.name, sink_schema.name, sinks.name
642        "#,
643        where_clause
644    );
645
646    // Build params vector with references to the schema tuples
647    let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new();
648    for sq in schemas {
649        params.push(&sq.database);
650        params.push(&sq.schema);
651    }
652
653    let rows = client.query(&query, &params).await?;
654
655    Ok(rows
656        .iter()
657        .map(|row| DependentSink {
658            sink_database: row.get("sink_database"),
659            sink_schema: row.get("sink_schema"),
660            sink_name: row.get("sink_name"),
661            dependency_database: row.get("dependency_database"),
662            dependency_schema: row.get("dependency_schema"),
663            dependency_name: row.get("dependency_name"),
664            dependency_type: row.get("dependency_type"),
665        })
666        .collect())
667}
668
669/// Check if a connection exists in the specified database and schema.
670pub(super) async fn check_connection_exists(
671    client: &Client,
672    database: &str,
673    schema: &str,
674    name: &str,
675) -> Result<bool, ConnectionError> {
676    let query = r#"
677        SELECT EXISTS(
678            SELECT 1
679            FROM mz_catalog.mz_connections c
680            JOIN mz_catalog.mz_schemas s ON c.schema_id = s.id
681            JOIN mz_catalog.mz_databases d ON s.database_id = d.id
682            WHERE d.name = $1 AND s.name = $2 AND c.name = $3
683        ) AS exists
684    "#;
685    let row = client
686        .query_one(query, &[&database, &schema, &name])
687        .await?;
688    Ok(row.get("exists"))
689}
690
691/// Check if an object (MV, table, source) exists in the specified schema.
692///
693/// Used to verify that a replacement object exists before repointing a sink.
694pub(super) async fn object_exists(
695    client: &Client,
696    database: &str,
697    schema: &str,
698    object: &str,
699) -> Result<bool, ConnectionError> {
700    let query = r#"
701        SELECT EXISTS(
702            SELECT 1 FROM mz_objects o
703            JOIN mz_schemas s ON o.schema_id = s.id
704            JOIN mz_databases d ON s.database_id = d.id
705            WHERE d.name = $1 AND s.name = $2 AND o.name = $3
706              AND o.type IN ('materialized-view', 'table', 'source')
707        ) AS exists
708    "#;
709
710    let row = client
711        .query_one(query, &[&database, &schema, &object])
712        .await?;
713
714    Ok(row.get("exists"))
715}
716
717/// Get staging schema names for a specific deployment.
718pub(super) async fn get_staging_schemas(
719    client: &Client,
720    deploy_id: &str,
721) -> Result<Vec<SchemaQualifier>, ConnectionError> {
722    let pattern = staging_suffix_like_pattern(deploy_id);
723
724    let query = r#"
725        SELECT d.name as database, s.name as schema
726        FROM mz_schemas s
727        JOIN mz_databases d ON s.database_id = d.id
728        WHERE s.name LIKE $1 ESCAPE '\'
729    "#;
730
731    let rows = client.query(query, &[&pattern]).await?;
732
733    Ok(rows
734        .iter()
735        .map(|row| {
736            let database: String = row.get("database");
737            let schema: String = row.get("schema");
738            SchemaQualifier::new(database, schema)
739        })
740        .collect())
741}
742
743/// Get staging cluster names for a specific deployment.
744pub(super) async fn get_staging_clusters(
745    client: &Client,
746    deploy_id: &str,
747) -> Result<Vec<String>, ConnectionError> {
748    let pattern = staging_suffix_like_pattern(deploy_id);
749
750    let query = r#"
751        SELECT name
752        FROM mz_clusters
753        WHERE name LIKE $1 ESCAPE '\'
754    "#;
755
756    let rows = client.query(query, &[&pattern]).await?;
757
758    Ok(rows.iter().map(|row| row.get("name")).collect())
759}
760
761/// Map a Materialize object type string to its DROP keyword.
762fn mz_type_to_drop_keyword(obj_type: &str) -> Option<&'static str> {
763    match obj_type {
764        "table" => Some("TABLE"),
765        "view" => Some("VIEW"),
766        "materialized-view" => Some("MATERIALIZED VIEW"),
767        "source" => Some("SOURCE"),
768        "sink" => Some("SINK"),
769        _ => None,
770    }
771}
772
773/// Drop all objects in a schema.
774///
775/// Returns the fully-qualified names of dropped objects.
776pub(super) async fn drop_schema_objects(
777    client: &Client,
778    database: &str,
779    schema: &str,
780) -> Result<Vec<String>, ConnectionError> {
781    let query = r#"
782        SELECT mo.name, mo.type
783        FROM mz_objects mo
784        JOIN mz_schemas s ON mo.schema_id = s.id
785        JOIN mz_databases d ON s.database_id = d.id
786        WHERE d.name = $1 AND s.name = $2
787        AND mo.type IN ('table', 'view', 'materialized-view', 'source', 'sink')
788        ORDER BY mo.id DESC
789    "#;
790
791    let rows = client.query(query, &[&database, &schema]).await?;
792
793    let mut dropped = Vec::new();
794    for row in rows {
795        let name: String = row.get("name");
796        let obj_type: String = row.get("type");
797
798        let fqn = format!(
799            "{}.{}.{}",
800            quote_identifier(database),
801            quote_identifier(schema),
802            quote_identifier(&name)
803        );
804        let Some(drop_type) = mz_type_to_drop_keyword(obj_type.as_str()) else {
805            continue;
806        };
807
808        let drop_sql = format!("DROP {} IF EXISTS {} CASCADE", drop_type, fqn);
809        client.execute(&drop_sql, &[]).await?;
810
811        dropped.push(fqn);
812    }
813
814    Ok(dropped)
815}
816
817/// Drop specific objects by their ObjectIds.
818///
819/// Returns the fully-qualified names of dropped objects.
820pub(super) async fn drop_objects(
821    client: &Client,
822    objects: &BTreeSet<ObjectId>,
823) -> Result<Vec<String>, ConnectionError> {
824    let mut dropped = Vec::new();
825
826    if objects.is_empty() {
827        return Ok(dropped);
828    }
829
830    let placeholders_str = sql_placeholders(objects.len());
831
832    let query = format!(
833        r#"
834        SELECT mo.name, s.name as schema_name, d.name as database_name, mo.type
835        FROM mz_objects mo
836        JOIN mz_schemas s ON mo.schema_id = s.id
837        JOIN mz_databases d ON s.database_id = d.id
838        WHERE d.name || '.' || s.name || '.' || mo.name IN ({})
839        AND mo.type IN ('table', 'view', 'materialized-view', 'source', 'sink')
840        ORDER BY mo.id DESC
841    "#,
842        placeholders_str
843    );
844
845    let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new();
846    let fqns: Vec<_> = objects.iter().map(|object| object.to_string()).collect();
847    for fqn in &fqns {
848        params.push(fqn);
849    }
850
851    let rows = client.query(&query, &params).await?;
852
853    for row in rows {
854        let name: String = row.get("name");
855        let schema: String = row.get("schema_name");
856        let database: String = row.get("database_name");
857        let obj_type: String = row.get("type");
858
859        let fqn = format!(
860            "{}.{}.{}",
861            quote_identifier(&database),
862            quote_identifier(&schema),
863            quote_identifier(&name)
864        );
865        let Some(drop_type) = mz_type_to_drop_keyword(obj_type.as_str()) else {
866            continue;
867        };
868
869        let drop_sql = format!("DROP {} IF EXISTS {} CASCADE", drop_type, fqn);
870        client.execute(&drop_sql, &[]).await?;
871
872        dropped.push(fqn);
873    }
874
875    Ok(dropped)
876}
877
878/// Drop staging schemas by name.
879pub(super) async fn drop_staging_schemas(
880    client: &Client,
881    schemas: &[SchemaQualifier],
882) -> Result<(), ConnectionError> {
883    for sq in schemas {
884        let drop_sql = format!(
885            "DROP SCHEMA IF EXISTS {}.{} CASCADE",
886            quote_identifier(&sq.database),
887            quote_identifier(&sq.schema)
888        );
889        client.execute(&drop_sql, &[]).await?;
890    }
891
892    Ok(())
893}
894
895/// Drop staging clusters by name.
896pub(super) async fn drop_staging_clusters(
897    client: &Client,
898    clusters: &[String],
899) -> Result<(), ConnectionError> {
900    for cluster in clusters {
901        let drop_sql = format!(
902            "DROP CLUSTER IF EXISTS {} CASCADE",
903            quote_identifier(cluster)
904        );
905        client.execute(&drop_sql, &[]).await?;
906    }
907
908    Ok(())
909}
910
911/// Get privilege grants on a named infrastructure object (cluster, network policy).
912///
913/// `catalog_table` is the system catalog table (e.g., `"mz_clusters"`,
914/// `"mz_network_policies"`). Returns `(grantee, privilege_type)` pairs from
915/// `mz_aclexplode`, filtering out system roles.
916async fn get_named_object_grants(
917    client: &Client,
918    catalog_table: &str,
919    name: &str,
920) -> Result<Vec<ObjectGrant>, ConnectionError> {
921    let query = format!(
922        r#"
923        -- Explode the ACL bitmap into individual (grantee, privilege_type) rows.
924        -- Each object stores privileges as a compact bitmap; mz_aclexplode unpacks it.
925        WITH privilege AS (
926            SELECT mz_internal.mz_aclexplode(privileges).*, owner_id
927            FROM {}
928            WHERE name = $1
929        )
930        SELECT
931            grantee.name AS grantee,
932            p.privilege_type
933        FROM privilege AS p
934        -- Resolve grantee role IDs to human-readable names.
935        JOIN mz_roles AS grantee ON p.grantee = grantee.id
936        -- Exclude system roles that are not user-manageable.
937        WHERE grantee.name NOT IN ('none', 'mz_system', 'mz_support')
938          -- Owners implicitly have all privileges; don't surface those as explicit grants.
939          AND p.grantee != p.owner_id
940        "#,
941        catalog_table
942    );
943
944    let rows = client.query(&query, &[&name]).await?;
945
946    Ok(rows
947        .iter()
948        .map(|row| ObjectGrant {
949            grantee: row.get("grantee"),
950            privilege_type: row.get("privilege_type"),
951        })
952        .collect())
953}
954
955/// Get privilege grants on a cluster by name.
956pub(super) async fn get_cluster_grants(
957    client: &Client,
958    name: &str,
959) -> Result<Vec<ObjectGrant>, ConnectionError> {
960    get_named_object_grants(client, "mz_clusters", name).await
961}
962
963/// Get privilege grants on a network policy by name.
964pub(super) async fn get_network_policy_grants(
965    client: &Client,
966    name: &str,
967) -> Result<Vec<ObjectGrant>, ConnectionError> {
968    get_named_object_grants(client, "mz_network_policies", name).await
969}
970
971/// Get privilege grants on a database object (table, source, secret, connection).
972///
973/// `catalog_table` is the system catalog table name (e.g., `"mz_tables"`, `"mz_secrets"`).
974pub(super) async fn get_database_object_grants(
975    client: &Client,
976    catalog_table: &str,
977    database: &str,
978    schema: &str,
979    name: &str,
980) -> Result<Vec<ObjectGrant>, ConnectionError> {
981    let query = format!(
982        r#"
983        -- Locate the object by its fully-qualified name (database.schema.object)
984        -- using a 3-table join chain: catalog_table -> mz_schemas -> mz_databases.
985        -- Then explode the ACL bitmap into individual privilege rows.
986        WITH privilege AS (
987            SELECT mz_internal.mz_aclexplode(t.privileges).*, t.owner_id
988            FROM {} t
989            JOIN mz_schemas s ON t.schema_id = s.id
990            JOIN mz_databases d ON s.database_id = d.id
991            WHERE d.name = $1 AND s.name = $2 AND t.name = $3
992        )
993        SELECT
994            grantee.name AS grantee,
995            p.privilege_type
996        FROM privilege AS p
997        JOIN mz_roles AS grantee ON p.grantee = grantee.id
998        WHERE grantee.name NOT IN ('none', 'mz_system', 'mz_support')
999          AND p.grantee != p.owner_id
1000        "#,
1001        catalog_table
1002    );
1003
1004    let rows = client.query(&query, &[&database, &schema, &name]).await?;
1005
1006    Ok(rows
1007        .iter()
1008        .map(|row| ObjectGrant {
1009            grantee: row.get("grantee"),
1010            privilege_type: row.get("privilege_type"),
1011        })
1012        .collect())
1013}
1014
1015/// Get default privilege grants for a named infrastructure object (cluster, network policy).
1016///
1017/// Queries `mz_default_privileges` to find grants that would be auto-applied
1018/// to the given object based on its owner and any PUBLIC default privileges.
1019/// These grants should be protected from revocation during reconciliation.
1020async fn get_default_privilege_grants_for_named_object(
1021    client: &Client,
1022    catalog_table: &str,
1023    name: &str,
1024    object_type: &str,
1025) -> Result<Vec<ObjectGrant>, ConnectionError> {
1026    let query = format!(
1027        r#"
1028        -- Query default privileges from ALTER DEFAULT PRIVILEGES rules.
1029        -- These are auto-applied grants that should be protected from revocation.
1030        SELECT
1031            grantee_role.name AS grantee,
1032            dp_priv.privilege_type
1033        FROM mz_default_privileges dp
1034        -- Expand the privilege bitmap into individual privilege type strings.
1035        CROSS JOIN LATERAL unnest(
1036            mz_internal.mz_format_privileges(dp.privileges)
1037        ) AS dp_priv(privilege_type)
1038        JOIN {} obj ON obj.name = $1
1039        JOIN mz_roles AS grantee_role ON dp.grantee = grantee_role.id
1040        WHERE dp.object_type = $2
1041          -- Match rules targeting the object's owner, or PUBLIC ('p') rules
1042          -- that apply to all owners.
1043          AND (dp.role_id = obj.owner_id OR dp.role_id = 'p')
1044          -- Named objects (clusters, network policies) are not schema-scoped,
1045          -- so only global default privileges (both NULL) apply.
1046          AND dp.database_id IS NULL
1047          AND dp.schema_id IS NULL
1048          AND grantee_role.name NOT IN ('none', 'mz_system', 'mz_support')
1049        "#,
1050        catalog_table
1051    );
1052
1053    let rows = client.query(&query, &[&name, &object_type]).await?;
1054
1055    Ok(rows
1056        .iter()
1057        .map(|row| ObjectGrant {
1058            grantee: row.get("grantee"),
1059            privilege_type: row.get("privilege_type"),
1060        })
1061        .collect())
1062}
1063
1064/// Get default privilege grants for a cluster by name.
1065pub(super) async fn get_default_privilege_grants_for_cluster(
1066    client: &Client,
1067    name: &str,
1068) -> Result<Vec<ObjectGrant>, ConnectionError> {
1069    get_default_privilege_grants_for_named_object(client, "mz_clusters", name, "cluster").await
1070}
1071
1072/// Get default privilege grants for a network policy by name.
1073pub(super) async fn get_default_privilege_grants_for_network_policy(
1074    client: &Client,
1075    name: &str,
1076) -> Result<Vec<ObjectGrant>, ConnectionError> {
1077    get_default_privilege_grants_for_named_object(client, "mz_network_policies", name, "type").await
1078}
1079
1080/// Get default privilege grants for a database object (table, source, secret, connection).
1081///
1082/// Queries `mz_default_privileges` to find grants that would be auto-applied
1083/// to the given object based on its owner, database, schema, and any PUBLIC
1084/// default privileges. These grants should be protected from revocation.
1085pub(super) async fn get_default_privilege_grants_for_database_object(
1086    client: &Client,
1087    catalog_table: &str,
1088    database: &str,
1089    schema: &str,
1090    name: &str,
1091    object_type: &str,
1092) -> Result<Vec<ObjectGrant>, ConnectionError> {
1093    let query = format!(
1094        r#"
1095        -- Query default privileges from ALTER DEFAULT PRIVILEGES rules
1096        -- for a schema-qualified database object.
1097        SELECT
1098            grantee_role.name AS grantee,
1099            dp_priv.privilege_type
1100        FROM mz_default_privileges dp
1101        -- Expand the privilege bitmap into individual privilege type strings.
1102        CROSS JOIN LATERAL unnest(
1103            mz_internal.mz_format_privileges(dp.privileges)
1104        ) AS dp_priv(privilege_type)
1105        -- Locate the object by FQN to determine its owner, database, and schema.
1106        JOIN {} obj ON obj.name = $3
1107        JOIN mz_schemas s ON obj.schema_id = s.id
1108        JOIN mz_databases d ON s.database_id = d.id
1109        JOIN mz_roles AS grantee_role ON dp.grantee = grantee_role.id
1110        WHERE d.name = $1 AND s.name = $2
1111          AND dp.object_type = $4
1112          -- Match rules targeting the object's owner, or PUBLIC ('p') rules.
1113          AND (dp.role_id = obj.owner_id OR dp.role_id = 'p')
1114          -- Match both global rules (database_id IS NULL) and rules scoped to
1115          -- this specific database. Global rules apply to all databases.
1116          AND (dp.database_id IS NULL OR dp.database_id = d.id)
1117          -- Same for schema: global or scoped to this specific schema.
1118          AND (dp.schema_id IS NULL OR dp.schema_id = s.id)
1119          AND grantee_role.name NOT IN ('none', 'mz_system', 'mz_support')
1120        "#,
1121        catalog_table
1122    );
1123
1124    let rows = client
1125        .query(&query, &[&database, &schema, &name, &object_type])
1126        .await?;
1127
1128    Ok(rows
1129        .iter()
1130        .map(|row| ObjectGrant {
1131            grantee: row.get("grantee"),
1132            privilege_type: row.get("privilege_type"),
1133        })
1134        .collect())
1135}
1136
1137/// Get the `CREATE CONNECTION` SQL for an existing connection.
1138///
1139/// Uses `SHOW CREATE CONNECTION` which returns the canonical, non-redacted SQL
1140/// including fully-qualified secret references. Returns `None` if the
1141/// connection does not exist.
1142pub(super) async fn get_connection_create_sql(
1143    client: &Client,
1144    database: &str,
1145    schema: &str,
1146    name: &str,
1147) -> Result<Option<String>, ConnectionError> {
1148    let fqn = format!(
1149        "{}.{}.{}",
1150        quote_identifier(database),
1151        quote_identifier(schema),
1152        quote_identifier(name)
1153    );
1154    let query = format!("SHOW CREATE CONNECTION {}", fqn);
1155    let rows = client.query(&query, &[]).await?;
1156    Ok(rows.first().map(|row| row.get("create_sql")))
1157}
1158
1159impl IntrospectionClient<'_> {
1160    /// Get the current Materialize user/role.
1161    pub async fn get_current_user(&self) -> Result<String, ConnectionError> {
1162        get_current_user(self.client).await
1163    }
1164
1165    /// Check which objects from a set exist in the production database.
1166    pub async fn check_objects_exist(
1167        &self,
1168        objects: &BTreeSet<ObjectId>,
1169    ) -> Result<BTreeSet<ObjectId>, ConnectionError> {
1170        check_objects_exist(self.client, objects).await
1171    }
1172
1173    /// Check which objects from a set exist in a specific catalog table.
1174    pub async fn check_catalog_objects_exist(
1175        &self,
1176        objects: &BTreeSet<ObjectId>,
1177        catalog_table: &str,
1178    ) -> Result<BTreeSet<ObjectId>, ConnectionError> {
1179        check_catalog_objects_exist(self.client, objects, catalog_table).await
1180    }
1181
1182    /// Check which tables from the given set exist in the database.
1183    pub async fn check_tables_exist(
1184        &self,
1185        tables: &BTreeSet<ObjectId>,
1186    ) -> Result<BTreeSet<ObjectId>, ConnectionError> {
1187        check_tables_exist(self.client, tables).await
1188    }
1189
1190    /// Check which sources from the given set exist in the database.
1191    pub async fn check_sources_exist(
1192        &self,
1193        sources: &BTreeSet<ObjectId>,
1194    ) -> Result<BTreeSet<ObjectId>, ConnectionError> {
1195        check_sources_exist(self.client, sources).await
1196    }
1197
1198    /// Check which secrets from the given set exist in the database.
1199    pub async fn check_secrets_exist(
1200        &self,
1201        secrets: &BTreeSet<ObjectId>,
1202    ) -> Result<BTreeSet<ObjectId>, ConnectionError> {
1203        check_secrets_exist(self.client, secrets).await
1204    }
1205
1206    /// Check which connections from the given set exist in the database.
1207    pub async fn check_connections_exist(
1208        &self,
1209        connections: &BTreeSet<ObjectId>,
1210    ) -> Result<BTreeSet<ObjectId>, ConnectionError> {
1211        check_connections_exist(self.client, connections).await
1212    }
1213
1214    /// Check which sinks from the given set exist in the database.
1215    pub async fn check_sinks_exist(
1216        &self,
1217        sinks: &BTreeSet<ObjectId>,
1218    ) -> Result<BTreeSet<ObjectId>, ConnectionError> {
1219        check_sinks_exist(self.client, sinks).await
1220    }
1221
1222    /// Check which schemas from a set of (database, schema) pairs exist.
1223    pub async fn check_schemas_exist(
1224        &self,
1225        schemas: &[(String, String)],
1226    ) -> Result<BTreeSet<(String, String)>, ConnectionError> {
1227        check_schemas_exist(self.client, schemas).await
1228    }
1229
1230    /// Check which clusters from a set of names exist.
1231    pub async fn check_clusters_exist(
1232        &self,
1233        clusters: &[String],
1234    ) -> Result<BTreeSet<String>, ConnectionError> {
1235        check_clusters_exist(self.client, clusters).await
1236    }
1237
1238    /// Find sinks that depend on objects in the specified schemas.
1239    pub async fn find_sinks_depending_on_schemas(
1240        &self,
1241        schemas: &[SchemaQualifier],
1242    ) -> Result<Vec<DependentSink>, ConnectionError> {
1243        find_sinks_depending_on_schemas(self.client, schemas).await
1244    }
1245
1246    /// Check if a connection exists in the specified database and schema.
1247    pub async fn check_connection_exists(
1248        &self,
1249        database: &str,
1250        schema: &str,
1251        name: &str,
1252    ) -> Result<bool, ConnectionError> {
1253        check_connection_exists(self.client, database, schema, name).await
1254    }
1255
1256    /// Check if an object (MV, table, source) exists in the specified schema.
1257    pub async fn object_exists(
1258        &self,
1259        database: &str,
1260        schema: &str,
1261        object: &str,
1262    ) -> Result<bool, ConnectionError> {
1263        object_exists(self.client, database, schema, object).await
1264    }
1265
1266    /// Get staging schema names for a specific deployment.
1267    pub async fn get_staging_schemas(
1268        &self,
1269        deploy_id: &str,
1270    ) -> Result<Vec<SchemaQualifier>, ConnectionError> {
1271        get_staging_schemas(self.client, deploy_id).await
1272    }
1273
1274    /// Get staging cluster names for a specific deployment.
1275    pub async fn get_staging_clusters(
1276        &self,
1277        deploy_id: &str,
1278    ) -> Result<Vec<String>, ConnectionError> {
1279        get_staging_clusters(self.client, deploy_id).await
1280    }
1281
1282    /// Drop all objects in a schema.
1283    pub async fn drop_schema_objects(
1284        &self,
1285        database: &str,
1286        schema: &str,
1287    ) -> Result<Vec<String>, ConnectionError> {
1288        drop_schema_objects(self.client, database, schema).await
1289    }
1290
1291    /// Drop specific objects by their ObjectIds.
1292    pub async fn drop_objects(
1293        &self,
1294        objects: &BTreeSet<ObjectId>,
1295    ) -> Result<Vec<String>, ConnectionError> {
1296        drop_objects(self.client, objects).await
1297    }
1298
1299    /// Drop staging schemas by name.
1300    pub async fn drop_staging_schemas(
1301        &self,
1302        schemas: &[SchemaQualifier],
1303    ) -> Result<(), ConnectionError> {
1304        drop_staging_schemas(self.client, schemas).await
1305    }
1306
1307    /// Drop staging clusters by name.
1308    pub async fn drop_staging_clusters(&self, clusters: &[String]) -> Result<(), ConnectionError> {
1309        drop_staging_clusters(self.client, clusters).await
1310    }
1311
1312    /// Check if a schema exists in the specified database.
1313    pub async fn schema_exists(
1314        &self,
1315        database: &str,
1316        schema: &str,
1317    ) -> Result<bool, ConnectionError> {
1318        schema_exists(self.client, database, schema).await
1319    }
1320
1321    /// Check if a network policy exists.
1322    pub async fn network_policy_exists(&self, name: &str) -> Result<bool, ConnectionError> {
1323        network_policy_exists(self.client, name).await
1324    }
1325
1326    /// Check if a role exists.
1327    pub async fn role_exists(&self, name: &str) -> Result<bool, ConnectionError> {
1328        role_exists(self.client, name).await
1329    }
1330
1331    /// Get the members granted to a role.
1332    pub async fn get_role_members(&self, name: &str) -> Result<Vec<String>, ConnectionError> {
1333        get_role_members(self.client, name).await
1334    }
1335
1336    /// Get session default parameter names for a role.
1337    pub async fn get_role_parameters(&self, name: &str) -> Result<Vec<String>, ConnectionError> {
1338        get_role_parameters(self.client, name).await
1339    }
1340
1341    /// Check if a cluster exists.
1342    pub async fn cluster_exists(&self, name: &str) -> Result<bool, ConnectionError> {
1343        cluster_exists(self.client, name).await
1344    }
1345
1346    /// Get a cluster by name.
1347    pub async fn get_cluster(&self, name: &str) -> Result<Option<Cluster>, ConnectionError> {
1348        get_cluster(self.client, name).await
1349    }
1350
1351    /// List all clusters.
1352    pub async fn list_clusters(&self) -> Result<Vec<Cluster>, ConnectionError> {
1353        list_clusters(self.client).await
1354    }
1355
1356    /// Get cluster configuration including replicas and grants.
1357    pub async fn get_cluster_config(
1358        &self,
1359        name: &str,
1360    ) -> Result<Option<ClusterConfig>, ConnectionError> {
1361        get_cluster_config(self.client, name).await
1362    }
1363
1364    /// Get privilege grants on a cluster by name.
1365    pub async fn get_cluster_grants(
1366        &self,
1367        name: &str,
1368    ) -> Result<Vec<ObjectGrant>, ConnectionError> {
1369        get_cluster_grants(self.client, name).await
1370    }
1371
1372    /// Get privilege grants on a network policy by name.
1373    pub async fn get_network_policy_grants(
1374        &self,
1375        name: &str,
1376    ) -> Result<Vec<ObjectGrant>, ConnectionError> {
1377        get_network_policy_grants(self.client, name).await
1378    }
1379
1380    /// Get privilege grants on a database object.
1381    pub async fn get_database_object_grants(
1382        &self,
1383        catalog_table: &str,
1384        database: &str,
1385        schema: &str,
1386        name: &str,
1387    ) -> Result<Vec<ObjectGrant>, ConnectionError> {
1388        get_database_object_grants(self.client, catalog_table, database, schema, name).await
1389    }
1390
1391    /// Get the `CREATE CONNECTION` SQL for an existing connection.
1392    pub async fn get_connection_create_sql(
1393        &self,
1394        database: &str,
1395        schema: &str,
1396        name: &str,
1397    ) -> Result<Option<String>, ConnectionError> {
1398        get_connection_create_sql(self.client, database, schema, name).await
1399    }
1400
1401    /// Get default privilege grants for a cluster by name.
1402    pub async fn get_default_privilege_grants_for_cluster(
1403        &self,
1404        name: &str,
1405    ) -> Result<Vec<ObjectGrant>, ConnectionError> {
1406        get_default_privilege_grants_for_cluster(self.client, name).await
1407    }
1408
1409    /// Get default privilege grants for a network policy by name.
1410    pub async fn get_default_privilege_grants_for_network_policy(
1411        &self,
1412        name: &str,
1413    ) -> Result<Vec<ObjectGrant>, ConnectionError> {
1414        get_default_privilege_grants_for_network_policy(self.client, name).await
1415    }
1416
1417    /// Get default privilege grants for a database object.
1418    pub async fn get_default_privilege_grants_for_database_object(
1419        &self,
1420        catalog_table: &str,
1421        database: &str,
1422        schema: &str,
1423        name: &str,
1424        object_type: &str,
1425    ) -> Result<Vec<ObjectGrant>, ConnectionError> {
1426        get_default_privilege_grants_for_database_object(
1427            self.client,
1428            catalog_table,
1429            database,
1430            schema,
1431            name,
1432            object_type,
1433        )
1434        .await
1435    }
1436}