1use std::collections::{BTreeMap, BTreeSet};
14use std::sync::LazyLock;
15
16use itertools::Itertools;
17use mz_catalog_protos::objects::CatalogItemType as ProtoCatalogItemType;
18use mz_ore::collections::CollectionExt;
19use mz_pgrepr::oid;
20use mz_repr::namespaces::MZ_INTERNAL_SCHEMA;
21use mz_repr::{RelationDesc, SemanticType, SqlScalarType};
22use mz_sql::catalog::{CatalogType, NameReference};
23use mz_sql_parser::ast::UnresolvedItemName;
24use mz_sql_parser::ast::item_refs::collect_item_references;
25
26use super::{
27 Builtin, BuiltinMaterializedView, BuiltinView, LinkProperties, Ontology, OntologyLink,
28 PUBLIC_SELECT, assert_safe_builtin_name,
29};
30
31pub(super) const MZ_OBJECT_DEPENDENCIES_RAW: &str = "mz_object_dependencies_raw";
32
33fn object_type_code(object_type: ProtoCatalogItemType) -> String {
36 serde_json::to_string(&object_type).expect("CatalogItemType is serializable")
37}
38
39#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
42struct BuiltinEdgeRow {
43 object_schema: String,
44 object_name: String,
45 object_type: String,
46 ref_schema: String,
47 ref_name: String,
48 ref_kind: &'static str,
49}
50
51impl BuiltinEdgeRow {
52 fn to_sql_row(&self) -> String {
57 let BuiltinEdgeRow {
58 object_schema,
59 object_name,
60 object_type,
61 ref_schema,
62 ref_name,
63 ref_kind,
64 } = self;
65 format!(
66 "('{object_schema}', '{object_name}', '{object_type}', \
67 '{ref_schema}', '{ref_name}', '{ref_kind}')"
68 )
69 }
70}
71
72struct BuiltinEdgeCollector<'a> {
74 schema_by_name: BTreeMap<&'a str, &'a str>,
76 array_type_by_elem: BTreeMap<&'a str, (&'a str, &'a str)>,
79 rows: BTreeSet<BuiltinEdgeRow>,
80}
81
82impl<'a> BuiltinEdgeCollector<'a> {
83 fn new(builtins: &'a [Builtin<NameReference>]) -> Self {
84 let mut schema_by_name: BTreeMap<&str, &str> = BTreeMap::new();
85 for b in builtins {
86 if let Some(prev) = schema_by_name.insert(b.name(), b.schema()) {
87 assert_eq!(
88 prev,
89 b.schema(),
90 "builtin name {} appears in multiple schemas; unqualified references are ambiguous",
91 b.name()
92 );
93 }
94 }
95
96 let mut array_type_by_elem: BTreeMap<&str, (&str, &str)> = BTreeMap::new();
97 for b in builtins {
98 if let Builtin::Type(t) = b {
99 if let CatalogType::Array { element_reference } = &t.details.typ {
100 array_type_by_elem.insert(element_reference, (t.schema, t.name));
101 }
102 }
103 }
104
105 BuiltinEdgeCollector {
106 schema_by_name,
107 array_type_by_elem,
108 rows: BTreeSet::new(),
109 }
110 }
111
112 fn resolve(&self, object: &str, name: &UnresolvedItemName) -> (String, String) {
113 match &name.0[..] {
114 [.., schema, item] => (schema.as_str().to_string(), item.as_str().to_string()),
115 [item] => {
116 let item = item.as_str();
117 let schema = self.schema_by_name.get(item).unwrap_or_else(|| {
118 panic!(
119 "cannot resolve unqualified reference {item:?} in builtin {object}; \
120 qualify the reference or check that the referenced builtin exists"
121 )
122 });
123 (schema.to_string(), item.to_string())
124 }
125 [] => panic!("empty item reference in builtin {object}"),
126 }
127 }
128
129 fn collect(
131 &mut self,
132 object_schema: &str,
133 object_name: &str,
134 object_type: ProtoCatalogItemType,
135 create_sql: &str,
136 ) {
137 assert_safe_builtin_name(object_schema, "object schema");
138 assert_safe_builtin_name(object_name, "object");
139
140 let stmt = mz_sql::parse::parse(create_sql)
141 .unwrap_or_else(|e| panic!("invalid sql for builtin {object_name}: {e}"))
142 .into_element()
143 .ast;
144 let refs = collect_item_references(&stmt);
145
146 assert!(
149 refs.ids.is_empty(),
150 "builtin {} references items by id: {:?}",
151 object_name,
152 refs.ids
153 );
154
155 let mut referenced: Vec<(String, String, &'static str)> = Vec::new();
156 for item_name in &refs.named_relations {
157 let (schema, name) = self.resolve(object_name, item_name);
158 referenced.push((schema, name, "rel"));
159 }
160 for item_name in &refs.named_funcs {
161 let (schema, name) = self.resolve(object_name, item_name);
162 referenced.push((schema, name, "func"));
163 }
164 for item_name in &refs.named_types {
165 let (schema, name) = self.resolve(object_name, item_name);
166 referenced.push((schema, name, "type"));
167 }
168 for elem in &refs.named_array_elements {
170 let (_, name) = self.resolve(object_name, elem);
172 let (array_schema, array_name) = self
173 .array_type_by_elem
174 .get(name.as_str())
175 .unwrap_or_else(|| {
176 panic!(
177 "builtin {object_name} uses {name}[], but no builtin array type has \
178 element {name}"
179 )
180 });
181 referenced.push((array_schema.to_string(), array_name.to_string(), "type"));
182 }
183
184 let object_type = object_type_code(object_type);
185 for (ref_schema, ref_name, ref_kind) in referenced {
186 assert_safe_builtin_name(&ref_schema, "referenced schema");
187 assert_safe_builtin_name(&ref_name, "referenced object");
188 assert!(
189 !(object_schema == ref_schema && object_name == ref_name),
190 "builtin {object_schema}.{object_name} references itself"
191 );
192 self.rows.insert(BuiltinEdgeRow {
193 object_schema: object_schema.to_string(),
194 object_name: object_name.to_string(),
195 object_type: object_type.clone(),
196 ref_schema,
197 ref_name,
198 ref_kind,
199 });
200 }
201 }
202}
203
204fn mz_object_dependencies_raw_sql(rows: &BTreeSet<BuiltinEdgeRow>) -> String {
207 let builtin_ref_values = rows.iter().map(BuiltinEdgeRow::to_sql_row).join(",");
208
209 let func_type = object_type_code(ProtoCatalogItemType::Func);
210 let type_type = object_type_code(ProtoCatalogItemType::Type);
211 let source_type = object_type_code(ProtoCatalogItemType::Source);
212 let relation_types = format!("NOT IN ('{type_type}', '{func_type}')");
214
215 format!(
216 "
217WITH
218 user_items AS (
219 SELECT
220 mz_internal.parse_catalog_id(data->'key'->'gid') AS id,
221 mz_internal.parse_catalog_item_references(data->'value'->'definition'->'V1'->>'create_sql') AS refs
222 FROM mz_internal.mz_catalog_raw
223 WHERE
224 data->>'kind' = 'Item' AND
225 -- Exclude temporary objects
226 data->'value'->>'ephemeral_owner_session' IS NULL
227 ),
228 gid_mappings AS (
229 SELECT
230 's' || (data->'value'->>'catalog_id') AS id,
231 data->'key'->>'schema_name' AS schema_name,
232 data->'key'->>'object_name' AS object_name,
233 data->'key'->>'object_type' AS object_type
234 FROM mz_internal.mz_catalog_raw
235 WHERE data->>'kind' = 'GidMapping'
236 ),
237 user_id_edges AS (
238 SELECT u.id AS object_id, r.ref AS referenced_object_id
239 FROM user_items u
240 CROSS JOIN LATERAL jsonb_array_elements_text(u.refs->'ids') AS r(ref)
241 ),
242 user_func_edges AS (
243 SELECT u.id AS object_id, gm.id AS referenced_object_id
244 FROM user_items u
245 CROSS JOIN LATERAL jsonb_array_elements(u.refs->'named_funcs') AS f(func)
246 JOIN gid_mappings gm ON
247 gm.object_type = '{func_type}' AND
248 gm.schema_name = f.func->>'schema' AND
249 gm.object_name = f.func->>'name'
250 ),
251 user_type_edges AS (
252 SELECT u.id AS object_id, gm.id AS referenced_object_id
253 FROM user_items u
254 CROSS JOIN LATERAL jsonb_array_elements(u.refs->'named_types') AS t(typ)
255 JOIN gid_mappings gm ON
256 gm.object_type = '{type_type}' AND
257 gm.schema_name = t.typ->>'schema' AND
258 gm.object_name = t.typ->>'name'
259 ),
260 user_relation_edges AS (
261 SELECT u.id AS object_id, gm.id AS referenced_object_id
262 FROM user_items u
263 CROSS JOIN LATERAL jsonb_array_elements(u.refs->'named_relations') AS n(rel)
264 JOIN gid_mappings gm ON
265 gm.object_type {relation_types} AND
266 gm.schema_name = n.rel->>'schema' AND
267 gm.object_name = n.rel->>'name'
268 ),
269 builtin_edges AS (
270 SELECT obj.id AS object_id, ref.id AS referenced_object_id
271 FROM
272 (VALUES {builtin_ref_values})
273 AS bv(object_schema, object_name, object_type, ref_schema, ref_name, ref_kind)
274 JOIN gid_mappings obj ON
275 obj.schema_name = bv.object_schema AND
276 obj.object_name = bv.object_name AND
277 obj.object_type = bv.object_type
278 JOIN gid_mappings ref ON
279 ref.schema_name = bv.ref_schema AND
280 ref.object_name = bv.ref_name AND
281 CASE bv.ref_kind
282 WHEN 'func' THEN ref.object_type = '{func_type}'
283 WHEN 'type' THEN ref.object_type = '{type_type}'
284 ELSE ref.object_type {relation_types}
285 END
286 ),
287 introspection_source_index_edges AS (
288 SELECT
289 'si' || (isi.data->'value'->>'catalog_id') AS object_id,
290 's' || (gm.data->'value'->>'catalog_id') AS referenced_object_id
291 FROM mz_internal.mz_catalog_raw AS isi
292 JOIN mz_internal.mz_catalog_raw AS gm ON
293 gm.data->>'kind' = 'GidMapping' AND
294 gm.data->'key'->>'object_type' = '{source_type}' AND
295 gm.data->'key'->>'schema_name' = 'mz_introspection' AND
296 gm.data->'key'->>'object_name' = isi.data->'key'->>'name'
297 WHERE isi.data->>'kind' = 'ClusterIntrospectionSourceIndex'
298 )
299-- UNION rather than UNION ALL: the branches are disjoint for the most part, but a
300-- reference can appear both by id and by name in one statement (a table function
301-- spelled `FROM [sNN AS pg_catalog.generate_series](...)` next to a plain call to
302-- the same function, which resolves by name)
303SELECT object_id, referenced_object_id FROM user_id_edges
304UNION
305SELECT object_id, referenced_object_id FROM user_func_edges
306UNION
307SELECT object_id, referenced_object_id FROM user_type_edges
308UNION
309SELECT object_id, referenced_object_id FROM user_relation_edges
310UNION
311SELECT object_id, referenced_object_id FROM builtin_edges
312UNION
313SELECT object_id, referenced_object_id FROM introspection_source_index_edges
314"
315 )
316}
317
318pub(super) fn make_mz_object_dependencies_raw(builtins: &[Builtin<NameReference>]) -> BuiltinView {
333 let mut collector = BuiltinEdgeCollector::new(builtins);
334 for b in builtins {
335 let (object_type, create_sql) = match b {
336 Builtin::View(v) => (ProtoCatalogItemType::View, v.create_sql()),
337 Builtin::MaterializedView(mv) => {
338 (ProtoCatalogItemType::MaterializedView, mv.create_sql())
339 }
340 Builtin::Index(i) => (ProtoCatalogItemType::Index, i.create_sql()),
341 Builtin::Connection(c) if c.runtime_alterable => continue,
344 Builtin::Connection(c) => (ProtoCatalogItemType::Connection, c.sql.to_string()),
345 Builtin::Log(_)
347 | Builtin::Table(_)
348 | Builtin::Type(_)
349 | Builtin::Func(_)
350 | Builtin::Source(_) => continue,
351 };
352 collector.collect(b.schema(), b.name(), object_type, &create_sql);
353 }
354
355 let self_edges = |sql: &str| {
358 let mut collector = BuiltinEdgeCollector::new(builtins);
359 collector.collect(
360 MZ_INTERNAL_SCHEMA,
361 MZ_OBJECT_DEPENDENCIES_RAW,
362 ProtoCatalogItemType::View,
363 &format!("CREATE VIEW {MZ_INTERNAL_SCHEMA}.{MZ_OBJECT_DEPENDENCIES_RAW} AS {sql}"),
364 );
365 collector.rows
366 };
367
368 let self_rows = self_edges(&mz_object_dependencies_raw_sql(&collector.rows));
369 let mut rows = collector.rows;
370 rows.extend(self_rows.iter().cloned());
371 let sql = mz_object_dependencies_raw_sql(&rows);
372
373 BuiltinView {
374 name: MZ_OBJECT_DEPENDENCIES_RAW,
375 schema: MZ_INTERNAL_SCHEMA,
376 oid: oid::VIEW_MZ_OBJECT_DEPENDENCIES_RAW_OID,
377 desc: RelationDesc::builder()
378 .with_column("object_id", SqlScalarType::String.nullable(true))
379 .with_column("referenced_object_id", SqlScalarType::String.nullable(true))
380 .with_key(vec![0, 1])
381 .finish(),
382 column_comments: BTreeMap::from_iter([
383 (
384 "object_id",
385 "The ID of the dependent object. Corresponds to `mz_objects.id`.",
386 ),
387 (
388 "referenced_object_id",
389 "The ID of the referenced object. Corresponds to `mz_objects.id`.",
390 ),
391 ]),
392 sql: Box::leak(sql.into_boxed_str()),
393 access: vec![PUBLIC_SELECT],
394 ontology: None,
395 }
396}
397
398pub static MZ_OBJECT_DEPENDENCIES: LazyLock<BuiltinMaterializedView> =
399 LazyLock::new(|| BuiltinMaterializedView {
400 name: "mz_object_dependencies",
401 schema: MZ_INTERNAL_SCHEMA,
402 oid: oid::MV_MZ_OBJECT_DEPENDENCIES_OID,
403 desc: RelationDesc::builder()
404 .with_column("object_id", SqlScalarType::String.nullable(false))
405 .with_column(
406 "referenced_object_id",
407 SqlScalarType::String.nullable(false),
408 )
409 .with_key(vec![0, 1])
410 .finish(),
411 column_comments: BTreeMap::from_iter([
412 (
413 "object_id",
414 "The ID of the dependent object. Corresponds to `mz_objects.id`.",
415 ),
416 (
417 "referenced_object_id",
418 "The ID of the referenced object. Corresponds to `mz_objects.id`.",
419 ),
420 ]),
421 sql: "
422IN CLUSTER mz_catalog_server
423WITH (
424 ASSERT NOT NULL object_id,
425 ASSERT NOT NULL referenced_object_id
426) AS
427SELECT object_id, referenced_object_id
428FROM mz_internal.mz_object_dependencies_raw",
429 is_retained_metrics_object: true,
430 access: vec![PUBLIC_SELECT],
431 ontology: Some(Ontology {
432 entity_name: "object_dependency",
433 description: "A dependency edge: one object depends on another",
434 links: &const {
435 [
436 OntologyLink {
437 name: "depends_on",
438 target: "object",
439 properties: LinkProperties::DependsOn {
440 source_column: "object_id",
441 target_column: "id",
442 source_id_type: Some(mz_repr::SemanticType::CatalogItemId),
443 requires_mapping: None,
444 },
445 },
446 OntologyLink {
447 name: "dependency_is",
448 target: "object",
449 properties: LinkProperties::DependsOn {
450 source_column: "referenced_object_id",
451 target_column: "id",
452 source_id_type: Some(mz_repr::SemanticType::CatalogItemId),
453 requires_mapping: None,
454 },
455 },
456 ]
457 },
458 column_semantic_types: &const {
459 [
460 ("object_id", SemanticType::CatalogItemId),
461 ("referenced_object_id", SemanticType::CatalogItemId),
462 ]
463 },
464 }),
465 });