vibesql_executor/update/
mod.rs

1//! UPDATE statement execution
2//!
3//! This module provides UPDATE statement execution with the following architecture:
4//!
5//! - `row_selector`: Handles WHERE clause evaluation and primary key index optimization
6//! - `value_updater`: Applies assignment expressions to rows
7//! - `constraints`: Validates NOT NULL, PRIMARY KEY, UNIQUE, and CHECK constraints
8//! - `foreign_keys`: Validates foreign key constraints and child references
9//!
10//! The main `UpdateExecutor` orchestrates these components to implement SQL's two-phase
11//! update semantics: first collect all updates evaluating against original rows, then
12//! apply all updates atomically.
13//!
14//! ## Performance Optimizations
15//!
16//! The executor includes a fast path for single-row primary key updates that:
17//! - Skips trigger checks when no triggers exist for the table
18//! - Avoids schema cloning
19//! - Uses single-pass execution instead of two-phase
20//! - Minimizes allocations
21
22mod constraints;
23mod foreign_keys;
24mod row_selector;
25mod value_updater;
26
27use vibesql_ast::{BinaryOperator, Expression, UpdateStmt};
28use constraints::ConstraintValidator;
29use foreign_keys::ForeignKeyValidator;
30use row_selector::RowSelector;
31use vibesql_storage::Database;
32use value_updater::ValueUpdater;
33
34use crate::{
35    errors::ExecutorError, evaluator::ExpressionEvaluator, privilege_checker::PrivilegeChecker,
36};
37
38/// Executor for UPDATE statements
39pub struct UpdateExecutor;
40
41impl UpdateExecutor {
42    /// Execute an UPDATE statement
43    ///
44    /// # Arguments
45    ///
46    /// * `stmt` - The UPDATE statement AST node
47    /// * `database` - The database to update
48    ///
49    /// # Returns
50    ///
51    /// Number of rows updated or error
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// use vibesql_ast::{Assignment, Expression, UpdateStmt};
57    /// use vibesql_catalog::{ColumnSchema, TableSchema};
58    /// use vibesql_executor::UpdateExecutor;
59    /// use vibesql_storage::Database;
60    /// use vibesql_types::{DataType, SqlValue};
61    ///
62    /// let mut db = Database::new();
63    ///
64    /// // Create table
65    /// let schema = TableSchema::new(
66    ///     "employees".to_string(),
67    ///     vec![
68    ///         ColumnSchema::new("id".to_string(), DataType::Integer, false),
69    ///         ColumnSchema::new("salary".to_string(), DataType::Integer, false),
70    ///     ],
71    /// );
72    /// db.create_table(schema).unwrap();
73    ///
74    /// // Insert a row
75    /// db.insert_row(
76    ///     "employees",
77    ///     vibesql_storage::Row::new(vec![SqlValue::Integer(1), SqlValue::Integer(50000)]),
78    /// )
79    /// .unwrap();
80    ///
81    /// // Update salary
82    /// let stmt = UpdateStmt {
83    ///     table_name: "employees".to_string(),
84    ///     assignments: vec![Assignment {
85    ///         column: "salary".to_string(),
86    ///         value: Expression::Literal(SqlValue::Integer(60000)),
87    ///     }],
88    ///     where_clause: None,
89    /// };
90    ///
91    /// let count = UpdateExecutor::execute(&stmt, &mut db).unwrap();
92    /// assert_eq!(count, 1);
93    /// ```
94    pub fn execute(stmt: &UpdateStmt, database: &mut Database) -> Result<usize, ExecutorError> {
95        Self::execute_internal(stmt, database, None, None, None)
96    }
97
98    /// Execute an UPDATE statement with procedural context
99    /// Supports procedural variables in SET and WHERE clauses
100    pub fn execute_with_procedural_context(
101        stmt: &UpdateStmt,
102        database: &mut Database,
103        procedural_context: &crate::procedural::ExecutionContext,
104    ) -> Result<usize, ExecutorError> {
105        Self::execute_internal(stmt, database, None, Some(procedural_context), None)
106    }
107
108    /// Execute an UPDATE statement with trigger context
109    /// This allows UPDATE statements within trigger bodies to reference OLD/NEW pseudo-variables
110    pub fn execute_with_trigger_context(
111        stmt: &UpdateStmt,
112        database: &mut Database,
113        trigger_context: &crate::trigger_execution::TriggerContext,
114    ) -> Result<usize, ExecutorError> {
115        Self::execute_internal(stmt, database, None, None, Some(trigger_context))
116    }
117
118    /// Execute an UPDATE statement with optional pre-fetched schema
119    ///
120    /// This method allows cursor-level schema caching to reduce redundant catalog lookups.
121    /// If schema is provided, skips the catalog lookup step.
122    ///
123    /// # Arguments
124    ///
125    /// * `stmt` - The UPDATE statement AST node
126    /// * `database` - The database to update
127    /// * `schema` - Optional pre-fetched schema (from cursor cache)
128    ///
129    /// # Returns
130    ///
131    /// Number of rows updated or error
132    pub fn execute_with_schema(
133        stmt: &UpdateStmt,
134        database: &mut Database,
135        schema: Option<&vibesql_catalog::TableSchema>,
136    ) -> Result<usize, ExecutorError> {
137        Self::execute_internal(stmt, database, schema, None, None)
138    }
139
140    /// Internal implementation supporting both schema caching, procedural context, and trigger context
141    fn execute_internal(
142        stmt: &UpdateStmt,
143        database: &mut Database,
144        schema: Option<&vibesql_catalog::TableSchema>,
145        procedural_context: Option<&crate::procedural::ExecutionContext>,
146        trigger_context: Option<&crate::trigger_execution::TriggerContext>,
147    ) -> Result<usize, ExecutorError> {
148        // Check UPDATE privilege on the table
149        PrivilegeChecker::check_update(database, &stmt.table_name)?;
150
151        // Step 1: Get table schema - clone it to avoid borrow issues
152        // We need owned schema because we take mutable references to database later
153        let schema_owned: vibesql_catalog::TableSchema = if let Some(s) = schema {
154            s.clone()
155        } else {
156            database
157                .catalog
158                .get_table(&stmt.table_name)
159                .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?
160                .clone()
161        };
162        let schema = &schema_owned;
163
164        // Check if table has UPDATE triggers (check once, use multiple times)
165        let has_triggers = trigger_context.is_none()
166            && database
167                .catalog
168                .get_triggers_for_table(&stmt.table_name, Some(vibesql_ast::TriggerEvent::Update(None)))
169                .next()
170                .is_some();
171
172        // Try fast path for simple single-row PK updates without triggers
173        // Conditions: no triggers, no procedural context, simple WHERE pk = value
174        if !has_triggers && procedural_context.is_none() && trigger_context.is_none() {
175            if let Some(result) = Self::try_fast_path_update(stmt, database, schema)? {
176                return Ok(result);
177            }
178        }
179
180        // Fire BEFORE STATEMENT triggers only if triggers exist
181        if has_triggers {
182            crate::TriggerFirer::execute_before_statement_triggers(
183                database,
184                &stmt.table_name,
185                vibesql_ast::TriggerEvent::Update(None),
186            )?;
187        }
188
189        // Get PK indices without cloning entire schema
190        let pk_indices = schema.get_primary_key_indices();
191
192        // Step 2: Get table from storage (for reading rows)
193        let table = database
194            .get_table(&stmt.table_name)
195            .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
196
197        // Step 3: Create expression evaluator with database reference for subquery support
198        //         and optional procedural/trigger context for variable resolution
199        let evaluator = if let Some(ctx) = trigger_context {
200            // Trigger context takes precedence (trigger statements can't have procedural context)
201            ExpressionEvaluator::with_trigger_context(schema, database, ctx)
202        } else if let Some(ctx) = procedural_context {
203            ExpressionEvaluator::with_procedural_context(schema, database, ctx)
204        } else {
205            ExpressionEvaluator::with_database(schema, database)
206        };
207
208        // Step 4: Select rows to update using RowSelector
209        let row_selector = RowSelector::new(schema, &evaluator);
210        let candidate_rows = row_selector.select_rows(table, &stmt.where_clause)?;
211
212        // Step 5: Create value updater
213        let value_updater = ValueUpdater::new(schema, &evaluator, &stmt.table_name);
214
215        // Step 6: Build list of updates (two-phase execution for SQL semantics)
216        // Each update consists of: (row_index, old_row, new_row, changed_columns, updates_pk)
217        let mut updates: Vec<(
218            usize,
219            vibesql_storage::Row,
220            vibesql_storage::Row,
221            std::collections::HashSet<usize>,
222            bool, // whether PK is being updated
223        )> = Vec::new();
224
225        for (row_index, row) in candidate_rows {
226            // Clear CSE cache before evaluating assignment expressions for this row
227            // to prevent cached column values from previous rows
228            evaluator.clear_cse_cache();
229
230            // Apply assignments to build updated row
231            let (new_row, changed_columns) =
232                value_updater.apply_assignments(&row, &stmt.assignments)?;
233
234            // Check if primary key is being updated
235            let updates_pk = if let Some(ref pk_idx) = pk_indices {
236                stmt.assignments.iter().any(|a| {
237                    let col_index = schema.get_column_index(&a.column).unwrap();
238                    pk_idx.contains(&col_index)
239                })
240            } else {
241                false
242            };
243
244            // Validate all constraints (NOT NULL, PRIMARY KEY, UNIQUE, CHECK)
245            let constraint_validator = ConstraintValidator::new(schema);
246            constraint_validator.validate_row(
247                table,
248                &stmt.table_name,
249                row_index,
250                &new_row,
251                &row,
252            )?;
253
254            // Validate user-defined UNIQUE indexes (CREATE UNIQUE INDEX)
255            constraint_validator.validate_unique_indexes(
256                database,
257                &stmt.table_name,
258                &new_row,
259                &row,
260            )?;
261
262            // Enforce FOREIGN KEY constraints (child table)
263            if !schema.foreign_keys.is_empty() {
264                ForeignKeyValidator::validate_constraints(
265                    database,
266                    &stmt.table_name,
267                    &new_row.values,
268                )?;
269            }
270
271            updates.push((row_index, row.clone(), new_row, changed_columns, updates_pk));
272        }
273
274        // Step 7: Handle CASCADE updates for primary key changes (before triggers)
275        // This must happen after validation but before applying parent updates
276        for (_row_index, old_row, new_row, _changed_columns, updates_pk) in &updates {
277            if *updates_pk {
278                ForeignKeyValidator::check_no_child_references(
279                    database,
280                    &stmt.table_name,
281                    old_row,
282                    new_row,
283                )?;
284            }
285        }
286
287        // Fire BEFORE UPDATE triggers for all rows (before database mutation)
288        if has_triggers {
289            for (_row_index, old_row, new_row, _changed_columns, _updates_pk) in &updates {
290                crate::TriggerFirer::execute_before_triggers(
291                    database,
292                    &stmt.table_name,
293                    vibesql_ast::TriggerEvent::Update(None),
294                    Some(old_row),
295                    Some(new_row),
296                )?;
297            }
298        }
299
300        // Step 8: Apply all updates (after evaluation phase completes)
301        let update_count = updates.len();
302
303        // Get mutable table reference
304        let table_mut = database
305            .get_table_mut(&stmt.table_name)
306            .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
307
308        // Collect the updates first
309        let mut index_updates = Vec::new();
310        for (index, old_row, new_row, changed_columns, _updates_pk) in &updates {
311            table_mut
312                .update_row_selective(*index, new_row.clone(), changed_columns)
313                .map_err(|e| ExecutorError::StorageError(e.to_string()))?;
314
315            index_updates.push((*index, old_row.clone(), new_row.clone()));
316        }
317
318        // Fire AFTER UPDATE triggers for all updated rows
319        if has_triggers {
320            for (_index, old_row, new_row) in &index_updates {
321                crate::TriggerFirer::execute_after_triggers(
322                    database,
323                    &stmt.table_name,
324                    vibesql_ast::TriggerEvent::Update(None),
325                    Some(old_row),
326                    Some(new_row),
327                )?;
328            }
329        }
330
331        // Now update user-defined indexes after releasing table borrow
332        for (index, old_row, new_row) in index_updates {
333            database.update_indexes_for_update(&stmt.table_name, &old_row, &new_row, index);
334        }
335
336        // Invalidate columnar cache since table data has changed
337        if update_count > 0 {
338            database.invalidate_columnar_cache(&stmt.table_name);
339        }
340
341        // Fire AFTER STATEMENT triggers only if triggers exist
342        if has_triggers {
343            crate::TriggerFirer::execute_after_statement_triggers(
344                database,
345                &stmt.table_name,
346                vibesql_ast::TriggerEvent::Update(None),
347            )?;
348        }
349
350        Ok(update_count)
351    }
352
353    /// Try to execute UPDATE via fast path for simple single-row PK updates.
354    /// Returns Some(count) if fast path succeeded, None if we should use normal path.
355    ///
356    /// Fast path conditions:
357    /// - WHERE clause is simple equality on single-column primary key
358    /// - No foreign keys to validate
359    /// - Table has a primary key index
360    fn try_fast_path_update(
361        stmt: &UpdateStmt,
362        database: &mut Database,
363        schema: &vibesql_catalog::TableSchema,
364    ) -> Result<Option<usize>, ExecutorError> {
365        // Check if we have a simple PK lookup in WHERE clause
366        let where_clause = match &stmt.where_clause {
367            Some(vibesql_ast::WhereClause::Condition(expr)) => expr,
368            _ => return Ok(None), // No WHERE or CURRENT OF - use normal path
369        };
370
371        // Extract PK value from WHERE clause
372        let pk_value = match Self::extract_pk_equality(where_clause, schema) {
373            Some(val) => val,
374            None => return Ok(None), // Not a simple PK equality
375        };
376
377        // Get table and check for PK index
378        let table = database
379            .get_table(&stmt.table_name)
380            .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
381
382        let pk_index = match table.primary_key_index() {
383            Some(idx) => idx,
384            None => return Ok(None), // No PK index
385        };
386
387        // Look up row by PK
388        let row_index = match pk_index.get(&pk_value) {
389            Some(&idx) => idx,
390            None => return Ok(Some(0)), // Row not found - 0 rows updated
391        };
392
393        // Skip fast path if table has foreign keys (need validation)
394        if !schema.foreign_keys.is_empty() {
395            return Ok(None);
396        }
397
398        // Skip fast path if table has unique constraints (need validation)
399        if !schema.unique_constraints.is_empty() {
400            return Ok(None);
401        }
402
403        // Check if we're updating PK columns - if so, check for CASCADE requirements
404        if let Some(ref pk_idx) = schema.get_primary_key_indices() {
405            let updates_pk = stmt.assignments.iter().any(|a| {
406                schema
407                    .get_column_index(&a.column)
408                    .map(|idx| pk_idx.contains(&idx))
409                    .unwrap_or(false)
410            });
411            if updates_pk {
412                // Check if ANY table in database has foreign keys (might need CASCADE)
413                let has_any_fks = database.catalog.list_tables().iter().any(|table_name| {
414                    database
415                        .catalog
416                        .get_table(table_name)
417                        .map(|s| !s.foreign_keys.is_empty())
418                        .unwrap_or(false)
419                });
420                if has_any_fks {
421                    return Ok(None); // Use normal path for CASCADE handling
422                }
423            }
424        }
425
426        // Get the old row
427        let old_row = table.scan()[row_index].clone();
428
429        // Create evaluator for expression evaluation
430        let evaluator = ExpressionEvaluator::with_database(schema, database);
431
432        // Apply assignments
433        let mut new_row = old_row.clone();
434        let mut changed_columns = std::collections::HashSet::new();
435
436        for assignment in &stmt.assignments {
437            let col_index = schema.get_column_index(&assignment.column).ok_or_else(|| {
438                ExecutorError::ColumnNotFound {
439                    column_name: assignment.column.clone(),
440                    table_name: stmt.table_name.clone(),
441                    searched_tables: vec![stmt.table_name.clone()],
442                    available_columns: schema.columns.iter().map(|c| c.name.clone()).collect(),
443                }
444            })?;
445
446            let new_value = match &assignment.value {
447                vibesql_ast::Expression::Default => {
448                    let column = &schema.columns[col_index];
449                    if let Some(default_expr) = &column.default_value {
450                        match default_expr {
451                            vibesql_ast::Expression::Literal(lit) => lit.clone(),
452                            _ => return Ok(None), // Complex default - use normal path
453                        }
454                    } else {
455                        vibesql_types::SqlValue::Null
456                    }
457                }
458                _ => evaluator.eval(&assignment.value, &old_row)?,
459            };
460
461            new_row
462                .set(col_index, new_value)
463                .map_err(|e| ExecutorError::StorageError(e.to_string()))?;
464            changed_columns.insert(col_index);
465        }
466
467        // Quick constraint validation (NOT NULL only for changed columns)
468        for &col_idx in &changed_columns {
469            let column = &schema.columns[col_idx];
470            if !column.nullable && new_row.values[col_idx] == vibesql_types::SqlValue::Null {
471                return Err(ExecutorError::ConstraintViolation(format!(
472                    "NOT NULL constraint violation: column '{}' cannot be NULL",
473                    column.name
474                )));
475            }
476        }
477
478        // Check PK uniqueness if updating PK columns
479        let pk_indices = schema.get_primary_key_indices();
480        if let Some(ref pk_idx) = pk_indices {
481            let updates_pk = changed_columns.iter().any(|c| pk_idx.contains(c));
482            if updates_pk {
483                // PK is being updated - need to check uniqueness
484                let new_pk: Vec<_> = pk_idx.iter().map(|&i| new_row.values[i].clone()).collect();
485                if let Some(pk_index) = table.primary_key_index() {
486                    if let Some(&existing_idx) = pk_index.get(&new_pk) {
487                        if existing_idx != row_index {
488                            return Err(ExecutorError::ConstraintViolation(format!(
489                                "PRIMARY KEY constraint violation: duplicate key {:?} on {}",
490                                new_pk, stmt.table_name
491                            )));
492                        }
493                    }
494                }
495            }
496        }
497
498        // Apply the update directly
499        let table_mut = database
500            .get_table_mut(&stmt.table_name)
501            .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
502
503        table_mut
504            .update_row_selective(row_index, new_row.clone(), &changed_columns)
505            .map_err(|e| ExecutorError::StorageError(e.to_string()))?;
506
507        // Update user-defined indexes
508        database.update_indexes_for_update(&stmt.table_name, &old_row, &new_row, row_index);
509
510        // Invalidate columnar cache
511        database.invalidate_columnar_cache(&stmt.table_name);
512
513        Ok(Some(1))
514    }
515
516    /// Extract primary key value from a simple equality expression.
517    /// Returns Some(pk_values) if expression is `pk_column = literal` or `literal = pk_column`.
518    fn extract_pk_equality(
519        expr: &Expression,
520        schema: &vibesql_catalog::TableSchema,
521    ) -> Option<Vec<vibesql_types::SqlValue>> {
522        if let Expression::BinaryOp { left, op: BinaryOperator::Equal, right } = expr {
523            // Check: column = literal
524            if let (Expression::ColumnRef { column, .. }, Expression::Literal(value)) =
525                (left.as_ref(), right.as_ref())
526            {
527                if let Some(pk_indices) = schema.get_primary_key_indices() {
528                    if let Some(col_index) = schema.get_column_index(column) {
529                        if pk_indices.len() == 1 && pk_indices[0] == col_index {
530                            return Some(vec![value.clone()]);
531                        }
532                    }
533                }
534            }
535
536            // Check: literal = column
537            if let (Expression::Literal(value), Expression::ColumnRef { column, .. }) =
538                (left.as_ref(), right.as_ref())
539            {
540                if let Some(pk_indices) = schema.get_primary_key_indices() {
541                    if let Some(col_index) = schema.get_column_index(column) {
542                        if pk_indices.len() == 1 && pk_indices[0] == col_index {
543                            return Some(vec![value.clone()]);
544                        }
545                    }
546                }
547            }
548        }
549        None
550    }
551}
552
553/// Execute an UPDATE statement with trigger context
554/// This function is used when executing UPDATE statements within trigger bodies
555/// to support OLD/NEW pseudo-variable references
556pub fn execute_update_with_trigger_context(
557    database: &mut Database,
558    stmt: &UpdateStmt,
559    trigger_context: &crate::trigger_execution::TriggerContext,
560) -> Result<usize, ExecutorError> {
561    UpdateExecutor::execute_with_trigger_context(stmt, database, trigger_context)
562}