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