1use 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#[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
44pub(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
64pub(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
77fn 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
96async 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
113pub(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
149pub(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
183pub(super) async fn get_cluster_config(
190 client: &Client,
191 name: &str,
192) -> Result<Option<ClusterConfig>, ConnectionError> {
193 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 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 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 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 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
293pub(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
309pub(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
322pub(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
341pub(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
359pub(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
366pub(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 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, ¶ms).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
420pub(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, ¶ms).await?;
448
449 Ok(rows.iter().map(|row| row.get("name")).collect())
450}
451
452pub(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, ¶ms).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
495async 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 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, ¶ms).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
543pub(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
553pub(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
563pub(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
573pub(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
583pub(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
594pub(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 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 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, ¶ms).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
669pub(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
691pub(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
717pub(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
743pub(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
761fn 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
773pub(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
817pub(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, ¶ms).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
878pub(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
895pub(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
911async 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
955pub(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
963pub(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
971pub(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
1015async 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
1064pub(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
1072pub(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
1080pub(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
1137pub(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 pub async fn get_current_user(&self) -> Result<String, ConnectionError> {
1162 get_current_user(self.client).await
1163 }
1164
1165 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn drop_staging_clusters(&self, clusters: &[String]) -> Result<(), ConnectionError> {
1309 drop_staging_clusters(self.client, clusters).await
1310 }
1311
1312 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 pub async fn network_policy_exists(&self, name: &str) -> Result<bool, ConnectionError> {
1323 network_policy_exists(self.client, name).await
1324 }
1325
1326 pub async fn role_exists(&self, name: &str) -> Result<bool, ConnectionError> {
1328 role_exists(self.client, name).await
1329 }
1330
1331 pub async fn get_role_members(&self, name: &str) -> Result<Vec<String>, ConnectionError> {
1333 get_role_members(self.client, name).await
1334 }
1335
1336 pub async fn get_role_parameters(&self, name: &str) -> Result<Vec<String>, ConnectionError> {
1338 get_role_parameters(self.client, name).await
1339 }
1340
1341 pub async fn cluster_exists(&self, name: &str) -> Result<bool, ConnectionError> {
1343 cluster_exists(self.client, name).await
1344 }
1345
1346 pub async fn get_cluster(&self, name: &str) -> Result<Option<Cluster>, ConnectionError> {
1348 get_cluster(self.client, name).await
1349 }
1350
1351 pub async fn list_clusters(&self) -> Result<Vec<Cluster>, ConnectionError> {
1353 list_clusters(self.client).await
1354 }
1355
1356 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 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 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 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 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 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 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 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}