Skip to main content

uqa_sql/routines/lifecycle/
dependencies.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Stored routine/schema references and atomic registry removal candidates.
8
9use super::{RoutineDropResolution, RoutineDropTarget, RoutineRegistry, RoutineSchemaDependents};
10use crate::{
11    ast::{CreateFunction, FunctionBody},
12    routines::{routine_signature_types, SQLUserFunction},
13    SQLError,
14};
15use std::{collections::BTreeMap, sync::Arc};
16
17pub fn append_schema_function_dependents(
18    table_name: &str,
19    columns: &[crate::ast::ColumnDef],
20    checks: &[crate::ast::TableCheck],
21    target: &crate::ast::FunctionBinding,
22    foreign: bool,
23    dependents: &mut RoutineSchemaDependents,
24) -> Result<(), SQLError> {
25    let relation = if foreign {
26        format!("foreign table `{table_name}`")
27    } else {
28        format!("`{table_name}`")
29    };
30    for column in columns {
31        if let Some(generated) = &column.generated {
32            let referenced =
33                generated.function_dependencies.iter().any(|dependency| {
34                    crate::routines::function_binding_matches(dependency, target)
35                }) || crate::catalog::stored_ast::expression_references_routine_identity(
36                    &generated.expression,
37                    target,
38                )?;
39            if referenced {
40                dependents
41                    .columns
42                    .push((table_name.to_string(), column.name.clone(), foreign));
43            }
44        }
45        if let Some(default) = &column.default {
46            if crate::catalog::stored_ast::expression_references_routine_identity(default, target)?
47            {
48                dependents
49                    .defaults
50                    .push((table_name.to_string(), column.name.clone(), foreign));
51            }
52        }
53        if let Some(check) = &column.check {
54            if crate::catalog::stored_ast::expression_references_routine_identity(check, target)? {
55                let name = column.check_name.clone().ok_or_else(|| {
56                    SQLError::Internal(format!(
57                        "CHECK constraint on {relation}.`{}` has no catalog name",
58                        column.name
59                    ))
60                })?;
61                dependents
62                    .checks
63                    .push((table_name.to_string(), name, foreign));
64            }
65        }
66    }
67    for check in checks {
68        if crate::catalog::stored_ast::expression_references_routine_identity(&check.expr, target)?
69        {
70            let name = check.name.clone().ok_or_else(|| {
71                SQLError::Internal(format!(
72                    "table CHECK constraint on {relation} has no catalog name"
73                ))
74            })?;
75            dependents
76                .checks
77                .push((table_name.to_string(), name, foreign));
78        }
79    }
80    Ok(())
81}
82
83pub fn stored_routine_dependents(
84    registry: &BTreeMap<String, Vec<Arc<SQLUserFunction>>>,
85    target: &RoutineDropTarget,
86) -> Result<Vec<RoutineDropTarget>, SQLError> {
87    let binding = target.binding();
88    let mut dependents = Vec::new();
89    for (name, overloads) in registry {
90        for function in overloads {
91            if routine_definition_references(&function.def, &binding)? {
92                dependents.push(RoutineDropTarget {
93                    object_id: function.def.object_id,
94                    name: name.clone(),
95                    argument_types: routine_signature_types(&function.def),
96                    is_procedure: function.def.is_procedure,
97                });
98            }
99        }
100    }
101    dependents.retain(|dependent| dependent != target);
102    dependents.sort();
103    dependents.dedup();
104    Ok(dependents)
105}
106
107pub fn routine_definition_references(
108    def: &CreateFunction,
109    target: &crate::ast::FunctionBinding,
110) -> Result<bool, SQLError> {
111    for default in def
112        .params
113        .iter()
114        .filter_map(|parameter| parameter.default.as_ref())
115    {
116        if crate::catalog::stored_ast::expression_references_routine_identity(default, target)? {
117            return Ok(true);
118        }
119    }
120    let FunctionBody::Statements(statements) = &def.body else {
121        return Ok(false);
122    };
123    for statement in statements {
124        if crate::catalog::stored_ast::statement_references_routine_identity(statement, target)? {
125            return Ok(true);
126        }
127    }
128    Ok(false)
129}
130
131pub fn expand_stored_routine_drop_dependents(
132    registry: &BTreeMap<String, Vec<Arc<SQLUserFunction>>>,
133    cascade: bool,
134    resolution: &mut RoutineDropResolution,
135    display_label: &dyn Fn(&RoutineDropTarget) -> Result<String, SQLError>,
136) -> Result<Vec<RoutineDropTarget>, SQLError> {
137    let explicit_targets = resolution.seen_targets.clone();
138    let mut cascaded_routines = Vec::new();
139    let mut target_index = 0;
140    while target_index < resolution.targets.len() {
141        let target = resolution.targets[target_index].clone();
142        target_index += 1;
143        if target.is_procedure {
144            continue;
145        }
146        for dependent in stored_routine_dependents(registry, &target)? {
147            if explicit_targets.contains(&dependent) || resolution.seen_targets.contains(&dependent)
148            {
149                continue;
150            }
151            if !cascade {
152                return Err(SQLError::Routine {
153                    sqlstate: "2BP01".into(),
154                    message: format!(
155                        "cannot drop function {} because other objects depend on it",
156                        display_label(&target)?
157                    ),
158                });
159            }
160            resolution.seen_targets.insert(dependent.clone());
161            cascaded_routines.push(dependent.clone());
162            resolution.targets.push(dependent);
163        }
164    }
165    Ok(cascaded_routines)
166}
167
168pub fn remove_routine_registry_targets(
169    next: &mut RoutineRegistry,
170    targets: &[RoutineDropTarget],
171) -> Result<(), SQLError> {
172    // Revalidate every target before mutating `next`. This retains a concurrently registered unrelated overload and keeps a multi-target DROP all-or-nothing if any preflighted identity has disappeared.
173    for target in targets {
174        let overloads = next.get(&target.name).ok_or_else(|| {
175            SQLError::Internal(format!(
176                "resolved {} registry entry `{}` disappeared before DROP",
177                target.kind(),
178                target.name
179            ))
180        })?;
181        if !overloads.iter().any(|function| {
182            function.def.is_procedure == target.is_procedure
183                && routine_signature_types(&function.def) == target.argument_types
184        }) {
185            return Err(SQLError::Internal(format!(
186                "resolved {} {} disappeared before DROP",
187                target.kind(),
188                target.label()
189            )));
190        }
191    }
192
193    for target in targets.iter().rev() {
194        let overloads = next.get_mut(&target.name).ok_or_else(|| {
195            SQLError::Internal(format!(
196                "resolved {} registry entry `{}` disappeared while applying DROP",
197                target.kind(),
198                target.name
199            ))
200        })?;
201        let position = overloads
202            .iter()
203            .position(|function| {
204                function.def.is_procedure == target.is_procedure
205                    && routine_signature_types(&function.def) == target.argument_types
206            })
207            .ok_or_else(|| {
208                SQLError::Internal(format!(
209                    "resolved {} {} disappeared while applying DROP",
210                    target.kind(),
211                    target.label()
212                ))
213            })?;
214        overloads.remove(position);
215        if overloads.is_empty() {
216            next.remove(&target.name);
217        }
218    }
219    Ok(())
220}