Skip to main content

uqa_sql/routines/lifecycle/
relations.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Stored relation, sequence OID, column, and namespace references in routine definitions.
8
9use super::{RoutineDropResolution, RoutineDropTarget, RoutineRegistry};
10use crate::{
11    ast::{CreateFunction, Expr, FunctionBody},
12    routines::routine_signature_types,
13    SQLError,
14};
15use std::collections::BTreeSet;
16use uqa_core::Value;
17
18pub trait RoutineRelationOids {
19    fn bound_regclass_oid(&self, name: &str) -> Result<Option<i64>, SQLError>;
20}
21
22pub fn is_regclass(name: &str) -> bool {
23    name.eq_ignore_ascii_case("regclass") || name.eq_ignore_ascii_case("pg_catalog.regclass")
24}
25
26pub fn regclass_oid(expression: &Expr) -> Option<i64> {
27    match expression {
28        Expr::TypedLiteral {
29            value: Value::Int(oid),
30            ty,
31        } if is_regclass(ty) => Some(*oid),
32        Expr::Cast { expr, ty } if is_regclass(ty) => regclass_oid(expr),
33        _ => None,
34    }
35}
36
37pub fn stored_routine_references_columns(
38    catalog: crate::binding::stored_columns::StoredColumnBindingContext<'_>,
39    definition: &CreateFunction,
40    columns: &BTreeSet<(String, String)>,
41) -> Result<bool, SQLError> {
42    let FunctionBody::Statements(statements) = &definition.body else {
43        return Ok(false);
44    };
45    if columns.is_empty() {
46        return Ok(false);
47    }
48    for statement in statements {
49        let dependencies = crate::binding::stored_columns::stored_statement_column_dependencies(
50            catalog, statement,
51        )?;
52        if dependencies.iter().any(|dependency| {
53            columns.contains(&(
54                dependency.relation.qualified_name(),
55                dependency.column.clone(),
56            ))
57        }) {
58            return Ok(true);
59        }
60    }
61    Ok(false)
62}
63
64pub fn stored_routine_references_relations(
65    catalog: &dyn RoutineRelationOids,
66    definition: &CreateFunction,
67    relations: &BTreeSet<String>,
68) -> Result<bool, SQLError> {
69    if relations.is_empty() {
70        return Ok(false);
71    }
72    let mut oids = BTreeSet::new();
73    for relation in relations {
74        if let Some(oid) = catalog.bound_regclass_oid(relation)? {
75            oids.insert(oid);
76        }
77    }
78    let mut depends = false;
79    let mut visit = |expression: &mut crate::ast::Expr| {
80        depends |= regclass_oid(expression).is_some_and(|oid| oids.contains(&oid));
81        Ok(())
82    };
83    for default in definition
84        .params
85        .iter()
86        .filter_map(|parameter| parameter.default.as_ref())
87    {
88        crate::catalog::stored_ast::visit_stored_expression(&mut default.clone(), &mut visit)?;
89    }
90    if let FunctionBody::Statements(statements) = &definition.body {
91        for statement in statements {
92            if crate::catalog::stored_ast::stored_statement_relation_names(statement)?
93                .iter()
94                .any(|name| relations.contains(name))
95            {
96                return Ok(true);
97            }
98            crate::catalog::stored_ast::visit_stored_statement_expressions(
99                &mut statement.clone(),
100                &mut visit,
101            )?;
102        }
103    }
104    Ok(depends)
105}
106
107pub fn schema_routine_drop_targets(
108    registry: &RoutineRegistry,
109    schemas: &BTreeSet<String>,
110) -> Result<RoutineDropResolution, SQLError> {
111    let mut resolution = RoutineDropResolution {
112        targets: Vec::new(),
113        seen_targets: BTreeSet::new(),
114        notices: Vec::new(),
115    };
116    for (name, overloads) in registry {
117        for function in overloads {
118            let identity =
119                uqa_core::RelationIdentity::from_legacy_name(name).map_err(SQLError::Internal)?;
120            let mut depends = schemas.contains(&identity.schema);
121            if let crate::ast::FunctionBody::Statements(statements) = &function.def.body {
122                for statement in statements {
123                    for relation in
124                        crate::catalog::stored_ast::stored_statement_relation_names(statement)?
125                    {
126                        let relation = uqa_core::RelationIdentity::from_legacy_name(&relation)
127                            .map_err(SQLError::Internal)?;
128                        depends |= schemas.contains(&relation.schema);
129                    }
130                }
131            }
132            if depends {
133                let target = RoutineDropTarget {
134                    object_id: function.def.object_id,
135                    name: name.clone(),
136                    argument_types: routine_signature_types(&function.def),
137                    is_procedure: function.def.is_procedure,
138                };
139                resolution.seen_targets.insert(target.clone());
140                resolution.targets.push(target);
141            }
142        }
143    }
144    Ok(resolution)
145}