Skip to main content

mz_catalog/builtin/
mz_object_dependencies.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//! The `mz_internal.mz_object_dependencies` materialized view and the
11//! generated view it reads.
12
13use 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
33/// The durable encoding of a `GidMapping` key's `object_type`, as it appears in
34/// `mz_catalog_raw`'s JSON.
35fn object_type_code(object_type: ProtoCatalogItemType) -> String {
36    serde_json::to_string(&object_type).expect("CatalogItemType is serializable")
37}
38
39/// One builtin dependency edge, inlined as a `VALUES` row of
40/// `mz_object_dependencies_raw`.
41#[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    /// Renders the row as a SQL `VALUES` tuple.
53    ///
54    /// The literals are not escaped. `BuiltinEdgeCollector::collect` runs
55    /// every name through `assert_safe_builtin_name` before building a row.
56    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
72/// Collects builtin dependency edges by parsing builtin SQL.
73struct BuiltinEdgeCollector<'a> {
74    /// Builtin name to its schema, for resolving unqualified references.
75    schema_by_name: BTreeMap<&'a str, &'a str>,
76    /// Element type name to the `(schema, name)` of its array type. e.g. `T[]`
77    /// maps to the qualified Postgres compatible type `_T`.
78    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    /// Parses `create_sql` and records one row per catalog item it references.
130    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        // Builtin SQL is name-based. An id reference here means somebody
147        // hardcoded a system id, which is not stable across versions.
148        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        // Map the array type `T[]` to its Postgres compatible type `_T`.
169        for elem in &refs.named_array_elements {
170            // `T[]` references the array type paired with `T`, not `T` itself.
171            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
204/// Renders the `mz_object_dependencies_raw` view body with `rows` inlined as
205/// the builtin edge table.
206fn 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    // Every kind a name in relation position can denote.
213    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
318/// Generate the `mz_internal.mz_object_dependencies_raw` builtin view with
319/// builtin dependency edges inlined as VALUES clauses.
320///
321/// The view unions these edge sources:
322///
323/// - User items: references extracted from stored `create_sql` via `parse_catalog_item_references`.
324///   Id references are used directly. Function references are recovered by joining
325///   `(schema, name)` against `GidMapping` rows, which is sound by construction: name resolution
326///   never prints an id for a function (`print_id` is false for `Func`) and a function's
327///   `full_name` always carries its schema, so a function reference persists as exactly
328///   `"schema"."name"`. Temporary items are excluded.
329/// - Builtin items: name references joined to `GidMapping` at query time.
330/// - Introspection source indexes: `si<id> -> s<log id>` edges from
331///   `ClusterIntrospectionSourceIndex` entries.
332pub(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            // The user_items branch already covers runtime alterable connections, thus we skip as
342            // to not double-count.
343            Builtin::Connection(c) if c.runtime_alterable => continue,
344            Builtin::Connection(c) => (ProtoCatalogItemType::Connection, c.sql.to_string()),
345            // Skip objects constructed without SQL.
346            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    // Append `mz_object_dependencies_raw`'s own outgoing edges to its VALUES
356    // rows.
357    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    });