use std::collections::{HashMap, HashSet};
use vibesql_ast::{BinaryOperator, Expression, UpdateStmt};
use vibesql_storage::Database;
use vibesql_types::SqlValue;
use crate::{
errors::ExecutorError,
evaluator::{coercion::coerce_value_to_column_type, ExpressionEvaluator},
expression_index_maintenance,
insert::validation::coerce_value,
partial_index_maintenance,
};
pub(super) fn try_fast_path_update(
stmt: &UpdateStmt,
database: &mut Database,
schema: &vibesql_catalog::TableSchema,
) -> Result<Option<usize>, ExecutorError> {
let table_name = &schema.name;
let where_clause = match &stmt.where_clause {
Some(vibesql_ast::WhereClause::Condition(expr)) => expr,
_ => return Ok(None), };
let pk_value = match extract_pk_equality(where_clause, schema) {
Some(val) => val,
None => return Ok(None), };
let pk_value: Vec<SqlValue> = {
let pk_indices = match schema.get_primary_key_indices() {
Some(indices) => indices,
None => return Ok(None),
};
pk_value
.into_iter()
.zip(pk_indices.iter())
.map(|(val, &idx)| coerce_value_to_column_type(val, &schema.columns[idx].data_type))
.collect()
};
let row_index = {
let table = database
.get_table(table_name)
.ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
let pk_index = match table.primary_key_index() {
Some(idx) => idx,
None => return Ok(None), };
match pk_index.get(&pk_value) {
Some(&idx) => idx,
None => return Ok(Some(0)), }
};
if let Some(result) = try_super_fast_path(stmt, database, schema, row_index)? {
return Ok(Some(result));
}
if !schema.foreign_keys.is_empty() {
return Ok(None);
}
if !schema.unique_constraints.is_empty() {
return Ok(None);
}
if let Some(ref pk_idx) = schema.get_primary_key_indices() {
let updates_pk = stmt.assignments.iter().any(|a| {
schema.get_column_index(&a.column).map(|idx| pk_idx.contains(&idx)).unwrap_or(false)
});
if updates_pk {
let has_any_fks = database.catalog.list_tables().iter().any(|table_name| {
database
.catalog
.get_table(table_name)
.map(|s| !s.foreign_keys.is_empty())
.unwrap_or(false)
});
if has_any_fks {
return Ok(None); }
}
}
let table = database
.get_table(table_name)
.ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
let old_row = table.scan()[row_index].clone();
let evaluator = ExpressionEvaluator::with_database(schema, database);
let mut new_row = old_row.clone();
let mut changed_columns = HashSet::new();
for assignment in &stmt.assignments {
let col_name_lower = assignment.column.to_lowercase();
let is_rowid =
col_name_lower == "rowid" || col_name_lower == "_rowid_" || col_name_lower == "oid";
if is_rowid {
return Ok(None); }
let col_index = schema.get_column_index(&assignment.column).ok_or_else(|| {
ExecutorError::ColumnNotFound {
column_name: assignment.column.clone(),
table_name: stmt.table_name.clone(),
searched_tables: vec![stmt.table_name.clone()],
available_columns: schema.columns.iter().map(|c| c.name.clone()).collect(),
}
})?;
let new_value = match &assignment.value {
vibesql_ast::Expression::Default => {
let column = &schema.columns[col_index];
if let Some(default_expr) = &column.default_value {
match default_expr {
vibesql_ast::Expression::Literal(lit) => lit.clone(),
_ => return Ok(None), }
} else {
SqlValue::Null
}
}
_ => evaluator.eval(&assignment.value, &old_row)?,
};
let column = &schema.columns[col_index];
let coerced_value = coerce_value(new_value, &column.data_type)?;
new_row
.set(col_index, coerced_value)
.map_err(|e| ExecutorError::StorageError(e.to_string()))?;
changed_columns.insert(col_index);
}
for &col_idx in &changed_columns {
let column = &schema.columns[col_idx];
if !column.nullable && new_row.values[col_idx] == SqlValue::Null {
return Err(ExecutorError::ConstraintViolation(format!(
"NOT NULL constraint violation: column '{}' cannot be NULL",
column.name
)));
}
}
let pk_indices = schema.get_primary_key_indices();
if let Some(ref pk_idx) = pk_indices {
let updates_pk = changed_columns.iter().any(|c| pk_idx.contains(c));
if updates_pk {
let new_pk: Vec<_> = pk_idx.iter().map(|&i| new_row.values[i].clone()).collect();
if let Some(pk_index) = table.primary_key_index() {
if let Some(&existing_idx) = pk_index.get(&new_pk) {
if existing_idx != row_index {
return Err(ExecutorError::ConstraintViolation(format!(
"PRIMARY KEY constraint violation: duplicate key {:?} on {}",
new_pk, stmt.table_name
)));
}
}
}
}
}
database.update_indexes_for_update(
table_name,
&old_row,
&new_row,
row_index,
Some(&changed_columns),
);
expression_index_maintenance::maintain_expression_indexes_for_update(
database, table_name, &old_row, &new_row, row_index,
);
partial_index_maintenance::maintain_partial_indexes_for_update(
database, table_name, &old_row, &new_row, row_index,
);
let txn_id = database.transaction_id();
vibesql_storage::stamp_xmin_for_write(&mut new_row, txn_id);
new_row.xmax = None;
let table_mut = database
.get_table_mut(table_name)
.ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
table_mut.update_row_unchecked(row_index, new_row, &old_row, &changed_columns);
Ok(Some(1))
}
fn try_super_fast_path(
stmt: &UpdateStmt,
database: &mut Database,
schema: &vibesql_catalog::TableSchema,
row_index: usize,
) -> Result<Option<usize>, ExecutorError> {
let table_name = &schema.name;
let mut inplace_updates: Vec<(usize, SqlValue)> = Vec::new();
let pk_indices = schema.get_primary_key_indices();
for assignment in &stmt.assignments {
let col_name_lower = assignment.column.to_lowercase();
let is_rowid =
col_name_lower == "rowid" || col_name_lower == "_rowid_" || col_name_lower == "oid";
if is_rowid {
return Ok(None); }
let new_value = match &assignment.value {
vibesql_ast::Expression::Literal(val) => val.clone(),
_ => return Ok(None), };
let col_index = match schema.get_column_index(&assignment.column) {
Some(idx) => idx,
None => return Ok(None), };
let is_pk_col = pk_indices.as_ref().map(|pk| pk.contains(&col_index)).unwrap_or(false);
if is_pk_col {
return Ok(None); }
let col_name_upper = assignment.column.to_uppercase();
let is_unique_col = schema
.unique_constraints
.iter()
.any(|uc| uc.iter().any(|name| name.to_uppercase() == col_name_upper));
if is_unique_col {
return Ok(None); }
let column = &schema.columns[col_index];
let coerced_value = coerce_value(new_value, &column.data_type)?;
if !column.nullable && coerced_value == SqlValue::Null {
return Err(ExecutorError::ConstraintViolation(format!(
"NOT NULL constraint violation: column '{}' cannot be NULL",
column.name
)));
}
if database.has_index_on_column(table_name, &assignment.column) {
return Ok(None); }
if database.has_partial_indexes(table_name) {
return Ok(None);
}
inplace_updates.push((col_index, coerced_value));
}
if inplace_updates.is_empty() {
return Ok(None); }
#[cfg(feature = "mvcc_enabled")]
let txn_id = database.transaction_id();
let table_mut = database
.get_table_mut(table_name)
.ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
for (col_index, new_value) in inplace_updates {
table_mut.update_column_inplace(row_index, col_index, new_value);
}
#[cfg(feature = "mvcc_enabled")]
if let Some(id) = txn_id {
table_mut.stamp_row_xmin_inplace(row_index, id);
}
Ok(Some(1))
}
pub(super) fn extract_pk_equality(
expr: &Expression,
schema: &vibesql_catalog::TableSchema,
) -> Option<Vec<SqlValue>> {
let pk_indices = schema.get_primary_key_indices()?;
if pk_indices.is_empty() {
return None;
}
let mut equalities: HashMap<usize, SqlValue> = HashMap::new();
collect_pk_equalities(expr, schema, &mut equalities);
let mut pk_values = Vec::with_capacity(pk_indices.len());
for &pk_col in &pk_indices {
match equalities.get(&pk_col) {
Some(value) => pk_values.push(value.clone()),
None => return None, }
}
Some(pk_values)
}
fn collect_pk_equalities(
expr: &Expression,
schema: &vibesql_catalog::TableSchema,
equalities: &mut HashMap<usize, SqlValue>,
) {
match expr {
Expression::BinaryOp { left, op: BinaryOperator::And, right } => {
collect_pk_equalities(left, schema, equalities);
collect_pk_equalities(right, schema, equalities);
}
Expression::Conjunction(exprs) => {
for e in exprs {
collect_pk_equalities(e, schema, equalities);
}
}
Expression::BinaryOp { left, op: BinaryOperator::Equal, right } => {
if let (Expression::ColumnRef(col_id), Expression::Literal(value)) =
(left.as_ref(), right.as_ref())
{
if let Some(col_index) = schema.get_column_index(col_id.column_canonical()) {
equalities.insert(col_index, value.clone());
}
}
else if let (Expression::Literal(value), Expression::ColumnRef(col_id)) =
(left.as_ref(), right.as_ref())
{
if let Some(col_index) = schema.get_column_index(col_id.column_canonical()) {
equalities.insert(col_index, value.clone());
}
}
}
_ => {} }
}