Skip to main content

uqa_sql/schema/domains/
dependencies.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Domain dependency analysis over declared types, stored syntax, and immutable catalog definitions.
8
9use crate::{
10    ast::{
11        ColumnDef, ColumnType, CreateFunction, Expr, FunctionBody, FunctionReturns, IndexKey,
12        TableCheck,
13    },
14    catalog::{domain::StoredDomain, index::IndexDefinition, stored_view::StoredView},
15    ir::ScalarExpr,
16    SQLError,
17};
18use std::collections::{BTreeMap, BTreeSet};
19use uqa_core::RelationIdentity;
20
21pub trait DomainTypeCatalog {
22    fn resolve_domain_type_reference(&self, name: &str) -> Option<ColumnType>;
23}
24
25#[derive(Default)]
26pub struct DomainDependents {
27    pub indexes: BTreeSet<RelationIdentity>,
28    pub columns: BTreeSet<(String, String, bool)>,
29    pub defaults: BTreeSet<(String, String, bool)>,
30    pub checks: BTreeSet<(String, String, bool)>,
31}
32
33pub fn references_domain(ty: &ColumnType, targets: &BTreeSet<u32>) -> bool {
34    match ty {
35        ColumnType::Domain { oid, base, .. } => {
36            targets.contains(oid) || references_domain(base, targets)
37        }
38        ColumnType::Array(element) => references_domain(element, targets),
39        _ => false,
40    }
41}
42
43pub fn type_name_references_domain(
44    types: &dyn DomainTypeCatalog,
45    name: &str,
46    targets: &BTreeSet<u32>,
47) -> bool {
48    types
49        .resolve_domain_type_reference(name)
50        .is_some_and(|ty| references_domain(&ty, targets))
51}
52
53pub fn expression_references_domain(
54    types: &dyn DomainTypeCatalog,
55    expression: &Expr,
56    targets: &BTreeSet<u32>,
57) -> Result<bool, SQLError> {
58    Ok(
59        crate::catalog::stored_ast::stored_expression_type_names(expression)?
60            .iter()
61            .any(|name| type_name_references_domain(types, name, targets)),
62    )
63}
64
65pub fn routine_references_domain(
66    types: &dyn DomainTypeCatalog,
67    definition: &CreateFunction,
68    targets: &BTreeSet<u32>,
69) -> Result<bool, SQLError> {
70    for param in &definition.params {
71        if type_name_references_domain(types, &param.type_name, targets) {
72            return Ok(true);
73        }
74        if let Some(default) = &param.default {
75            if expression_references_domain(types, default, targets)? {
76                return Ok(true);
77            }
78        }
79    }
80    if let FunctionReturns::Scalar { type_name } | FunctionReturns::SetOf { type_name } =
81        &definition.returns
82    {
83        if type_name_references_domain(types, type_name, targets) {
84            return Ok(true);
85        }
86    }
87    if let FunctionBody::Statements(statements) = &definition.body {
88        for statement in statements {
89            let mut merge_assignment_depends = false;
90            crate::catalog::stored_ast::visit_stored_statement_merges(
91                &mut statement.clone(),
92                &mut |merge| {
93                    merge_assignment_depends |= merge
94                        .target_column_bindings
95                        .values()
96                        .any(|binding| !binding.domain_dependencies.is_disjoint(targets));
97                    Ok(())
98                },
99            )?;
100            if merge_assignment_depends {
101                return Ok(true);
102            }
103            if crate::catalog::stored_ast::stored_statement_type_names(statement)?
104                .iter()
105                .any(|name| type_name_references_domain(types, name, targets))
106            {
107                return Ok(true);
108            }
109        }
110    }
111    Ok(false)
112}
113
114pub fn expand_domain_drop_targets(
115    types: &dyn DomainTypeCatalog,
116    registry: &BTreeMap<String, StoredDomain>,
117    targets: &mut BTreeSet<u32>,
118    routines: &[crate::ast::FunctionBinding],
119) -> Result<(), SQLError> {
120    loop {
121        let previous = targets.len();
122        for domain in registry.values() {
123            let mut depends = references_domain(&domain.definition.base, targets);
124            if let Some(default) = &domain.definition.default {
125                depends |= expression_references_domain(types, default, targets)?;
126                for routine in routines {
127                    depends |= crate::catalog::stored_ast::expression_references_routine_identity(
128                        default, routine,
129                    )?;
130                }
131            }
132            if depends {
133                targets.insert(domain.oid);
134            }
135        }
136        if previous == targets.len() {
137            break;
138        }
139    }
140    Ok(())
141}
142
143pub fn domain_checks_depending_on_routines(
144    registry: BTreeMap<String, StoredDomain>,
145    routines: &[crate::ast::FunctionBinding],
146) -> Result<Vec<(String, String)>, SQLError> {
147    let mut checks = BTreeSet::new();
148    for (name, domain) in registry {
149        for check in &domain.definition.checks {
150            for routine in routines {
151                if crate::catalog::stored_ast::expression_references_routine_identity(
152                    &check.expression,
153                    routine,
154                )? {
155                    checks.insert((
156                        name.clone(),
157                        check
158                            .name
159                            .clone()
160                            .ok_or_else(|| SQLError::Internal("domain CHECK has no name".into()))?,
161                    ));
162                }
163            }
164        }
165    }
166    Ok(checks.into_iter().collect())
167}
168
169pub fn domain_schema_dependents(
170    types: &dyn DomainTypeCatalog,
171    table: &str,
172    columns: &[ColumnDef],
173    checks: &[TableCheck],
174    foreign: bool,
175    targets: &BTreeSet<u32>,
176    dependents: &mut DomainDependents,
177) -> Result<(), SQLError> {
178    for column in columns {
179        let target = (table.to_string(), column.name.clone(), foreign);
180        let mut drop_column = references_domain(&column.ty, targets);
181        if let Some(generated) = &column.generated {
182            drop_column |= expression_references_domain(types, &generated.expression, targets)?;
183        }
184        if drop_column {
185            dependents.columns.insert(target.clone());
186        }
187        if let Some(default) = &column.default {
188            if expression_references_domain(types, default, targets)? {
189                dependents.defaults.insert(target);
190            }
191        }
192        if let Some(check) = &column.check {
193            if expression_references_domain(types, check, targets)? {
194                let name = column.check_name.clone().ok_or_else(|| {
195                    SQLError::Internal("domain dependent CHECK has no name".into())
196                })?;
197                dependents.checks.insert((table.to_string(), name, foreign));
198            }
199        }
200    }
201    for check in checks {
202        if expression_references_domain(types, &check.expr, targets)? {
203            let name = check
204                .name
205                .clone()
206                .ok_or_else(|| SQLError::Internal("domain dependent CHECK has no name".into()))?;
207            dependents.checks.insert((table.to_string(), name, foreign));
208        }
209    }
210    loop {
211        let previous = dependents.columns.len();
212        let removed = dependents
213            .columns
214            .iter()
215            .filter(|(name, _, _)| name == table)
216            .map(|(_, column, _)| column.clone())
217            .collect::<Vec<_>>();
218        for column in columns {
219            let Some(generated) = &column.generated else {
220                continue;
221            };
222            for removed in &removed {
223                if crate::schema::dependencies::schema_expr_references_column(
224                    &generated.expression,
225                    removed,
226                ) {
227                    dependents
228                        .columns
229                        .insert((table.to_string(), column.name.clone(), foreign));
230                }
231            }
232        }
233        if previous == dependents.columns.len() {
234            break;
235        }
236    }
237    Ok(())
238}
239
240pub fn views_referencing_domains(
241    types: &dyn DomainTypeCatalog,
242    registry: BTreeMap<RelationIdentity, StoredView>,
243    targets: &BTreeSet<u32>,
244) -> BTreeSet<String> {
245    let mut views = BTreeSet::new();
246    for (identity, mut view) in registry {
247        let mut depends = false;
248        view.query.rewrite_scalar_expressions(&mut |expression| {
249            if let ScalarExpr::Cast { ty, .. } | ScalarExpr::TypedLiteral { ty, .. } = expression {
250                depends |= type_name_references_domain(types, ty, targets);
251            }
252        });
253        if depends {
254            views.insert(identity.qualified_name());
255        }
256    }
257    views
258}
259
260pub fn remove_domain_references(
261    types: &dyn DomainTypeCatalog,
262    registry: &mut BTreeMap<String, StoredDomain>,
263    targets: &BTreeSet<u32>,
264) -> Result<(), SQLError> {
265    registry.retain(|_, domain| !targets.contains(&domain.oid));
266    for domain in registry.values_mut() {
267        if let Some(default) = &domain.definition.default {
268            if expression_references_domain(types, default, targets)? {
269                domain.definition.default = None;
270            }
271        }
272        let mut checks = Vec::new();
273        for check in &domain.definition.checks {
274            if !expression_references_domain(types, &check.expression, targets)? {
275                checks.push(check.clone());
276            }
277        }
278        domain.definition.checks = checks;
279    }
280    Ok(())
281}
282
283pub fn remove_domain_routine_checks(
284    registry: &mut BTreeMap<String, StoredDomain>,
285    checks: Vec<(String, String)>,
286) -> Result<(), SQLError> {
287    for (domain, name) in checks {
288        let domain = registry
289            .get_mut(&domain)
290            .ok_or_else(|| SQLError::Internal("dependent domain disappeared".into()))?;
291        domain
292            .definition
293            .checks
294            .retain(|check| check.name.as_deref() != Some(&name));
295    }
296    Ok(())
297}
298
299pub fn parse_domain_index_keys(keys: &str) -> Result<Vec<IndexKey>, SQLError> {
300    serde_json::from_str(keys).map_err(|error| SQLError::Internal(error.to_string()))
301}
302
303pub fn index_references_domain(
304    types: &dyn DomainTypeCatalog,
305    definition: &IndexDefinition,
306    keys: &[IndexKey],
307    targets: &BTreeSet<u32>,
308) -> Result<bool, SQLError> {
309    let mut depends = definition
310        .key_types
311        .iter()
312        .any(|ty| references_domain(ty, targets));
313    for expression in keys
314        .iter()
315        .filter_map(|key| match key {
316            IndexKey::Expression(expression) => Some(expression.as_ref()),
317            IndexKey::Column(_) => None,
318        })
319        .chain(definition.predicate.as_deref())
320    {
321        depends |= expression_references_domain(types, expression, targets)?;
322    }
323    Ok(depends)
324}