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}