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}