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 constraints::ConstraintValidator;
28use foreign_keys::ForeignKeyValidator;
29use row_selector::RowSelector;
30use value_updater::ValueUpdater;
31use vibesql_ast::{BinaryOperator, Expression, UpdateStmt};
32use vibesql_storage::Database;
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(
169                    &stmt.table_name,
170                    Some(vibesql_ast::TriggerEvent::Update(None)),
171                )
172                .next()
173                .is_some();
174
175        // Try fast path for simple single-row PK updates without triggers
176        // Conditions: no triggers, no procedural context, simple WHERE pk = value
177        if !has_triggers && procedural_context.is_none() && trigger_context.is_none() {
178            if let Some(result) = Self::try_fast_path_update(stmt, database, schema)? {
179                return Ok(result);
180            }
181        }
182
183        // Fire BEFORE STATEMENT triggers only if triggers exist
184        if has_triggers {
185            crate::TriggerFirer::execute_before_statement_triggers(
186                database,
187                &stmt.table_name,
188                vibesql_ast::TriggerEvent::Update(None),
189            )?;
190        }
191
192        // Get PK indices without cloning entire schema
193        let pk_indices = schema.get_primary_key_indices();
194
195        // Step 2: Get table from storage (for reading rows)
196        let table = database
197            .get_table(&stmt.table_name)
198            .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
199
200        // Step 3: Create expression evaluator with database reference for subquery support
201        //         and optional procedural/trigger context for variable resolution
202        let evaluator = if let Some(ctx) = trigger_context {
203            // Trigger context takes precedence (trigger statements can't have procedural context)
204            ExpressionEvaluator::with_trigger_context(schema, database, ctx)
205        } else if let Some(ctx) = procedural_context {
206            ExpressionEvaluator::with_procedural_context(schema, database, ctx)
207        } else {
208            ExpressionEvaluator::with_database(schema, database)
209        };
210
211        // Step 4: Select rows to update using RowSelector
212        let row_selector = RowSelector::new(schema, &evaluator);
213        let candidate_rows = row_selector.select_rows(table, &stmt.where_clause)?;
214
215        // Step 5: Create value updater
216        let value_updater = ValueUpdater::new(schema, &evaluator, &stmt.table_name);
217
218        // Step 6: Build list of updates (two-phase execution for SQL semantics)
219        // Each update consists of: (row_index, old_row, new_row, changed_columns, updates_pk)
220        let mut updates: Vec<(
221            usize,
222            vibesql_storage::Row,
223            vibesql_storage::Row,
224            std::collections::HashSet<usize>,
225            bool, // whether PK is being updated
226        )> = Vec::new();
227
228        for (row_index, row) in candidate_rows {
229            // Clear CSE cache before evaluating assignment expressions for this row
230            // to prevent cached column values from previous rows
231            evaluator.clear_cse_cache();
232
233            // Apply assignments to build updated row
234            let (new_row, changed_columns) =
235                value_updater.apply_assignments(&row, &stmt.assignments)?;
236
237            // Check if primary key is being updated
238            let updates_pk = if let Some(ref pk_idx) = pk_indices {
239                stmt.assignments.iter().any(|a| {
240                    let col_index = schema.get_column_index(&a.column).unwrap();
241                    pk_idx.contains(&col_index)
242                })
243            } else {
244                false
245            };
246
247            // Validate all constraints (NOT NULL, PRIMARY KEY, UNIQUE, CHECK)
248            let constraint_validator = ConstraintValidator::new(schema);
249            constraint_validator.validate_row(
250                table,
251                &stmt.table_name,
252                row_index,
253                &new_row,
254                &row,
255            )?;
256
257            // Validate user-defined UNIQUE indexes (CREATE UNIQUE INDEX)
258            constraint_validator.validate_unique_indexes(
259                database,
260                &stmt.table_name,
261                &new_row,
262                &row,
263            )?;
264
265            // Enforce FOREIGN KEY constraints (child table)
266            if !schema.foreign_keys.is_empty() {
267                ForeignKeyValidator::validate_constraints(
268                    database,
269                    &stmt.table_name,
270                    &new_row.values,
271                )?;
272            }
273
274            updates.push((row_index, row.clone(), new_row, changed_columns, updates_pk));
275        }
276
277        // Step 7: Handle CASCADE updates for primary key changes (before triggers)
278        // This must happen after validation but before applying parent updates
279        for (_row_index, old_row, new_row, _changed_columns, updates_pk) in &updates {
280            if *updates_pk {
281                ForeignKeyValidator::check_no_child_references(
282                    database,
283                    &stmt.table_name,
284                    old_row,
285                    new_row,
286                )?;
287            }
288        }
289
290        // Fire BEFORE UPDATE triggers for all rows (before database mutation)
291        if has_triggers {
292            for (_row_index, old_row, new_row, _changed_columns, _updates_pk) in &updates {
293                crate::TriggerFirer::execute_before_triggers(
294                    database,
295                    &stmt.table_name,
296                    vibesql_ast::TriggerEvent::Update(None),
297                    Some(old_row),
298                    Some(new_row),
299                )?;
300            }
301        }
302
303        // Step 8: Apply all updates (after evaluation phase completes)
304        let update_count = updates.len();
305
306        // Get mutable table reference
307        let table_mut = database
308            .get_table_mut(&stmt.table_name)
309            .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
310
311        // Collect the updates first
312        let mut index_updates = Vec::new();
313        for (index, old_row, new_row, changed_columns, _updates_pk) in &updates {
314            table_mut
315                .update_row_selective(*index, new_row.clone(), changed_columns)
316                .map_err(|e| ExecutorError::StorageError(e.to_string()))?;
317
318            index_updates.push((*index, old_row.clone(), new_row.clone()));
319        }
320
321        // Fire AFTER UPDATE triggers for all updated rows
322        if has_triggers {
323            for (_index, old_row, new_row) in &index_updates {
324                crate::TriggerFirer::execute_after_triggers(
325                    database,
326                    &stmt.table_name,
327                    vibesql_ast::TriggerEvent::Update(None),
328                    Some(old_row),
329                    Some(new_row),
330                )?;
331            }
332        }
333
334        // Now update user-defined indexes after releasing table borrow
335        for (index, old_row, new_row) in index_updates {
336            database.update_indexes_for_update(&stmt.table_name, &old_row, &new_row, index);
337        }
338
339        // Invalidate columnar cache since table data has changed
340        if update_count > 0 {
341            database.invalidate_columnar_cache(&stmt.table_name);
342        }
343
344        // Fire AFTER STATEMENT triggers only if triggers exist
345        if has_triggers {
346            crate::TriggerFirer::execute_after_statement_triggers(
347                database,
348                &stmt.table_name,
349                vibesql_ast::TriggerEvent::Update(None),
350            )?;
351        }
352
353        Ok(update_count)
354    }
355
356    /// Try to execute UPDATE via fast path for simple single-row PK updates.
357    /// Returns Some(count) if fast path succeeded, None if we should use normal path.
358    ///
359    /// Fast path conditions:
360    /// - WHERE clause is simple equality on single-column primary key
361    /// - No foreign keys to validate
362    /// - Table has a primary key index
363    fn try_fast_path_update(
364        stmt: &UpdateStmt,
365        database: &mut Database,
366        schema: &vibesql_catalog::TableSchema,
367    ) -> Result<Option<usize>, ExecutorError> {
368        // Check if we have a simple PK lookup in WHERE clause
369        let where_clause = match &stmt.where_clause {
370            Some(vibesql_ast::WhereClause::Condition(expr)) => expr,
371            _ => return Ok(None), // No WHERE or CURRENT OF - use normal path
372        };
373
374        // Extract PK value from WHERE clause
375        let pk_value = match Self::extract_pk_equality(where_clause, schema) {
376            Some(val) => val,
377            None => return Ok(None), // Not a simple PK equality
378        };
379
380        // Get table and check for PK index
381        let table = database
382            .get_table(&stmt.table_name)
383            .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
384
385        let pk_index = match table.primary_key_index() {
386            Some(idx) => idx,
387            None => return Ok(None), // No PK index
388        };
389
390        // Look up row by PK
391        let row_index = match pk_index.get(&pk_value) {
392            Some(&idx) => idx,
393            None => return Ok(Some(0)), // Row not found - 0 rows updated
394        };
395
396        // Skip fast path if table has foreign keys (need validation)
397        if !schema.foreign_keys.is_empty() {
398            return Ok(None);
399        }
400
401        // Skip fast path if table has unique constraints (need validation)
402        if !schema.unique_constraints.is_empty() {
403            return Ok(None);
404        }
405
406        // Check if we're updating PK columns - if so, check for CASCADE requirements
407        if let Some(ref pk_idx) = schema.get_primary_key_indices() {
408            let updates_pk = stmt.assignments.iter().any(|a| {
409                schema.get_column_index(&a.column).map(|idx| pk_idx.contains(&idx)).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}