use radixdb_core::{CompactArc, DataType, Error, Result, Row, Value};
use radixdb_sql::ast::{Expression, InsertStatement};
use radixdb_storage::expression::{ComparisonExpr, Expression as StorageExpr};
use radixdb_storage::traits::Table;
use super::dml_support::{validate_coercion, CompiledUpsert};
use super::host::MutationHost;
use super::validation::{compile_table_check_constraints, validate_resulting_row_constraints};
use crate::context::ExecutionContext;
pub(super) fn compile_upsert<T: MutationHost + ?Sized>(
host: &T,
schema: &radixdb_core::Schema,
stmt: &InsertStatement,
) -> Result<CompiledUpsert> {
use crate::expression::{CompileContext, ExprCompiler, SharedProgram};
let col_map = schema.column_index_map();
let mut seen_targets = rustc_hash::FxHashSet::default();
let update_specs: Vec<(usize, DataType, u16, &Expression)> = stmt
.update_columns
.iter()
.zip(stmt.update_expressions.iter())
.map(|(col, expr)| {
let idx = col_map
.get(col.value_lower.as_str())
.copied()
.ok_or_else(|| Error::ColumnNotFound(col.value.to_string()))?;
if !seen_targets.insert(idx) {
return Err(Error::InvalidArgument(format!(
"ON CONFLICT target column '{}' is assigned more than once",
col.value
)));
}
if schema.pk_column_index() == Some(idx) {
return Err(Error::InvalidArgument(format!(
"cannot UPDATE primary key column '{}' in ON CONFLICT",
col.value
)));
}
Ok((
idx,
schema.columns[idx].data_type,
schema.columns[idx].vector_dimensions,
expr,
))
})
.collect::<Result<Vec<_>>>()?;
let column_names: Vec<String> = schema.column_names_owned().to_vec();
let excluded_columns: Vec<String> = column_names
.iter()
.map(|c| format!("excluded.{}", c))
.collect();
let mut compiled_updates: Vec<(usize, DataType, u16, SharedProgram)> =
Vec::with_capacity(update_specs.len());
for (idx, col_type, vec_dims, expr) in &update_specs {
let compile_ctx = CompileContext::new(&column_names, host.mutation_function_registry())
.with_second_row(&excluded_columns);
let compiler = ExprCompiler::new(&compile_ctx);
match compiler.compile(expr) {
Ok(program) => {
compiled_updates.push((*idx, *col_type, *vec_dims, CompactArc::new(program)));
}
Err(e) => {
return Err(Error::internal(format!(
"failed to compile ON CONFLICT update expression: {}",
e
)));
}
}
}
let compiled_table_checks = compile_table_check_constraints(schema)?;
Ok(CompiledUpsert {
compiled_updates,
compiled_table_checks,
})
}
#[allow(clippy::too_many_arguments)]
pub(super) fn apply_on_duplicate_update<T: MutationHost + ?Sized>(
host: &T,
table: &mut Box<dyn Table>,
schema: &radixdb_core::Schema,
row_id: i64,
conflict_column: Option<&str>,
insert_values: &[Value],
compiled: &CompiledUpsert,
ctx: &ExecutionContext,
capture_row: bool,
) -> Result<Option<Row>> {
let pk_col = schema
.pk_column_index()
.map(|idx| schema.columns[idx].name.clone());
let where_expr: Option<Box<dyn StorageExpr>> = if let Some(pk_name) = pk_col {
let mut expr =
ComparisonExpr::new(pk_name, radixdb_core::Operator::Eq, Value::Integer(row_id));
expr.prepare_for_schema(schema);
Some(Box::new(expr))
} else if let Some(conflict_cols) = conflict_column {
use radixdb_storage::expression::AndExpr;
let col_names: Vec<&str> = conflict_cols.split(", ").collect();
let col_map = schema.column_index_map();
let mut comparisons: Vec<Box<dyn StorageExpr>> = Vec::with_capacity(col_names.len());
for col_name in &col_names {
let col_lower = col_name.to_lowercase();
if let Some(&idx) = col_map.get(col_lower.as_str()) {
let value = insert_values
.get(idx)
.cloned()
.unwrap_or(Value::null_unknown());
let mut expr =
ComparisonExpr::new(col_name.to_string(), radixdb_core::Operator::Eq, value);
expr.prepare_for_schema(schema);
comparisons.push(Box::new(expr));
}
}
if comparisons.len() == 1 {
Some(comparisons.pop().unwrap())
} else if comparisons.len() > 1 {
let mut and_expr = AndExpr::new(comparisons);
and_expr.prepare_for_schema(schema);
Some(Box::new(and_expr))
} else {
None
}
} else {
None
};
use crate::expression::{ExecuteContext, ExprVM};
let excluded_row = Row::from_values(insert_values.to_vec());
let mut vm = ExprVM::new();
let params = ctx.params();
let named_params = ctx.named_params();
let mut captured_row: Option<Row> = None;
let table_txn_id = table.txn_id();
let mut setter = |mut row: Row| -> Result<(Row, bool)> {
let updates_to_apply: Vec<(usize, Value)> = {
let mut exec_ctx = ExecuteContext::for_join(&row, &excluded_row);
if !params.is_empty() {
exec_ctx = exec_ctx.with_params(params);
}
if !named_params.is_empty() {
exec_ctx = exec_ctx.with_named_params(named_params);
}
exec_ctx = exec_ctx
.with_transaction_id(ctx.transaction_id())
.with_stored_function_invoker(ctx.stored_function_invoker());
let mut updates = Vec::with_capacity(compiled.compiled_updates.len());
for (idx, col_type, vec_dims, program) in &compiled.compiled_updates {
let v = vm.execute_cow(program, &exec_ctx)?;
let coerced = v.try_coerce_to_type(*col_type)?;
validate_coercion(
&v,
&coerced,
&schema.columns[*idx].name,
*col_type,
*vec_dims,
)?;
if !schema.columns[*idx].nullable && coerced.is_null() {
return Err(Error::not_null_constraint(
schema.columns[*idx].name.clone(),
));
}
updates.push((*idx, coerced));
}
updates
};
let changed = !updates_to_apply.is_empty();
for (idx, new_value) in updates_to_apply {
let _ = row.set(idx, new_value);
}
if changed {
validate_resulting_row_constraints(
schema,
&compiled.compiled_table_checks,
&row,
&mut vm,
)?;
if !schema.foreign_keys.is_empty() {
crate::mutation::foreign_key::check_parent_exists(
host.mutation_engine(),
table_txn_id,
schema,
&row,
)?;
}
}
if capture_row {
captured_row = Some(row.clone());
}
Ok((row, changed))
};
if row_id >= 0 {
table.update_by_row_ids(&[row_id], &mut setter)?;
} else {
table.update(where_expr.as_deref(), &mut setter)?;
}
Ok(captured_row)
}