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