Skip to main content

uqa_sql/catalog/stored_view/
removal.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! View DROP authorization, dependency diagnostics and temporary dependency layers.
8use super::StoredView;
9use crate::{
10    binding::view_dependencies::query_plan_references_relation,
11    catalog::security::view_ownership::{self, ViewOwnershipContext},
12    SQLError,
13};
14use std::collections::BTreeMap;
15use uqa_core::RelationIdentity;
16
17pub fn direct_view_drop_target(target: Option<(String, &str)>) -> Result<Option<String>, SQLError> {
18    match target {
19        Some((canonical, "view")) => Ok(Some(canonical)),
20        Some((canonical, kind)) => Err(SQLError::Unsupported(format!(
21            "DROP VIEW: relation `{canonical}` is a {kind}, not a view"
22        ))),
23        None => Ok(None),
24    }
25}
26
27pub fn ensure_view_drop_authorities(
28    ownership: ViewOwnershipContext<'_>,
29    names: &[String],
30    views: &BTreeMap<RelationIdentity, StoredView>,
31) -> Result<(), SQLError> {
32    for name in names {
33        let relation = RelationIdentity::from_legacy_name(name).map_err(|error| {
34            SQLError::Internal(format!("resolve DROP VIEW target `{name}`: {error}"))
35        })?;
36        let view = views.get(&relation).ok_or_else(|| {
37            SQLError::Internal(format!("view `{name}` disappeared before owner check"))
38        })?;
39        view_ownership::ensure_view_drop_authority(ownership, name, view)?;
40    }
41    Ok(())
42}
43
44pub fn ensure_no_rule_dependents(
45    names: &[String],
46    dependent_rules: Vec<(RelationIdentity, String)>,
47) -> Result<(), SQLError> {
48    if !dependent_rules.is_empty() {
49        return Err(SQLError::Routine {
50            sqlstate: "2BP01".into(),
51            message: format!(
52                "cannot drop view {} because other objects depend on it: {}",
53                names.join(", "),
54                dependent_rules
55                    .into_iter()
56                    .map(|(table, rule)| format!("rule {rule} on table {}", table.qualified_name()))
57                    .collect::<Vec<_>>()
58                    .join(", ")
59            ),
60        });
61    }
62    Ok(())
63}
64
65pub fn ensure_no_view_dependents(name: &str, dependents: &[String]) -> Result<(), SQLError> {
66    if !dependents.is_empty() {
67        return Err(SQLError::Unsupported(format!(
68            "DROP VIEW `{name}` rejected: dependent view(s) `{}` still reference it",
69            dependents.join("`, `")
70        )));
71    }
72    Ok(())
73}
74
75pub fn temporary_view_dependency_layers(
76    canonical_name: &str,
77    target: RelationIdentity,
78    views: &BTreeMap<RelationIdentity, StoredView>,
79) -> Result<Vec<Vec<RelationIdentity>>, String> {
80    let empty_ctes = std::collections::BTreeSet::new();
81    let mut targets = std::collections::BTreeSet::from([target]);
82    let mut layers = Vec::new();
83    loop {
84        let layer = views
85            .iter()
86            .filter(|(relation, _)| !targets.contains(*relation))
87            .filter(|(_, view)| {
88                targets.iter().any(|target| {
89                    query_plan_references_relation(&view.query, target, &empty_ctes)
90                })
91            })
92            .map(|(relation, view)| {
93                if view.persistence != crate::ast::RelationPersistence::Temporary {
94                    return Err(format!(
95                        "temporary relation `{canonical_name}` has non-temporary dependent view `{}`",
96                        relation.qualified_name()
97                    ));
98                }
99                Ok(relation.clone())
100            })
101            .collect::<Result<Vec<_>, String>>()?;
102        if layer.is_empty() {
103            break;
104        }
105        targets.extend(layer.iter().cloned());
106        layers.push(layer);
107    }
108    Ok(layers)
109}
110
111#[cfg(test)]
112mod tests;