Skip to main content

uqa_sql/catalog/events/definition/
dependents.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Bound rule and trigger dependencies observed at each catalog read boundary.
8use super::lookup::EventLookupContext;
9use crate::{
10    catalog::events::{PreparedRuleColumnDrop, RuleColumnDependency, StoredRule},
11    SQLError,
12};
13use std::collections::BTreeMap;
14use uqa_core::RelationIdentity;
15pub struct EventColumnDependencies {
16    pub triggers: Vec<String>,
17    pub rules: Vec<(RelationIdentity, String)>,
18}
19impl EventLookupContext<'_> {
20    pub fn rules_depending_on_relations(
21        &self,
22        relations: &[String],
23    ) -> Result<Vec<(RelationIdentity, String)>, String> {
24        let targets = relations
25            .iter()
26            .map(|relation| RelationIdentity::from_legacy_name(relation))
27            .collect::<Result<std::collections::BTreeSet<_>, String>>()?;
28        let rules = self.registry.read_rules();
29        let mut dependents = Vec::new();
30        for (event_relation, entries) in rules.iter() {
31            if targets.contains(event_relation) {
32                continue;
33            }
34            for rule in entries.values() {
35                let dependencies = rule.dependencies.as_ref().ok_or_else(|| {
36                    format!(
37                        "rule `{}` on `{}` has no bound dependency state",
38                        rule.definition.name,
39                        event_relation.qualified_name()
40                    )
41                })?;
42                if dependencies
43                    .relations
44                    .iter()
45                    .any(|dependency| targets.contains(dependency))
46                {
47                    dependents.push((event_relation.clone(), rule.definition.name.clone()));
48                }
49            }
50        }
51        dependents.sort();
52        Ok(dependents)
53    }
54
55    pub fn rules_depending_on_routine(
56        &self,
57        target: &crate::ast::FunctionBinding,
58    ) -> Result<Vec<(RelationIdentity, String)>, String> {
59        let rules = self.registry.read_rules();
60        let mut dependents = Vec::new();
61        for (event_relation, entries) in rules.iter() {
62            for rule in entries.values() {
63                let dependencies = rule.dependencies.as_ref().ok_or_else(|| {
64                    format!(
65                        "rule `{}` on `{}` has no bound dependency state",
66                        rule.definition.name,
67                        event_relation.qualified_name()
68                    )
69                })?;
70                if dependencies.routines.iter().any(|dependency| {
71                    match (dependency.object_id, target.object_id) {
72                        (Some(dependency), Some(target)) => dependency == target,
73                        (None, None) => {
74                            dependency.name == target.name
75                                && dependency.argument_types == target.argument_types
76                        }
77                        _ => false,
78                    }
79                }) {
80                    dependents.push((event_relation.clone(), rule.definition.name.clone()));
81                }
82            }
83        }
84        dependents.sort();
85        Ok(dependents)
86    }
87
88    pub fn triggers_depending_on_routine(
89        &self,
90        target: &crate::ast::FunctionBinding,
91    ) -> Result<Vec<(String, String)>, SQLError> {
92        let mut dependents = Vec::new();
93        for trigger in self.list_triggers() {
94            let invokes_target = match (trigger.function_object_id, target.object_id) {
95                (Some(trigger), Some(target)) => trigger == target,
96                (None, None) => {
97                    target.argument_types.is_empty() && trigger.definition.function == target.name
98                }
99                _ => false,
100            };
101            let condition_references_target = trigger
102                .definition
103                .when
104                .as_ref()
105                .map(|condition| {
106                    crate::catalog::stored_ast::expression_references_routine_identity(
107                        condition, target,
108                    )
109                })
110                .transpose()?
111                .unwrap_or(false);
112            if invokes_target || condition_references_target {
113                dependents.push((
114                    trigger.definition.table.clone(),
115                    trigger.definition.name.clone(),
116                ));
117            }
118        }
119        dependents.sort();
120        Ok(dependents)
121    }
122
123    pub fn prepare_rule_column_drop(
124        &self,
125        table: &str,
126        column: &str,
127    ) -> Result<PreparedRuleColumnDrop, String> {
128        let relation = RelationIdentity::from_legacy_name(table)?;
129        let dependency = RuleColumnDependency {
130            relation: relation.clone(),
131            column: column.to_string(),
132        };
133        let mut rules = self.registry.read_rules().clone();
134        let mut rebind = std::collections::BTreeSet::new();
135        for (event_relation, entries) in &mut rules {
136            for (name, rule) in entries {
137                let dependencies = rule.dependencies.as_ref().ok_or_else(|| {
138                    format!(
139                        "rule `{}` on `{}` has no bound dependency state",
140                        rule.definition.name,
141                        event_relation.qualified_name()
142                    )
143                })?;
144                if dependencies.columns.contains(&dependency) {
145                    return Err(format!(
146                        "cannot drop column {column} of table {table} because rule {} on {} depends on it",
147                        rule.definition.name,
148                        event_relation.qualified_name()
149                    ));
150                }
151                if event_relation != &relation && !dependencies.relations.contains(&relation) {
152                    continue;
153                }
154                crate::binding::stored_columns::remove_rule_source_column_aliases(
155                    self.analysis.columns,
156                    &mut rule.definition,
157                    &dependency,
158                )
159                .map_err(|error| {
160                    format!(
161                        "reshape rule `{}` source aliases before column drop: {error}",
162                        rule.definition.name
163                    )
164                })?;
165                rebind.insert((event_relation.clone(), name.clone()));
166            }
167        }
168        Ok(PreparedRuleColumnDrop { rules, rebind })
169    }
170
171    pub fn column_event_dependencies(
172        &self,
173        table: &str,
174        column: &str,
175    ) -> Result<EventColumnDependencies, SQLError> {
176        let relation = RelationIdentity::from_legacy_name(table).map_err(|error| {
177            SQLError::Internal(format!("decode trigger relation `{table}`: {error}"))
178        })?;
179        let dependent_triggers = self
180            .registry
181            .read_triggers()
182            .get(&relation)
183            .into_iter()
184            .flat_map(BTreeMap::values)
185            .filter(|trigger| {
186                trigger
187                    .definition
188                    .update_columns
189                    .iter()
190                    .any(|name| name == column)
191                    || trigger.definition.when.as_ref().is_some_and(|condition| {
192                        crate::schema::dependencies::schema_expr_references_column(
193                            condition, column,
194                        )
195                    })
196            })
197            .map(|trigger| trigger.definition.name.clone())
198            .collect::<Vec<_>>();
199        let rules = self.registry.read_rules();
200        let dependent_rules = dependent_rules_for_column(&rules, &relation, column)?;
201        drop(rules);
202        Ok(EventColumnDependencies {
203            triggers: dependent_triggers,
204            rules: dependent_rules,
205        })
206    }
207}
208
209fn dependent_rules_for_column(
210    rules: &BTreeMap<RelationIdentity, BTreeMap<String, StoredRule>>,
211    relation: &RelationIdentity,
212    column: &str,
213) -> Result<Vec<(RelationIdentity, String)>, SQLError> {
214    let dependency = RuleColumnDependency {
215        relation: relation.clone(),
216        column: column.to_string(),
217    };
218    let mut dependent = Vec::new();
219    for (event_relation, entries) in rules {
220        for rule in entries.values() {
221            let dependencies = rule.dependencies.as_ref().ok_or_else(|| {
222                SQLError::Internal(format!(
223                    "rule `{}` on `{}` has no bound dependency state",
224                    rule.definition.name,
225                    event_relation.qualified_name()
226                ))
227            })?;
228            if dependencies.columns.contains(&dependency) {
229                dependent.push((event_relation.clone(), rule.definition.name.clone()));
230            }
231        }
232    }
233    Ok(dependent)
234}