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}