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    /// The relation followed by each partitioned table it is a partition of, nearest first.
26    fn partition_ancestor_tables(&self, table: &str) -> Result<Vec<String>, SQLError>;
27}
28
29/// The tables whose foreign keys reference `table`'s rows: those that reference it and those that reference a partitioned table it is a partition of, as `PostgreSQL` derives a constraint on each referenced partition.
30fn referencing_tables(catalog: &dyn TruncateCatalog, table: &str) -> Result<Vec<String>, SQLError> {
31    let mut referencing = Vec::new();
32    for ancestor in catalog.partition_ancestor_tables(table)? {
33        for referrer in catalog
34            .referrers_to(&ancestor)
35            .map_err(|err| SQLError::Internal(format!("read foreign keys: {err}")))?
36        {
37            if !referencing.contains(&referrer) {
38                referencing.push(referrer);
39            }
40        }
41    }
42    Ok(referencing)
43}
44
45#[cfg(test)]
46mod tests;
47
48#[derive(Debug)]
49pub struct TruncateTargets {
50    pub all: BTreeSet<String>,
51    pub trigger_order: Vec<String>,
52    pub privilege_targets: BTreeSet<String>,
53    /// The tables `CASCADE` added because their foreign keys reference a target, in the order they were added.
54    pub cascaded: Vec<String>,
55}
56
57pub fn resolve_truncate_targets(
58    catalog: &dyn TruncateCatalog,
59    tables: &[crate::ast::TruncateTarget],
60    cascade: bool,
61) -> Result<TruncateTargets, SQLError> {
62    let mut targets = BTreeSet::new();
63    let mut trigger_targets = Vec::new();
64    let mut privilege_targets = BTreeSet::new();
65    for requested in tables {
66        let Some((table, "table")) = catalog.try_resolve_visible_relation_kind(&requested.table)?
67        else {
68            return Err(SQLError::Unsupported(format!(
69                "TRUNCATE TABLE: relation `{}` does not exist",
70                requested.table
71            )));
72        };
73        privilege_targets.insert(table.clone());
74        let partitioned = catalog
75            .is_partitioned(&table)
76            .map_err(|err| SQLError::Internal(format!("read table hierarchy: {err}")))?;
77        if !requested.include_descendants && partitioned {
78            return Err(SQLError::Routine {
79                sqlstate: "42809".into(),
80                message: "cannot truncate only a partitioned table".into(),
81            });
82        }
83        for target in catalog.hierarchy_scan_tables(&table, requested.include_descendants)? {
84            if targets.insert(target.clone()) {
85                trigger_targets.push(target);
86            }
87        }
88    }
89    let mut cascaded = Vec::new();
90    if cascade {
91        let mut cursor = 0;
92        while let Some(table) = trigger_targets.get(cursor).cloned() {
93            cursor += 1;
94            for referrer in referencing_tables(catalog, &table)? {
95                if targets.insert(referrer.clone()) {
96                    privilege_targets.insert(referrer.clone());
97                    cascaded.push(referrer.clone());
98                    trigger_targets.push(referrer);
99                }
100            }
101        }
102    }
103    Ok(TruncateTargets {
104        all: targets,
105        trigger_order: trigger_targets,
106        privilege_targets,
107        cascaded,
108    })
109}
110
111/// Reject truncating a table that a table outside the targets references, naming the first such pair in target order, as `PostgreSQL`'s `heap_truncate_check_FKs` does.
112pub fn validate_truncate_references(
113    catalog: &dyn TruncateCatalog,
114    targets: &TruncateTargets,
115) -> Result<(), SQLError> {
116    for table in &targets.trigger_order {
117        if let Some(referrer) = referencing_tables(catalog, table)?
118            .into_iter()
119            .find(|referrer| !targets.all.contains(referrer))
120        {
121            let referrer = crate::semantics::foreign_keys::foreign_key_relation_name(&referrer);
122            return Err(SQLError::Diagnostic {
123                sqlstate: "0A000".into(),
124                message: "cannot truncate a table referenced in a foreign key constraint".into(),
125                detail: Some(format!(
126                    "Table \"{referrer}\" references \"{}\".",
127                    crate::semantics::foreign_keys::foreign_key_relation_name(table)
128                )),
129                hint: Some(format!(
130                    "Truncate table \"{referrer}\" at the same time, or use TRUNCATE ... CASCADE."
131                )),
132            });
133        }
134    }
135    Ok(())
136}
137
138/// Referencing relations precede their targets, with cycles visited once.
139pub fn truncate_dependency_order(
140    catalog: &dyn TruncateCatalog,
141    targets: &TruncateTargets,
142) -> Result<Vec<String>, SQLError> {
143    let mut ordered = Vec::with_capacity(targets.all.len());
144    let mut visiting = BTreeSet::new();
145    let mut visited = BTreeSet::new();
146    for table in &targets.trigger_order {
147        visit_truncate_target(
148            catalog,
149            table,
150            &targets.all,
151            &mut visiting,
152            &mut visited,
153            &mut ordered,
154        )?;
155    }
156    Ok(ordered)
157}
158
159fn visit_truncate_target(
160    catalog: &dyn TruncateCatalog,
161    table: &str,
162    targets: &BTreeSet<String>,
163    visiting: &mut BTreeSet<String>,
164    visited: &mut BTreeSet<String>,
165    ordered: &mut Vec<String>,
166) -> Result<(), SQLError> {
167    if visited.contains(table) || !visiting.insert(table.to_string()) {
168        return Ok(());
169    }
170    for referrer in referencing_tables(catalog, table)? {
171        if targets.contains(&referrer) {
172            visit_truncate_target(catalog, &referrer, targets, visiting, visited, ordered)?;
173        }
174    }
175    visiting.remove(table);
176    if visited.insert(table.to_string()) {
177        ordered.push(table.to_string());
178    }
179    Ok(())
180}