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}