use vibesql_ast::{
ColumnIdentifier, Expression, FromClause, JoinType, SelectItem, SelectStmt, TriggerTiming,
UpdateStmt, WhereClause,
};
use vibesql_catalog::{ColumnSchema, TableSchema, ViewDefinition};
use vibesql_storage::{Database, Row};
use vibesql_types::{DataType, SqlValue};
use crate::{errors::ExecutorError, evaluator::ExpressionEvaluator};
pub fn execute_update_with_trigger_context(
database: &mut Database,
stmt: &UpdateStmt,
trigger_context: &crate::trigger_execution::TriggerContext,
) -> Result<usize, ExecutorError> {
super::UpdateExecutor::execute_with_trigger_context(stmt, database, trigger_context)
}
pub(super) fn execute_update_on_view(
database: &mut Database,
stmt: &UpdateStmt,
view_def: &ViewDefinition,
procedural_context: Option<&crate::procedural::ExecutionContext>,
trigger_context: Option<&crate::trigger_execution::TriggerContext>,
) -> Result<(usize, Option<crate::select::SelectResult>), ExecutorError> {
let triggers = crate::TriggerFirer::find_triggers(
database,
&view_def.name,
TriggerTiming::InsteadOf,
vibesql_ast::TriggerEvent::Update(None),
);
if triggers.is_empty() {
return Err(ExecutorError::UnsupportedExpression(format!(
"Cannot UPDATE view '{}' without INSTEAD OF trigger",
view_def.name
)));
}
let view_schema = build_view_schema(database, view_def)?;
let updates: Vec<(Row, Row)> = if let Some(ref from_clauses) = stmt.from_clause {
collect_view_updates_with_from(
database,
stmt,
view_def,
&view_schema,
from_clauses,
trigger_context,
)?
} else {
collect_view_updates_no_from(
database,
stmt,
view_def,
&view_schema,
procedural_context,
trigger_context,
)?
};
let rows_processed = updates.len();
for (old_row, new_row) in &updates {
for trigger in &triggers {
if crate::TriggerFirer::execute_trigger(database, trigger, Some(old_row), Some(new_row))?
== crate::TriggerOutcome::SkipRow
{
break;
}
}
}
let returning = if let Some(items) = &stmt.returning {
let new_rows: Vec<&Row> = updates.iter().map(|(_, new_row)| new_row).collect();
Some(crate::dml_returning::project_returning(
items,
&view_schema,
database,
stmt.alias.as_deref(),
&new_rows,
None,
)?)
} else {
None
};
Ok((rows_processed, returning))
}
fn collect_view_updates_no_from(
database: &Database,
stmt: &UpdateStmt,
view_def: &ViewDefinition,
view_schema: &TableSchema,
procedural_context: Option<&crate::procedural::ExecutionContext>,
trigger_context: Option<&crate::trigger_execution::TriggerContext>,
) -> Result<Vec<(Row, Row)>, ExecutorError> {
let select_executor = crate::SelectExecutor::new(database);
let all_rows = select_executor.execute_with_columns(&view_def.query)?;
let evaluator = if let Some(ctx) = trigger_context {
ExpressionEvaluator::with_trigger_context(view_schema, database, ctx)
} else if let Some(ctx) = procedural_context {
ExpressionEvaluator::with_procedural_context(view_schema, database, ctx)
} else {
ExpressionEvaluator::with_database(view_schema, database)
};
let mut collected_updates = Vec::new();
for row in &all_rows.rows {
evaluator.clear_cse_cache();
let matches = match &stmt.where_clause {
Some(WhereClause::Condition(expr)) => {
crate::evaluator::operators::is_truthy(&evaluator.eval(expr, row)?)
}
None => true, Some(WhereClause::CurrentOf(_)) => {
return Err(ExecutorError::UnsupportedExpression(
"CURRENT OF not supported for view updates".to_string(),
));
}
};
if matches {
let old_row = row.clone();
let mut new_row_values = old_row.values.clone();
for assignment in &stmt.assignments {
let col_idx = view_schema
.columns
.iter()
.position(|c| c.name.to_uppercase() == assignment.column.to_uppercase())
.ok_or_else(|| ExecutorError::ColumnNotFound {
column_name: assignment.column.clone(),
table_name: view_def.name.clone(),
searched_tables: vec![view_def.name.clone()],
available_columns: view_schema
.columns
.iter()
.map(|c| c.name.clone())
.collect(),
})?;
let new_value = evaluator.eval(&assignment.value, &old_row)?;
new_row_values[col_idx] = new_value;
}
let new_row = Row::new(new_row_values);
collected_updates.push((old_row, new_row));
}
}
Ok(collected_updates)
}
fn collect_view_updates_with_from(
database: &Database,
stmt: &UpdateStmt,
view_def: &ViewDefinition,
view_schema: &TableSchema,
from_clauses: &[FromClause],
trigger_context: Option<&crate::trigger_execution::TriggerContext>,
) -> Result<Vec<(Row, Row)>, ExecutorError> {
let view_name = &view_def.name;
let view_prefix = stmt.alias.clone().unwrap_or_else(|| view_name.clone());
let view_col_count = view_schema.columns.len();
let mut select_list = Vec::with_capacity(view_col_count + stmt.assignments.len());
for col in &view_schema.columns {
select_list.push(SelectItem::Expression {
expr: Expression::ColumnRef(ColumnIdentifier::qualified(
&view_prefix,
false,
&col.name,
false,
)),
alias: Some(format!("__old_{}__", col.name)),
source_text: None,
});
}
for (i, assignment) in stmt.assignments.iter().enumerate() {
select_list.push(SelectItem::Expression {
expr: assignment.value.clone(),
alias: Some(format!("__set_{}__", i)),
source_text: None,
});
}
let view_from = FromClause::Table {
index_hint: None,
name: view_name.clone(),
alias: stmt.alias.clone(),
column_aliases: None,
quoted: stmt.quoted,
};
let mut combined_from = view_from;
for from_clause in from_clauses {
combined_from = combine_with_from_clause(combined_from, from_clause.clone());
}
let where_clause = match stmt.where_clause.as_ref() {
Some(WhereClause::Condition(expr)) => Some(expr.clone()),
Some(WhereClause::CurrentOf(_)) => {
return Err(ExecutorError::UnsupportedExpression(
"CURRENT OF not supported for view updates".to_string(),
));
}
None => None,
};
let select_stmt = SelectStmt {
with_clause: stmt.with_clause.clone(),
select_list,
distinct: false,
into_table: None,
into_variables: None,
from: Some(combined_from),
where_clause,
group_by: None,
having: None,
window_definitions: None,
order_by: None,
limit: None,
offset: None,
set_operation: None,
values: None,
};
let rows = match trigger_context {
Some(ctx) => {
let executor = crate::SelectExecutor::new_with_trigger_context(database, ctx);
executor.execute(&select_stmt)?
}
None => {
let executor = crate::SelectExecutor::new(database);
executor.execute(&select_stmt)?
}
};
let mut collected_updates: Vec<(Row, Row)> = Vec::new();
for row in rows {
if row.values.len() < view_col_count + stmt.assignments.len() {
return Err(ExecutorError::UnsupportedExpression(format!(
"Internal error: synthetic SELECT for UPDATE on view '{}' produced row with {} \
columns, expected at least {}",
view_name,
row.values.len(),
view_col_count + stmt.assignments.len()
)));
}
let old_values: Vec<SqlValue> = row.values[..view_col_count].to_vec();
let old_row = Row::new(old_values);
let mut new_row_values = old_row.values.clone();
for (i, assignment) in stmt.assignments.iter().enumerate() {
let col_idx = view_schema
.columns
.iter()
.position(|c| c.name.to_uppercase() == assignment.column.to_uppercase())
.ok_or_else(|| ExecutorError::ColumnNotFound {
column_name: assignment.column.clone(),
table_name: view_def.name.clone(),
searched_tables: vec![view_def.name.clone()],
available_columns: view_schema.columns.iter().map(|c| c.name.clone()).collect(),
})?;
new_row_values[col_idx] = row.values[view_col_count + i].clone();
}
let new_row = Row::new(new_row_values);
collected_updates.push((old_row, new_row));
}
Ok(collected_updates)
}
fn combine_with_from_clause(accumulated: FromClause, from_clause: FromClause) -> FromClause {
match from_clause {
FromClause::Table { .. } | FromClause::Subquery { .. } | FromClause::Values { .. } => {
FromClause::Join {
left: Box::new(accumulated),
right: Box::new(from_clause),
join_type: JoinType::Cross,
condition: None,
using_columns: None,
natural: false,
alias: None,
}
}
FromClause::Join { left, right, join_type, condition, using_columns, natural, alias } => {
let new_left = combine_with_from_clause(accumulated, *left);
FromClause::Join {
left: Box::new(new_left),
right,
join_type,
condition,
using_columns,
natural,
alias,
}
}
}
}
pub(super) fn build_view_schema(
database: &Database,
view_def: &ViewDefinition,
) -> Result<TableSchema, ExecutorError> {
let select_executor = crate::SelectExecutor::new(database);
let select_result = select_executor.execute_with_columns(&view_def.query);
let result = if view_def.is_temp() {
select_result?
} else {
select_result.map_err(ExecutorError::with_main_schema_qualifier)?
};
let column_names: Vec<String> =
if let Some(ref cols) = view_def.columns { cols.clone() } else { result.columns.clone() };
let columns: Vec<ColumnSchema> = column_names
.into_iter()
.map(|name| ColumnSchema::new(name, DataType::Null, true))
.collect();
let mut schema = TableSchema::new(view_def.name.clone(), columns);
schema.set_is_view(true);
Ok(schema)
}