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}