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}