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 dml_cost::DmlOptimizer, errors::ExecutorError, evaluator::ExpressionEvaluator,
9 privilege_checker::PrivilegeChecker, 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(arcstr::ArcStr::from("Alice"))]),
56 /// )
57 /// .unwrap();
58 /// db.insert_row(
59 /// "users",
60 /// vibesql_storage::Row::new(vec![SqlValue::Integer(2), SqlValue::Varchar(arcstr::ArcStr::from("Bob"))]),
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 // Fast path: Single-row PK delete without triggers/FKs
135 // This avoids ExpressionEvaluator creation and row cloning
136 if procedural_context.is_none() && trigger_context.is_none() {
137 if let Some(vibesql_ast::WhereClause::Condition(where_expr)) = &stmt.where_clause {
138 if let Some(pk_values) = Self::extract_primary_key_lookup(where_expr, &schema) {
139 // Check if we can use the super-fast path (no triggers, no FKs)
140 let has_triggers = database
141 .catalog
142 .get_triggers_for_table(&stmt.table_name, Some(vibesql_ast::TriggerEvent::Delete))
143 .next()
144 .is_some();
145
146 // Fast check: if this table has no PK, FKs can't reference it
147 let has_pk = schema.get_primary_key_indices().is_some();
148 let has_referencing_fks = has_pk && database.catalog.list_tables().iter().any(|t| {
149 database
150 .catalog
151 .get_table(t)
152 .map(|s| s.foreign_keys.iter().any(|fk| fk.parent_table.eq_ignore_ascii_case(&stmt.table_name)))
153 .unwrap_or(false)
154 });
155
156 if !has_triggers && !has_referencing_fks {
157 // Use the fast path - no triggers, no FKs, single row PK delete
158 match database.delete_by_pk_fast(&stmt.table_name, &pk_values) {
159 Ok(deleted) => return Ok(if deleted { 1 } else { 0 }),
160 Err(_) => {
161 // Fall through to standard path on error
162 }
163 }
164 }
165 }
166 }
167 }
168
169 // Step 2: Evaluate WHERE clause and collect rows to delete (two-phase execution)
170 // Get table for scanning
171 let table = database
172 .get_table(&stmt.table_name)
173 .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
174
175 // Create evaluator with database reference for subquery support (EXISTS, NOT EXISTS, IN
176 // with subquery, etc.) and optional procedural/trigger context for variable resolution
177 let evaluator = if let Some(ctx) = trigger_context {
178 // Trigger context takes precedence (trigger statements can't have procedural context)
179 ExpressionEvaluator::with_trigger_context(&schema, database, ctx)
180 } else if let Some(ctx) = procedural_context {
181 ExpressionEvaluator::with_procedural_context(&schema, database, ctx)
182 } else {
183 ExpressionEvaluator::with_database(&schema, database)
184 };
185
186 // Check once if any DELETE triggers exist for this table (used for fast-path checks)
187 let has_delete_triggers = database
188 .catalog
189 .get_triggers_for_table(&stmt.table_name, Some(vibesql_ast::TriggerEvent::Delete))
190 .next()
191 .is_some();
192
193 // Find rows to delete and their indices
194 // Try to use primary key index for fast lookup
195 let mut rows_and_indices_to_delete: Vec<(usize, vibesql_storage::Row)> = Vec::new();
196
197 if let Some(vibesql_ast::WhereClause::Condition(where_expr)) = &stmt.where_clause {
198 // Try primary key optimization
199 if let Some(pk_values) = Self::extract_primary_key_lookup(where_expr, &schema) {
200 if let Some(pk_index) = table.primary_key_index() {
201 if let Some(&row_index) = pk_index.get(&pk_values) {
202 // Found the row via index - single row to delete
203 rows_and_indices_to_delete
204 .push((row_index, table.scan()[row_index].clone()));
205 }
206 // If not found, rows_and_indices_to_delete stays empty (no rows to delete)
207 } else {
208 // No PK index, fall through to table scan below
209 Self::collect_rows_with_scan(
210 table,
211 &stmt.where_clause,
212 &evaluator,
213 &mut rows_and_indices_to_delete,
214 )?;
215 }
216 } else {
217 // Can't extract PK lookup, fall through to table scan
218 Self::collect_rows_with_scan(
219 table,
220 &stmt.where_clause,
221 &evaluator,
222 &mut rows_and_indices_to_delete,
223 )?;
224 }
225 } else {
226 // No WHERE clause - collect all rows
227 Self::collect_rows_with_scan(
228 table,
229 &stmt.where_clause,
230 &evaluator,
231 &mut rows_and_indices_to_delete,
232 )?;
233 }
234
235 // Cost-based optimization: Log delete cost and check for early compaction recommendation
236 let optimizer = DmlOptimizer::new(database, &stmt.table_name);
237 if optimizer.should_chunk_delete(rows_and_indices_to_delete.len()) {
238 // Log recommendation for potential chunked delete (informational only)
239 // Actual chunked delete would require transaction support to be safe
240 if std::env::var("DML_COST_DEBUG").is_ok() {
241 eprintln!(
242 "DML_COST_DEBUG: DELETE on {} - {} rows qualifies for chunked delete",
243 stmt.table_name,
244 rows_and_indices_to_delete.len()
245 );
246 }
247 }
248 if optimizer.should_trigger_early_compaction() {
249 // Log early compaction recommendation (informational only)
250 // Table compaction is triggered automatically after >50% deleted rows
251 if std::env::var("DML_COST_DEBUG").is_ok() {
252 eprintln!(
253 "DML_COST_DEBUG: DELETE on {} - early compaction recommended due to high deleted ratio",
254 stmt.table_name
255 );
256 }
257 }
258
259 // Fire BEFORE STATEMENT triggers only if triggers exist AND we're not inside a trigger context
260 // (Statement-level triggers don't fire for deletes within trigger bodies)
261 if has_delete_triggers && trigger_context.is_none() {
262 crate::TriggerFirer::execute_before_statement_triggers(
263 database,
264 &stmt.table_name,
265 vibesql_ast::TriggerEvent::Delete,
266 )?;
267 }
268
269 // Step 3: Fire BEFORE DELETE ROW triggers only if triggers exist
270 if has_delete_triggers {
271 for (_, row) in &rows_and_indices_to_delete {
272 crate::TriggerFirer::execute_before_triggers(
273 database,
274 &stmt.table_name,
275 vibesql_ast::TriggerEvent::Delete,
276 Some(row),
277 None,
278 )?;
279 }
280 }
281
282 // Step 4: Handle referential integrity for each row to be deleted
283 // This may CASCADE deletes, SET NULL, or SET DEFAULT in child tables
284 for (_, row) in &rows_and_indices_to_delete {
285 check_no_child_references(database, &stmt.table_name, row)?;
286 }
287
288 // Extract indices for deletion
289 let mut deleted_indices: Vec<usize> =
290 rows_and_indices_to_delete.iter().map(|(idx, _)| *idx).collect();
291 deleted_indices.sort_unstable();
292
293 // Step 5a: Emit WAL entries and remove entries from user-defined indexes
294 // BEFORE deleting rows (while row indices are still valid and we have old values)
295 // First emit WAL entries for each row (needed for recovery replay)
296 for (idx, row) in &rows_and_indices_to_delete {
297 database.emit_wal_delete(&stmt.table_name, *idx as u64, row.values.to_vec());
298 }
299
300 // Then use batch method for index updates: O(d + m*log n) vs O(d*m*log n)
301 // where d=deletes, m=indexes
302 let rows_refs: Vec<(usize, &vibesql_storage::Row)> = rows_and_indices_to_delete
303 .iter()
304 .map(|(idx, row)| (*idx, row))
305 .collect();
306 database.batch_update_indexes_for_delete(&stmt.table_name, &rows_refs);
307
308 // Step 5b: Actually delete the rows using fast path (no table scan needed)
309 let table_mut = database
310 .get_table_mut(&stmt.table_name)
311 .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
312
313 // Use delete_by_indices_batch for O(d) instead of O(n) where d = deletes
314 // The batch version pre-computes schema lookups for internal hash indexes,
315 // reducing overhead by ~30-40% for multi-row deletes.
316 // User-defined index entries have already been removed by batch_update_indexes_for_delete above.
317 // Note: If >50% of rows are deleted, compaction triggers and row indices change.
318 // When compaction occurs, we must rebuild user-defined indexes.
319 let delete_result = table_mut.delete_by_indices_batch(&deleted_indices);
320
321 // If compaction occurred, rebuild user-defined indexes since all row indices changed
322 if delete_result.compacted {
323 database.rebuild_indexes(&stmt.table_name);
324 }
325
326 // Invalidate the database-level columnar cache since table data changed.
327 // Note: The table-level cache is already invalidated by delete_by_indices().
328 // Both invalidations are necessary because they manage separate caches:
329 // - Table-level cache: used by Table::scan_columnar() for SIMD filtering
330 // - Database-level cache: used by Database::get_columnar() for cached access
331 if delete_result.deleted_count > 0 {
332 database.invalidate_columnar_cache(&stmt.table_name);
333 }
334
335 // Step 6: Fire AFTER DELETE ROW triggers only if triggers exist
336 if has_delete_triggers {
337 for (_, row) in &rows_and_indices_to_delete {
338 crate::TriggerFirer::execute_after_triggers(
339 database,
340 &stmt.table_name,
341 vibesql_ast::TriggerEvent::Delete,
342 Some(row),
343 None,
344 )?;
345 }
346 }
347
348 // Fire AFTER STATEMENT triggers only if triggers exist AND we're not inside a trigger context
349 // (Statement-level triggers don't fire for deletes within trigger bodies)
350 if has_delete_triggers && trigger_context.is_none() {
351 crate::TriggerFirer::execute_after_statement_triggers(
352 database,
353 &stmt.table_name,
354 vibesql_ast::TriggerEvent::Delete,
355 )?;
356 }
357
358 Ok(delete_result.deleted_count)
359 }
360
361 /// Extract primary key value from WHERE expression if it's a simple equality
362 fn extract_primary_key_lookup(
363 where_expr: &vibesql_ast::Expression,
364 schema: &vibesql_catalog::TableSchema,
365 ) -> Option<Vec<vibesql_types::SqlValue>> {
366 use vibesql_ast::{BinaryOperator, Expression};
367
368 // Only handle simple binary equality operations
369 if let Expression::BinaryOp { left, op: BinaryOperator::Equal, right } = where_expr {
370 // Check if left side is a column reference and right side is a literal
371 if let (Expression::ColumnRef { column, .. }, Expression::Literal(value)) =
372 (left.as_ref(), right.as_ref())
373 {
374 // Check if this column is the primary key
375 if let Some(pk_indices) = schema.get_primary_key_indices() {
376 if let Some(col_index) = schema.get_column_index(column) {
377 // Only handle single-column primary keys for now
378 if pk_indices.len() == 1 && pk_indices[0] == col_index {
379 return Some(vec![value.clone()]);
380 }
381 }
382 }
383 }
384
385 // Also check the reverse: literal = column
386 if let (Expression::Literal(value), Expression::ColumnRef { column, .. }) =
387 (left.as_ref(), right.as_ref())
388 {
389 if let Some(pk_indices) = schema.get_primary_key_indices() {
390 if let Some(col_index) = schema.get_column_index(column) {
391 if pk_indices.len() == 1 && pk_indices[0] == col_index {
392 return Some(vec![value.clone()]);
393 }
394 }
395 }
396 }
397 }
398
399 None
400 }
401
402 /// Collect rows using table scan (fallback when PK optimization can't be used)
403 fn collect_rows_with_scan(
404 table: &vibesql_storage::Table,
405 where_clause: &Option<vibesql_ast::WhereClause>,
406 evaluator: &ExpressionEvaluator,
407 rows_and_indices: &mut Vec<(usize, vibesql_storage::Row)>,
408 ) -> Result<(), ExecutorError> {
409 // Use scan_live() to skip already-deleted rows
410 for (index, row) in table.scan_live() {
411 // Clear CSE cache before evaluating each row to prevent column values
412 // from being incorrectly cached across different rows
413 evaluator.clear_cse_cache();
414
415 let should_delete = if let Some(ref where_clause) = where_clause {
416 match where_clause {
417 vibesql_ast::WhereClause::Condition(where_expr) => {
418 matches!(
419 evaluator.eval(where_expr, row),
420 Ok(vibesql_types::SqlValue::Boolean(true))
421 )
422 }
423 vibesql_ast::WhereClause::CurrentOf(_cursor_name) => {
424 return Err(ExecutorError::UnsupportedFeature(
425 "WHERE CURRENT OF cursor is not yet implemented".to_string(),
426 ));
427 }
428 }
429 } else {
430 true
431 };
432
433 if should_delete {
434 rows_and_indices.push((index, row.clone()));
435 }
436 }
437
438 Ok(())
439 }
440}
441
442/// Execute TRUNCATE-style fast path for DELETE FROM table (no WHERE)
443///
444/// Clears all rows and indexes in a single operation instead of row-by-row deletion.
445/// Provides 100-1000x performance improvement for full table deletes.
446///
447/// # Safety
448/// Only call this after `can_use_truncate` returns true.
449fn execute_truncate(database: &mut Database, table_name: &str) -> Result<usize, ExecutorError> {
450 let table = database
451 .get_table_mut(table_name)
452 .ok_or_else(|| ExecutorError::TableNotFound(table_name.to_string()))?;
453
454 let row_count = table.row_count();
455
456 // Clear all data at once (O(1) operation)
457 // Note: table.clear() invalidates the table-level columnar cache internally
458 table.clear();
459
460 // Invalidate the database-level columnar cache since table data changed.
461 // Both the table-level (via clear()) and database-level invalidations are
462 // necessary because they manage separate caches at different levels.
463 if row_count > 0 {
464 database.invalidate_columnar_cache(table_name);
465 }
466
467 Ok(row_count)
468}
469
470/// Execute a DELETE statement with trigger context
471/// This function is used when executing DELETE statements within trigger bodies
472/// to support OLD/NEW pseudo-variable references
473pub fn execute_delete_with_trigger_context(
474 database: &mut Database,
475 stmt: &DeleteStmt,
476 trigger_context: &crate::trigger_execution::TriggerContext,
477) -> Result<usize, ExecutorError> {
478 DeleteExecutor::execute_with_trigger_context(stmt, database, trigger_context)
479}