Skip to main content

vibesql_executor/truncate/
mod.rs

1//! TRUNCATE TABLE statement execution
2//!
3//! This module coordinates truncate operations across multiple sub-modules:
4//! - `core`: Core truncate logic and table clearing
5//! - `triggers`: Trigger validation and coordination
6//! - `constraints`: Constraint and foreign key validation
7
8pub mod constraints;
9pub mod core;
10pub mod triggers;
11
12use vibesql_ast::TruncateTableStmt;
13use vibesql_storage::Database;
14
15use self::{
16    constraints::validate_truncate_allowed,
17    core::{execute_truncate, execute_truncate_cascade},
18};
19use crate::{errors::ExecutorError, privilege_checker::PrivilegeChecker};
20
21/// Executor for TRUNCATE TABLE statements
22pub struct TruncateTableExecutor;
23
24impl TruncateTableExecutor {
25    /// Execute a TRUNCATE TABLE statement
26    ///
27    /// # Arguments
28    ///
29    /// * `stmt` - The TRUNCATE TABLE statement AST node
30    /// * `database` - The database to truncate the table(s) in
31    ///
32    /// # Returns
33    ///
34    /// Total number of rows deleted from all tables or error
35    ///
36    /// # Behavior
37    ///
38    /// Supports truncating multiple tables in a single statement with all-or-nothing semantics:
39    /// - Validates all tables first (existence, privileges, constraints)
40    /// - Only truncates if all validations pass
41    /// - IF EXISTS: skips non-existent tables, continues with existing ones
42    ///
43    /// # Examples
44    ///
45    /// ```
46    /// use vibesql_ast::{ColumnDef, CreateTableStmt, TruncateTableStmt};
47    /// use vibesql_executor::{CreateTableExecutor, TruncateTableExecutor};
48    /// use vibesql_storage::{Database, Row};
49    /// use vibesql_types::{DataType, SqlValue};
50    ///
51    /// let mut db = Database::new();
52    /// let create_stmt = CreateTableStmt {
53    ///     if_not_exists: false,
54    ///     temporary: false,
55    ///     table_name: "users".to_string(),
56    ///     columns: vec![ColumnDef {
57    ///         name: "id".to_string(),
58    ///         data_type: DataType::Integer,
59    ///         nullable: false,
60    ///         constraints: vec![],
61    ///         default_value: None,
62    ///         comment: None,
63    ///         generated_expr: None, is_exact_integer_type: false,
64    ///     }],
65    ///     table_constraints: vec![],
66    ///     table_options: vec![],
67    ///     quoted: false,
68    ///     name_source: None,
69    ///     as_query: None, without_rowid: false,
70    /// };
71    /// CreateTableExecutor::execute(&create_stmt, &mut db).unwrap();
72    ///
73    /// // Insert some rows
74    /// db.insert_row("users", Row::new(vec![SqlValue::Integer(1)])).unwrap();
75    /// db.insert_row("users", Row::new(vec![SqlValue::Integer(2)])).unwrap();
76    ///
77    /// let stmt = TruncateTableStmt {
78    ///     table_names: vec!["users".to_string()],
79    ///     if_exists: false,
80    ///     cascade: None,
81    /// };
82    ///
83    /// let result = TruncateTableExecutor::execute(&stmt, &mut db);
84    /// assert_eq!(result.unwrap(), 2); // 2 rows deleted
85    /// assert_eq!(db.get_table("users").unwrap().row_count(), 0);
86    /// ```
87    pub fn execute(
88        stmt: &TruncateTableStmt,
89        database: &mut Database,
90    ) -> Result<usize, ExecutorError> {
91        // Phase 1: Validation - Check all tables before truncating any
92        // Collect tables that exist and need to be truncated
93        let mut tables_to_truncate = Vec::new();
94
95        for table_name in &stmt.table_names {
96            // Check if table exists
97            if !database.catalog.table_exists(table_name) {
98                if stmt.if_exists {
99                    // IF EXISTS specified and table doesn't exist - skip this table
100                    continue;
101                } else {
102                    return Err(ExecutorError::TableNotFound(table_name.clone()));
103                }
104            }
105
106            tables_to_truncate.push(table_name.as_str());
107        }
108
109        // If no tables to truncate (all were non-existent with IF EXISTS), return 0
110        if tables_to_truncate.is_empty() {
111            return Ok(0);
112        }
113
114        // Check DELETE privilege on all tables
115        for table_name in &tables_to_truncate {
116            PrivilegeChecker::check_delete(database, table_name)?;
117        }
118
119        // Determine CASCADE behavior - check explicit CASCADE, default to RESTRICT
120        let cascade_mode = &stmt.cascade;
121
122        match cascade_mode {
123            Some(vibesql_ast::TruncateCascadeOption::Cascade) => {
124                // CASCADE mode: recursively truncate dependent tables for each requested table
125                let mut total_rows = 0;
126                for table_name in &tables_to_truncate {
127                    total_rows += execute_truncate_cascade(database, table_name)?;
128                }
129                Ok(total_rows)
130            }
131            _ => {
132                // RESTRICT mode (default): fail if referenced by foreign keys
133                // Check if TRUNCATE is allowed on all tables (no DELETE triggers, no FK references)
134                for table_name in &tables_to_truncate {
135                    validate_truncate_allowed(database, table_name)?;
136                }
137
138                // Phase 2: Execution - All validations passed, now truncate all tables
139                let mut total_rows = 0;
140                for table_name in &tables_to_truncate {
141                    total_rows += execute_truncate(database, table_name)?;
142                }
143
144                Ok(total_rows)
145            }
146        }
147    }
148}