Skip to main content

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//! - `fast_path`: Fast path optimizations for single-row PK updates
10//! - `triggers`: Trigger execution and view update handling
11//! - `index_sync`: Index maintenance coordination for REPLACE operations
12//! - `executor`: Core execution orchestration
13//!
14//! The main `UpdateExecutor` orchestrates these components to implement SQL's two-phase
15//! update semantics: first collect all updates evaluating against original rows, then
16//! apply all updates atomically.
17//!
18//! ## Performance Optimizations
19//!
20//! The executor includes a fast path for single-row primary key updates that:
21//! - Skips trigger checks when no triggers exist for the table
22//! - Avoids schema cloning
23//! - Uses single-pass execution instead of two-phase
24//! - Minimizes allocations
25
26mod constraints;
27mod executor;
28mod fast_path;
29mod foreign_keys;
30mod from_clause;
31mod index_sync;
32mod row_selector;
33#[cfg(test)]
34mod tests;
35mod triggers;
36mod value_updater;
37
38use std::collections::HashSet;
39
40use vibesql_ast::UpdateStmt;
41use vibesql_storage::{Database, Row};
42
43use crate::errors::ExecutorError;
44
45/// One pending UPDATE row, collected in the first phase of two-phase UPDATE
46/// execution and applied in the second phase.
47///
48/// Both the default UPDATE path (`executor::execute_internal`) and the
49/// `UPDATE ... FROM ...` path (`executor::execute_update_from`) collect a
50/// `Vec<PendingUpdate>` before validating constraints and writing rows back.
51///
52/// This was previously an anonymous 5-tuple
53/// `(usize, Row, Row, HashSet<usize>, bool)`. The judge on PR #5138 flagged
54/// the tuple as load-bearing once a second consumer existed (UPDATE FROM, in
55/// PR for issue #5140). Naming the fields makes call sites self-documenting
56/// and prevents accidental positional confusion when the shape grows.
57#[derive(Debug, Clone)]
58pub(super) struct PendingUpdate {
59    /// Index of the row in the target table's storage (the rowid in storage
60    /// order, used by `update_row_selective` and index maintenance).
61    pub row_index: usize,
62    /// Original row state — needed for PK/UNIQUE self-check and trigger OLD.
63    pub old_row: Row,
64    /// New row state after assignments (and generated-column recomputation).
65    pub new_row: Row,
66    /// Column indices whose value changed; passed to `update_row_selective`
67    /// and index maintenance so unchanged columns are skipped.
68    pub changed_columns: HashSet<usize>,
69    /// True if any assignment touches a PRIMARY KEY column (or the rowid
70    /// alias for INTEGER PRIMARY KEY tables). Used to gate child-reference
71    /// FK checks.
72    pub updates_pk: bool,
73}
74
75// Re-export for external use
76pub use triggers::execute_update_with_trigger_context;
77
78/// Executor for UPDATE statements
79pub struct UpdateExecutor;
80
81impl UpdateExecutor {
82    /// Execute an UPDATE statement
83    ///
84    /// # Arguments
85    ///
86    /// * `stmt` - The UPDATE statement AST node
87    /// * `database` - The database to update
88    ///
89    /// # Returns
90    ///
91    /// Number of rows updated or error
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use vibesql_ast::{Assignment, Expression, UpdateStmt};
97    /// use vibesql_catalog::{ColumnSchema, TableSchema};
98    /// use vibesql_executor::UpdateExecutor;
99    /// use vibesql_storage::Database;
100    /// use vibesql_types::{DataType, SqlValue};
101    ///
102    /// let mut db = Database::new();
103    ///
104    /// // Create table
105    /// let schema = TableSchema::new(
106    ///     "employees".to_string(),
107    ///     vec![
108    ///         ColumnSchema::new("id".to_string(), DataType::Integer, false),
109    ///         ColumnSchema::new("salary".to_string(), DataType::Integer, false),
110    ///     ],
111    /// );
112    /// db.create_table(schema).unwrap();
113    ///
114    /// // Insert a row
115    /// db.insert_row(
116    ///     "employees",
117    ///     vibesql_storage::Row::new(vec![SqlValue::Integer(1), SqlValue::Integer(50000)]),
118    /// )
119    /// .unwrap();
120    ///
121    /// // Update salary
122    /// let stmt = UpdateStmt {
123    ///     with_clause: None,
124    ///     table_name: "employees".to_string(),
125    ///     quoted: false,
126    ///     alias: None,
127    ///     assignments: vec![Assignment {
128    ///         column: "salary".to_string(),
129    ///         value: Expression::Literal(SqlValue::Integer(60000)),
130    ///     }],
131    ///     from_clause: None,
132    ///     where_clause: None,
133    ///     conflict_clause: None,
134    ///     returning: None,
135    /// };
136    ///
137    /// let count = UpdateExecutor::execute(&stmt, &mut db).unwrap();
138    /// assert_eq!(count, 1);
139    /// ```
140    pub fn execute(stmt: &UpdateStmt, database: &mut Database) -> Result<usize, ExecutorError> {
141        // SQLite per-variant RAISE scope handling for the top-level statement
142        // (#5417): see [`crate::raise_scope::run_top_level_dml`].
143        let may_fire = crate::raise_scope::table_may_fire_trigger(database, &stmt.table_name);
144        crate::raise_scope::run_top_level_dml(database, may_fire, |database| {
145            executor::execute_internal(stmt, database, None, None, None).map(|(count, _)| count)
146        })
147    }
148
149    /// Execute an UPDATE statement, capturing RETURNING rows (SQLite 3.35.0+)
150    ///
151    /// Returns the number of updated rows plus, when the statement carries a
152    /// RETURNING clause, the projected NEW rows (after SET assignments) — one
153    /// per updated row, or one per INSTEAD OF trigger fire for views.
154    ///
155    /// When the statement has no RETURNING clause the second element is `None`.
156    pub fn execute_returning(
157        stmt: &UpdateStmt,
158        database: &mut Database,
159    ) -> Result<(usize, Option<crate::select::SelectResult>), ExecutorError> {
160        // Per-variant RAISE scope for RETURNING DML (#5432, follow-on to #5417):
161        // a RAISE fired from a trigger during a RETURNING UPDATE gets the same
162        // statement-savepoint scope as the bare `execute` path; when it aborts
163        // the statement the error propagates (no rows returned).
164        let may_fire = crate::raise_scope::table_may_fire_trigger(database, &stmt.table_name);
165        crate::raise_scope::run_top_level_dml(database, may_fire, |database| {
166            executor::execute_internal(stmt, database, None, None, None)
167        })
168    }
169
170    /// Execute an UPDATE statement with procedural context
171    /// Supports procedural variables in SET and WHERE clauses
172    pub fn execute_with_procedural_context(
173        stmt: &UpdateStmt,
174        database: &mut Database,
175        procedural_context: &crate::procedural::ExecutionContext,
176    ) -> Result<usize, ExecutorError> {
177        // Per-variant RAISE scope for procedural-context DML (#5432): a RAISE
178        // fired from a trigger inside a procedure/script gets the same
179        // statement-savepoint scope as the bare `execute` path.
180        let may_fire = crate::raise_scope::table_may_fire_trigger(database, &stmt.table_name);
181        crate::raise_scope::run_top_level_dml(database, may_fire, |database| {
182            executor::execute_internal(stmt, database, None, Some(procedural_context), None)
183                .map(|(count, _)| count)
184        })
185    }
186
187    /// Execute an UPDATE statement with trigger context
188    /// This allows UPDATE statements within trigger bodies to reference OLD/NEW pseudo-variables
189    pub fn execute_with_trigger_context(
190        stmt: &UpdateStmt,
191        database: &mut Database,
192        trigger_context: &crate::trigger_execution::TriggerContext,
193    ) -> Result<usize, ExecutorError> {
194        executor::execute_internal(stmt, database, None, None, Some(trigger_context))
195            .map(|(count, _)| count)
196    }
197
198    /// Execute an UPDATE statement with optional pre-fetched schema
199    ///
200    /// This method allows cursor-level schema caching to reduce redundant catalog lookups.
201    /// If schema is provided, skips the catalog lookup step.
202    ///
203    /// # Arguments
204    ///
205    /// * `stmt` - The UPDATE statement AST node
206    /// * `database` - The database to update
207    /// * `schema` - Optional pre-fetched schema (from cursor cache)
208    ///
209    /// # Returns
210    ///
211    /// Number of rows updated or error
212    pub fn execute_with_schema(
213        stmt: &UpdateStmt,
214        database: &mut Database,
215        schema: Option<&vibesql_catalog::TableSchema>,
216    ) -> Result<usize, ExecutorError> {
217        executor::execute_internal(stmt, database, schema, None, None).map(|(count, _)| count)
218    }
219}