vibesql_executor/delete/executor.rs
1//! DELETE statement execution
2
3use vibesql_ast::DeleteStmt;
4use vibesql_storage::Database;
5
6use super::integrity::check_no_child_references;
7use crate::{
8 errors::ExecutorError, evaluator::ExpressionEvaluator, privilege_checker::PrivilegeChecker,
9 truncate_validation::can_use_truncate,
10};
11
12/// Executor for DELETE statements
13pub struct DeleteExecutor;
14
15impl DeleteExecutor {
16 /// Execute a DELETE statement
17 ///
18 /// # Arguments
19 ///
20 /// * `stmt` - The DELETE statement AST node
21 /// * `database` - The database to delete from
22 ///
23 /// # Returns
24 ///
25 /// Number of rows deleted or error
26 ///
27 /// # Examples
28 ///
29 /// ```
30 /// use vibesql_ast::{BinaryOperator, DeleteStmt, Expression, WhereClause};
31 /// use vibesql_catalog::{ColumnSchema, TableSchema};
32 /// use vibesql_executor::DeleteExecutor;
33 /// use vibesql_storage::Database;
34 /// use vibesql_types::{DataType, SqlValue};
35 ///
36 /// let mut db = Database::new();
37 ///
38 /// // Create table
39 /// let schema = TableSchema::new(
40 /// "users".to_string(),
41 /// vec![
42 /// ColumnSchema::new("id".to_string(), DataType::Integer, false),
43 /// ColumnSchema::new(
44 /// "name".to_string(),
45 /// DataType::Varchar { max_length: Some(50) },
46 /// false,
47 /// ),
48 /// ],
49 /// );
50 /// db.create_table(schema).unwrap();
51 ///
52 /// // Insert rows
53 /// db.insert_row(
54 /// "users",
55 /// vibesql_storage::Row::new(vec![SqlValue::Integer(1), SqlValue::Varchar("Alice".to_string())]),
56 /// )
57 /// .unwrap();
58 /// db.insert_row(
59 /// "users",
60 /// vibesql_storage::Row::new(vec![SqlValue::Integer(2), SqlValue::Varchar("Bob".to_string())]),
61 /// )
62 /// .unwrap();
63 ///
64 /// // Delete specific row
65 /// let stmt = DeleteStmt {
66 /// only: false,
67 /// table_name: "users".to_string(),
68 /// where_clause: Some(WhereClause::Condition(Expression::BinaryOp {
69 /// left: Box::new(Expression::ColumnRef { table: None, column: "id".to_string() }),
70 /// op: BinaryOperator::Equal,
71 /// right: Box::new(Expression::Literal(SqlValue::Integer(1))),
72 /// })),
73 /// };
74 ///
75 /// let count = DeleteExecutor::execute(&stmt, &mut db).unwrap();
76 /// assert_eq!(count, 1);
77 /// ```
78 pub fn execute(stmt: &DeleteStmt, database: &mut Database) -> Result<usize, ExecutorError> {
79 Self::execute_internal(stmt, database, None, None)
80 }
81
82 /// Execute a DELETE statement with procedural context
83 /// Supports procedural variables in WHERE clause
84 pub fn execute_with_procedural_context(
85 stmt: &DeleteStmt,
86 database: &mut Database,
87 procedural_context: &crate::procedural::ExecutionContext,
88 ) -> Result<usize, ExecutorError> {
89 Self::execute_internal(stmt, database, Some(procedural_context), None)
90 }
91
92 /// Execute a DELETE statement with trigger context
93 /// This allows DELETE statements within trigger bodies to reference OLD/NEW pseudo-variables
94 pub fn execute_with_trigger_context(
95 stmt: &DeleteStmt,
96 database: &mut Database,
97 trigger_context: &crate::trigger_execution::TriggerContext,
98 ) -> Result<usize, ExecutorError> {
99 Self::execute_internal(stmt, database, None, Some(trigger_context))
100 }
101
102 /// Internal implementation supporting procedural context and trigger context
103 fn execute_internal(
104 stmt: &DeleteStmt,
105 database: &mut Database,
106 procedural_context: Option<&crate::procedural::ExecutionContext>,
107 trigger_context: Option<&crate::trigger_execution::TriggerContext>,
108 ) -> Result<usize, ExecutorError> {
109 // Note: stmt.only is currently ignored (treated as false)
110 // ONLY keyword is used in table inheritance to exclude derived tables.
111 // Since table inheritance is not yet implemented, we treat all deletes the same.
112
113 // Check DELETE privilege on the table
114 PrivilegeChecker::check_delete(database, &stmt.table_name)?;
115
116 // Check table exists
117 if !database.catalog.table_exists(&stmt.table_name) {
118 return Err(ExecutorError::TableNotFound(stmt.table_name.clone()));
119 }
120
121 // Fast path: DELETE FROM table (no WHERE clause)
122 // Use TRUNCATE-style optimization for 100-1000x performance improvement
123 if stmt.where_clause.is_none() && can_use_truncate(database, &stmt.table_name)? {
124 return execute_truncate(database, &stmt.table_name);
125 }
126
127 // Step 1: Get schema (clone to avoid borrow issues)
128 let schema = database
129 .catalog
130 .get_table(&stmt.table_name)
131 .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?
132 .clone();
133
134 // Step 2: Evaluate WHERE clause and collect rows to delete (two-phase execution)
135 // Get table for scanning
136 let table = database
137 .get_table(&stmt.table_name)
138 .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
139
140 // Create evaluator with database reference for subquery support (EXISTS, NOT EXISTS, IN
141 // with subquery, etc.) and optional procedural/trigger context for variable resolution
142 let evaluator = if let Some(ctx) = trigger_context {
143 // Trigger context takes precedence (trigger statements can't have procedural context)
144 ExpressionEvaluator::with_trigger_context(&schema, database, ctx)
145 } else if let Some(ctx) = procedural_context {
146 ExpressionEvaluator::with_procedural_context(&schema, database, ctx)
147 } else {
148 ExpressionEvaluator::with_database(&schema, database)
149 };
150
151 // Find rows to delete and their indices
152 // Try to use primary key index for fast lookup
153 let mut rows_and_indices_to_delete: Vec<(usize, vibesql_storage::Row)> = Vec::new();
154
155 if let Some(vibesql_ast::WhereClause::Condition(where_expr)) = &stmt.where_clause {
156 // Try primary key optimization
157 if let Some(pk_values) = Self::extract_primary_key_lookup(where_expr, &schema) {
158 if let Some(pk_index) = table.primary_key_index() {
159 if let Some(&row_index) = pk_index.get(&pk_values) {
160 // Found the row via index - single row to delete
161 rows_and_indices_to_delete
162 .push((row_index, table.scan()[row_index].clone()));
163 }
164 // If not found, rows_and_indices_to_delete stays empty (no rows to delete)
165 } else {
166 // No PK index, fall through to table scan below
167 Self::collect_rows_with_scan(
168 table,
169 &stmt.where_clause,
170 &evaluator,
171 &mut rows_and_indices_to_delete,
172 )?;
173 }
174 } else {
175 // Can't extract PK lookup, fall through to table scan
176 Self::collect_rows_with_scan(
177 table,
178 &stmt.where_clause,
179 &evaluator,
180 &mut rows_and_indices_to_delete,
181 )?;
182 }
183 } else {
184 // No WHERE clause - collect all rows
185 Self::collect_rows_with_scan(
186 table,
187 &stmt.where_clause,
188 &evaluator,
189 &mut rows_and_indices_to_delete,
190 )?;
191 }
192
193 // Fire BEFORE STATEMENT triggers (unless we're already inside a trigger context)
194 if trigger_context.is_none() {
195 crate::TriggerFirer::execute_before_statement_triggers(
196 database,
197 &stmt.table_name,
198 vibesql_ast::TriggerEvent::Delete,
199 )?;
200 }
201
202 // Step 3: Fire BEFORE DELETE ROW triggers
203 for (_, row) in &rows_and_indices_to_delete {
204 crate::TriggerFirer::execute_before_triggers(
205 database,
206 &stmt.table_name,
207 vibesql_ast::TriggerEvent::Delete,
208 Some(row),
209 None,
210 )?;
211 }
212
213 // Step 4: Handle referential integrity for each row to be deleted
214 // This may CASCADE deletes, SET NULL, or SET DEFAULT in child tables
215 for (_, row) in &rows_and_indices_to_delete {
216 check_no_child_references(database, &stmt.table_name, row)?;
217 }
218
219 // Extract indices for deletion
220 let mut deleted_indices: Vec<usize> =
221 rows_and_indices_to_delete.iter().map(|(idx, _)| *idx).collect();
222 deleted_indices.sort_unstable();
223
224 // Step 5a: Remove entries from user-defined indexes BEFORE deleting rows
225 // (while row indices are still valid)
226 for (idx, row) in &rows_and_indices_to_delete {
227 database.update_indexes_for_delete(&stmt.table_name, row, *idx);
228 }
229
230 // Step 5b: Actually delete the rows using fast path (no table scan needed)
231 let table_mut = database
232 .get_table_mut(&stmt.table_name)
233 .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
234
235 // Use delete_by_indices for O(d * log n) instead of O(n) where d = deletes
236 let deleted_count = table_mut.delete_by_indices(&deleted_indices);
237
238 // Step 5c: Adjust remaining user-defined index entries
239 // (entries pointing to indices > deleted need to be decremented)
240 database.adjust_indexes_after_delete(&stmt.table_name, &deleted_indices);
241
242 // Invalidate columnar cache since table data has changed
243 if deleted_count > 0 {
244 database.invalidate_columnar_cache(&stmt.table_name);
245 }
246
247 // Step 6: Fire AFTER DELETE ROW triggers for each deleted row
248 for (_, row) in &rows_and_indices_to_delete {
249 crate::TriggerFirer::execute_after_triggers(
250 database,
251 &stmt.table_name,
252 vibesql_ast::TriggerEvent::Delete,
253 Some(row),
254 None,
255 )?;
256 }
257
258 // Fire AFTER STATEMENT triggers (unless we're already inside a trigger context)
259 if trigger_context.is_none() {
260 crate::TriggerFirer::execute_after_statement_triggers(
261 database,
262 &stmt.table_name,
263 vibesql_ast::TriggerEvent::Delete,
264 )?;
265 }
266
267 Ok(deleted_count)
268 }
269
270 /// Extract primary key value from WHERE expression if it's a simple equality
271 fn extract_primary_key_lookup(
272 where_expr: &vibesql_ast::Expression,
273 schema: &vibesql_catalog::TableSchema,
274 ) -> Option<Vec<vibesql_types::SqlValue>> {
275 use vibesql_ast::{BinaryOperator, Expression};
276
277 // Only handle simple binary equality operations
278 if let Expression::BinaryOp { left, op: BinaryOperator::Equal, right } = where_expr {
279 // Check if left side is a column reference and right side is a literal
280 if let (Expression::ColumnRef { column, .. }, Expression::Literal(value)) =
281 (left.as_ref(), right.as_ref())
282 {
283 // Check if this column is the primary key
284 if let Some(pk_indices) = schema.get_primary_key_indices() {
285 if let Some(col_index) = schema.get_column_index(column) {
286 // Only handle single-column primary keys for now
287 if pk_indices.len() == 1 && pk_indices[0] == col_index {
288 return Some(vec![value.clone()]);
289 }
290 }
291 }
292 }
293
294 // Also check the reverse: literal = column
295 if let (Expression::Literal(value), Expression::ColumnRef { column, .. }) =
296 (left.as_ref(), right.as_ref())
297 {
298 if let Some(pk_indices) = schema.get_primary_key_indices() {
299 if let Some(col_index) = schema.get_column_index(column) {
300 if pk_indices.len() == 1 && pk_indices[0] == col_index {
301 return Some(vec![value.clone()]);
302 }
303 }
304 }
305 }
306 }
307
308 None
309 }
310
311 /// Collect rows using table scan (fallback when PK optimization can't be used)
312 fn collect_rows_with_scan(
313 table: &vibesql_storage::Table,
314 where_clause: &Option<vibesql_ast::WhereClause>,
315 evaluator: &ExpressionEvaluator,
316 rows_and_indices: &mut Vec<(usize, vibesql_storage::Row)>,
317 ) -> Result<(), ExecutorError> {
318 for (index, row) in table.scan().iter().enumerate() {
319 // Clear CSE cache before evaluating each row to prevent column values
320 // from being incorrectly cached across different rows
321 evaluator.clear_cse_cache();
322
323 let should_delete = if let Some(ref where_clause) = where_clause {
324 match where_clause {
325 vibesql_ast::WhereClause::Condition(where_expr) => {
326 matches!(
327 evaluator.eval(where_expr, row),
328 Ok(vibesql_types::SqlValue::Boolean(true))
329 )
330 }
331 vibesql_ast::WhereClause::CurrentOf(_cursor_name) => {
332 return Err(ExecutorError::UnsupportedFeature(
333 "WHERE CURRENT OF cursor is not yet implemented".to_string(),
334 ));
335 }
336 }
337 } else {
338 true
339 };
340
341 if should_delete {
342 rows_and_indices.push((index, row.clone()));
343 }
344 }
345
346 Ok(())
347 }
348}
349
350/// Execute TRUNCATE-style fast path for DELETE FROM table (no WHERE)
351///
352/// Clears all rows and indexes in a single operation instead of row-by-row deletion.
353/// Provides 100-1000x performance improvement for full table deletes.
354///
355/// # Safety
356/// Only call this after `can_use_truncate` returns true.
357fn execute_truncate(database: &mut Database, table_name: &str) -> Result<usize, ExecutorError> {
358 let table = database
359 .get_table_mut(table_name)
360 .ok_or_else(|| ExecutorError::TableNotFound(table_name.to_string()))?;
361
362 let row_count = table.row_count();
363
364 // Clear all data at once (O(1) operation)
365 table.clear();
366
367 // Invalidate columnar cache since table data has changed
368 if row_count > 0 {
369 database.invalidate_columnar_cache(table_name);
370 }
371
372 Ok(row_count)
373}
374
375/// Execute a DELETE statement with trigger context
376/// This function is used when executing DELETE statements within trigger bodies
377/// to support OLD/NEW pseudo-variable references
378pub fn execute_delete_with_trigger_context(
379 database: &mut Database,
380 stmt: &DeleteStmt,
381 trigger_context: &crate::trigger_execution::TriggerContext,
382) -> Result<usize, ExecutorError> {
383 DeleteExecutor::execute_with_trigger_context(stmt, database, trigger_context)
384}