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 crate::errors::ExecutorError;
16use crate::privilege_checker::PrivilegeChecker;
17
18use self::constraints::validate_truncate_allowed;
19use self::core::{execute_truncate, execute_truncate_cascade};
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 /// table_name: "users".to_string(),
55 /// columns: vec![ColumnDef {
56 /// name: "id".to_string(),
57 /// data_type: DataType::Integer,
58 /// nullable: false,
59 /// constraints: vec![],
60 /// default_value: None,
61 /// comment: None,
62 /// }],
63 /// table_constraints: vec![],
64 /// table_options: vec![],
65 /// };
66 /// CreateTableExecutor::execute(&create_stmt, &mut db).unwrap();
67 ///
68 /// // Insert some rows
69 /// db.insert_row("users", Row::new(vec![SqlValue::Integer(1)])).unwrap();
70 /// db.insert_row("users", Row::new(vec![SqlValue::Integer(2)])).unwrap();
71 ///
72 /// let stmt = TruncateTableStmt { table_names: vec!["users".to_string()], if_exists: false, cascade: None };
73 ///
74 /// let result = TruncateTableExecutor::execute(&stmt, &mut db);
75 /// assert_eq!(result.unwrap(), 2); // 2 rows deleted
76 /// assert_eq!(db.get_table("users").unwrap().row_count(), 0);
77 /// ```
78 pub fn execute(
79 stmt: &TruncateTableStmt,
80 database: &mut Database,
81 ) -> Result<usize, ExecutorError> {
82 // Phase 1: Validation - Check all tables before truncating any
83 // Collect tables that exist and need to be truncated
84 let mut tables_to_truncate = Vec::new();
85
86 for table_name in &stmt.table_names {
87 // Check if table exists
88 if !database.catalog.table_exists(table_name) {
89 if stmt.if_exists {
90 // IF EXISTS specified and table doesn't exist - skip this table
91 continue;
92 } else {
93 return Err(ExecutorError::TableNotFound(table_name.clone()));
94 }
95 }
96
97 tables_to_truncate.push(table_name.as_str());
98 }
99
100 // If no tables to truncate (all were non-existent with IF EXISTS), return 0
101 if tables_to_truncate.is_empty() {
102 return Ok(0);
103 }
104
105 // Check DELETE privilege on all tables
106 for table_name in &tables_to_truncate {
107 PrivilegeChecker::check_delete(database, table_name)?;
108 }
109
110 // Determine CASCADE behavior - check explicit CASCADE, default to RESTRICT
111 let cascade_mode = &stmt.cascade;
112
113 match cascade_mode {
114 Some(vibesql_ast::TruncateCascadeOption::Cascade) => {
115 // CASCADE mode: recursively truncate dependent tables for each requested table
116 let mut total_rows = 0;
117 for table_name in &tables_to_truncate {
118 total_rows += execute_truncate_cascade(database, table_name)?;
119 }
120 Ok(total_rows)
121 }
122 _ => {
123 // RESTRICT mode (default): fail if referenced by foreign keys
124 // Check if TRUNCATE is allowed on all tables (no DELETE triggers, no FK references)
125 for table_name in &tables_to_truncate {
126 validate_truncate_allowed(database, table_name)?;
127 }
128
129 // Phase 2: Execution - All validations passed, now truncate all tables
130 let mut total_rows = 0;
131 for table_name in &tables_to_truncate {
132 total_rows += execute_truncate(database, table_name)?;
133 }
134
135 Ok(total_rows)
136 }
137 }
138 }
139}