Skip to main content

uqa_sql/schema/
truncate.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! TRUNCATE target expansion, partition rules and foreign-key dependency ordering.
8
9use crate::SQLError;
10use std::collections::BTreeSet;
11
12/// Catalog metadata required to bind a TRUNCATE target set.
13pub trait TruncateCatalog {
14    fn try_resolve_visible_relation_kind(
15        &self,
16        name: &str,
17    ) -> Result<Option<(String, &'static str)>, SQLError>;
18    fn is_partitioned(&self, table: &str) -> Result<bool, String>;
19    fn hierarchy_scan_tables(
20        &self,
21        table: &str,
22        descendants: bool,
23    ) -> Result<Vec<String>, SQLError>;
24    fn referrers_to(&self, table: &str) -> Result<Vec<String>, String>;
25}
26
27#[cfg(test)]
28mod tests;
29
30#[derive(Debug)]
31pub struct TruncateTargets {
32    pub all: BTreeSet<String>,
33    pub trigger_order: Vec<String>,
34    pub privilege_targets: BTreeSet<String>,
35}
36
37pub fn resolve_truncate_targets(
38    catalog: &dyn TruncateCatalog,
39    tables: &[crate::ast::TruncateTarget],
40    cascade: bool,
41) -> Result<TruncateTargets, SQLError> {
42    let mut targets = BTreeSet::new();
43    let mut trigger_targets = Vec::new();
44    let mut privilege_targets = BTreeSet::new();
45    for requested in tables {
46        let Some((table, "table")) = catalog.try_resolve_visible_relation_kind(&requested.table)?
47        else {
48            return Err(SQLError::Unsupported(format!(
49                "TRUNCATE TABLE: relation `{}` does not exist",
50                requested.table
51            )));
52        };
53        privilege_targets.insert(table.clone());
54        let partitioned = catalog
55            .is_partitioned(&table)
56            .map_err(|err| SQLError::Internal(format!("read table hierarchy: {err}")))?;
57        if !requested.include_descendants && partitioned {
58            return Err(SQLError::Routine {
59                sqlstate: "42809".into(),
60                message: "cannot truncate only a partitioned table".into(),
61            });
62        }
63        for target in catalog.hierarchy_scan_tables(&table, requested.include_descendants)? {
64            if targets.insert(target.clone()) {
65                trigger_targets.push(target);
66            }
67        }
68    }
69    if cascade {
70        let mut cursor = 0;
71        while let Some(table) = trigger_targets.get(cursor).cloned() {
72            cursor += 1;
73            for referrer in catalog
74                .referrers_to(&table)
75                .map_err(|err| SQLError::Internal(format!("read foreign keys: {err}")))?
76            {
77                if targets.insert(referrer.clone()) {
78                    privilege_targets.insert(referrer.clone());
79                    trigger_targets.push(referrer);
80                }
81            }
82        }
83    }
84    Ok(TruncateTargets {
85        all: targets,
86        trigger_order: trigger_targets,
87        privilege_targets,
88    })
89}
90
91pub fn validate_truncate_references(
92    catalog: &dyn TruncateCatalog,
93    targets: &TruncateTargets,
94) -> Result<(), SQLError> {
95    for table in &targets.all {
96        if let Some(referrer) = catalog
97            .referrers_to(table)
98            .map_err(|err| SQLError::Internal(format!("read foreign keys: {err}")))?
99            .into_iter()
100            .find(|referrer| !targets.all.contains(referrer))
101        {
102            return Err(SQLError::TypeMismatch(format!(
103                    "cannot truncate `{table}` because `{referrer}` references it; truncate both tables or use CASCADE"
104                )));
105        }
106    }
107    Ok(())
108}
109
110/// Referencing relations precede their targets, with cycles visited once.
111pub fn truncate_dependency_order(
112    catalog: &dyn TruncateCatalog,
113    targets: &TruncateTargets,
114) -> Result<Vec<String>, SQLError> {
115    let mut ordered = Vec::with_capacity(targets.all.len());
116    let mut visiting = BTreeSet::new();
117    let mut visited = BTreeSet::new();
118    for table in &targets.trigger_order {
119        visit_truncate_target(
120            catalog,
121            table,
122            &targets.all,
123            &mut visiting,
124            &mut visited,
125            &mut ordered,
126        )?;
127    }
128    Ok(ordered)
129}
130
131fn visit_truncate_target(
132    catalog: &dyn TruncateCatalog,
133    table: &str,
134    targets: &BTreeSet<String>,
135    visiting: &mut BTreeSet<String>,
136    visited: &mut BTreeSet<String>,
137    ordered: &mut Vec<String>,
138) -> Result<(), SQLError> {
139    if visited.contains(table) || !visiting.insert(table.to_string()) {
140        return Ok(());
141    }
142    for referrer in catalog
143        .referrers_to(table)
144        .map_err(|err| SQLError::Internal(format!("read foreign keys: {err}")))?
145    {
146        if targets.contains(&referrer) {
147            visit_truncate_target(catalog, &referrer, targets, visiting, visited, ordered)?;
148        }
149    }
150    visiting.remove(table);
151    if visited.insert(table.to_string()) {
152        ordered.push(table.to_string());
153    }
154    Ok(())
155}