use crate::errors::ExecutorError;
use vibesql_ast::{
visitor::{transform_expression, ExpressionMutVisitor, VisitResult},
Assignment, ConflictTargetItem, Expression, FromClause, SelectStmt,
};
use vibesql_types::SqlValue;
use crate::partial_index_maintenance::is_predicate_truthy;
use crate::select::grouping::expressions_equal;
pub enum UpsertAction {
Updated(usize),
Skipped,
NoConflict,
}
enum KeyPart {
Column(usize),
Expr(Expression),
}
struct UniqueCandidate {
parts: Vec<KeyPart>,
predicate: Option<Expression>,
}
fn collect_unique_candidates(
db: &vibesql_storage::Database,
table_name: &str,
schema: &vibesql_catalog::TableSchema,
) -> Vec<UniqueCandidate> {
let mut candidates: Vec<UniqueCandidate> = Vec::new();
if let Some(pk) = schema.get_primary_key_indices() {
if !pk.is_empty() {
candidates.push(UniqueCandidate {
parts: pk.into_iter().map(KeyPart::Column).collect(),
predicate: None,
});
}
}
for unique in schema.get_unique_constraint_indices() {
if !unique.is_empty() {
candidates.push(UniqueCandidate {
parts: unique.into_iter().map(KeyPart::Column).collect(),
predicate: None,
});
}
}
for index_name in db.list_indexes_for_table(table_name) {
let Some(meta) = db.get_index(&index_name) else { continue };
if !meta.unique {
continue;
}
let mut parts = Vec::with_capacity(meta.columns.len());
let mut representable = true;
for index_col in &meta.columns {
if let Some(name) = index_col.column_name() {
match schema.get_column_index(name) {
Some(idx) => parts.push(KeyPart::Column(idx)),
None => {
representable = false;
break;
}
}
} else if let Some(expr) = index_col.get_expression() {
match bare_column_index(schema, expr) {
Some(idx) => parts.push(KeyPart::Column(idx)),
None => parts.push(KeyPart::Expr(expr.clone())),
}
} else {
representable = false;
break;
}
}
if representable && !parts.is_empty() {
candidates
.push(UniqueCandidate { parts, predicate: meta.where_clause.as_deref().cloned() });
}
}
candidates
}
fn bare_column_index(schema: &vibesql_catalog::TableSchema, expr: &Expression) -> Option<usize> {
match expr {
Expression::ColumnRef(id) if id.table_canonical().is_none() => {
schema.get_column_index(id.column_canonical())
}
_ => None,
}
}
enum ResolvedTargetItem<'a> {
Column(usize),
Expr(&'a Expression),
}
fn resolve_target_items<'a>(
schema: &vibesql_catalog::TableSchema,
target: &'a [ConflictTargetItem],
) -> Result<Vec<ResolvedTargetItem<'a>>, ExecutorError> {
target
.iter()
.map(|item| match item {
ConflictTargetItem::Column(name) => {
schema.get_column_index(name).map(ResolvedTargetItem::Column).ok_or_else(|| {
ExecutorError::SqliteCompatError(format!("no such column: {}", name))
})
}
ConflictTargetItem::Expression(expr) => match bare_column_index(schema, expr) {
Some(idx) => Ok(ResolvedTargetItem::Column(idx)),
None => Ok(ResolvedTargetItem::Expr(expr)),
},
})
.collect()
}
fn candidate_matches_target(
candidate: &UniqueCandidate,
target: &[ResolvedTargetItem<'_>],
target_where: Option<&Expression>,
) -> bool {
match (target_where, candidate.predicate.as_ref()) {
(None, None) => {}
(Some(tw), Some(pred)) => {
if !expressions_equal(tw, pred) {
return false;
}
}
_ => return false,
}
if candidate.parts.len() != target.len() {
return false;
}
let mut used = vec![false; candidate.parts.len()];
for item in target {
let mut matched = false;
for (i, part) in candidate.parts.iter().enumerate() {
if used[i] {
continue;
}
let matches = match (item, part) {
(ResolvedTargetItem::Column(t), KeyPart::Column(c)) => t == c,
(ResolvedTargetItem::Expr(t), KeyPart::Expr(c)) => expressions_equal(t, c),
_ => false,
};
if matches {
used[i] = true;
matched = true;
break;
}
}
if !matched {
return false;
}
}
true
}
pub fn validate_conflict_target(
db: &vibesql_storage::Database,
table_name: &str,
schema: &vibesql_catalog::TableSchema,
target: &[ConflictTargetItem],
target_where: Option<&Expression>,
) -> Result<(), ExecutorError> {
let resolved = resolve_target_items(schema, target)?;
let matched = collect_unique_candidates(db, table_name, schema)
.iter()
.any(|candidate| candidate_matches_target(candidate, &resolved, target_where));
if matched {
Ok(())
} else {
Err(ExecutorError::SqliteCompatError(
"ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint".to_string(),
))
}
}
fn eval_candidate_key(
evaluator: &crate::evaluator::ExpressionEvaluator,
candidate: &UniqueCandidate,
row: &vibesql_storage::Row,
) -> Option<Vec<SqlValue>> {
let mut key = Vec::with_capacity(candidate.parts.len());
for part in &candidate.parts {
let value = match part {
KeyPart::Column(idx) => row.values.get(*idx)?.clone(),
KeyPart::Expr(expr) => evaluator.eval(expr, row).unwrap_or(SqlValue::Null),
};
if matches!(value, SqlValue::Null) {
return None;
}
key.push(value);
}
Some(key)
}
fn row_in_candidate(
evaluator: &crate::evaluator::ExpressionEvaluator,
candidate: &UniqueCandidate,
row: &vibesql_storage::Row,
) -> bool {
match &candidate.predicate {
None => true,
Some(pred) => evaluator.eval(pred, row).map(|v| is_predicate_truthy(&v)).unwrap_or(false),
}
}
fn find_conflicting_live_row(
db: &vibesql_storage::Database,
table_name: &str,
schema: &vibesql_catalog::TableSchema,
candidates: &[UniqueCandidate],
row_values: &[SqlValue],
) -> Result<Option<usize>, ExecutorError> {
let table = db
.get_table(table_name)
.ok_or_else(|| ExecutorError::TableNotFound(table_name.to_string()))?;
let evaluator = crate::evaluator::ExpressionEvaluator::new(schema);
let candidate_row = vibesql_storage::Row::new(row_values.to_vec());
for candidate in candidates {
let Some(new_key) = eval_candidate_key(&evaluator, candidate, &candidate_row) else {
continue;
};
if !row_in_candidate(&evaluator, candidate, &candidate_row) {
continue;
}
for (row_id, row) in table.scan_live() {
if !row_in_candidate(&evaluator, candidate, row) {
continue;
}
if eval_candidate_key(&evaluator, candidate, row).as_deref() == Some(&new_key[..]) {
return Ok(Some(row_id));
}
}
}
Ok(None)
}
pub fn row_conflicts_on_target(
db: &vibesql_storage::Database,
table_name: &str,
schema: &vibesql_catalog::TableSchema,
row_values: &[SqlValue],
target: &[ConflictTargetItem],
target_where: Option<&Expression>,
batch_rows: &[Vec<SqlValue>],
) -> Result<bool, ExecutorError> {
let resolved = resolve_target_items(schema, target)?;
let candidates: Vec<UniqueCandidate> = collect_unique_candidates(db, table_name, schema)
.into_iter()
.filter(|candidate| candidate_matches_target(candidate, &resolved, target_where))
.collect();
if find_conflicting_live_row(db, table_name, schema, &candidates, row_values)?.is_some() {
return Ok(true);
}
let evaluator = crate::evaluator::ExpressionEvaluator::new(schema);
let candidate_row = vibesql_storage::Row::new(row_values.to_vec());
for candidate in &candidates {
let Some(new_key) = eval_candidate_key(&evaluator, candidate, &candidate_row) else {
continue;
};
if !row_in_candidate(&evaluator, candidate, &candidate_row) {
continue;
}
for batch_row in batch_rows {
let row = vibesql_storage::Row::new(batch_row.clone());
if !row_in_candidate(&evaluator, candidate, &row) {
continue;
}
if eval_candidate_key(&evaluator, candidate, &row).as_deref() == Some(&new_key[..]) {
return Ok(true);
}
}
}
Ok(false)
}
#[allow(clippy::too_many_arguments)]
pub fn handle_on_conflict_update(
db: &mut vibesql_storage::Database,
table_name: &str,
schema: &vibesql_catalog::TableSchema,
row_values: &[SqlValue],
conflict_target: Option<&[ConflictTargetItem]>,
target_where: Option<&Expression>,
assignments: &[Assignment],
where_clause: Option<&Expression>,
cte_results: Option<&std::collections::HashMap<String, crate::select::cte::CteResult>>,
) -> Result<UpsertAction, ExecutorError> {
let all_candidates = collect_unique_candidates(db, table_name, schema);
let candidates: Vec<UniqueCandidate> = match conflict_target {
Some(target) => {
let resolved = resolve_target_items(schema, target)?;
all_candidates
.into_iter()
.filter(|candidate| candidate_matches_target(candidate, &resolved, target_where))
.collect()
}
None => all_candidates,
};
let conflicting_row_id =
find_conflicting_live_row(db, table_name, schema, &candidates, row_values)?;
let Some(row_id) = conflicting_row_id else {
return Ok(UpsertAction::NoConflict);
};
let existing_row = {
let table = db
.get_table(table_name)
.ok_or_else(|| ExecutorError::TableNotFound(table_name.to_string()))?;
table
.scan()
.get(row_id)
.cloned()
.ok_or_else(|| ExecutorError::UnsupportedExpression("Row not found".to_string()))?
};
let new_values = {
let mut evaluator = crate::evaluator::ExpressionEvaluator::with_database(schema, db);
if let Some(ctes) = cte_results {
evaluator = evaluator.with_cte_context(ctes);
}
if let Some(where_expr) = where_clause {
let substituted = substitute_excluded_refs(where_expr.clone(), schema, row_values)?;
let value = evaluator.eval(&substituted, &existing_row)?;
if !crate::evaluator::operators::is_truthy(&value) {
return Ok(UpsertAction::Skipped);
}
}
let mut new_values = existing_row.values.clone();
for assignment in assignments {
let col_idx = schema.get_column_index(&assignment.column).ok_or_else(|| {
ExecutorError::SqliteCompatError(format!("no such column: {}", assignment.column))
})?;
let substituted =
substitute_excluded_refs(assignment.value.clone(), schema, row_values)?;
new_values[col_idx] = evaluator.eval(&substituted, &existing_row)?;
}
new_values
};
let table_mut = db
.get_table_mut(table_name)
.ok_or_else(|| ExecutorError::TableNotFound(table_name.to_string()))?;
table_mut
.update_row(row_id, vibesql_storage::Row::new(new_values))
.map_err(|e| ExecutorError::UnsupportedExpression(format!("Storage error: {}", e)))?;
db.invalidate_columnar_cache(table_name);
Ok(UpsertAction::Updated(row_id))
}
struct UpsertColumnSubstituter<'a> {
schema: &'a vibesql_catalog::TableSchema,
inserted: &'a [SqlValue],
error: Option<ExecutorError>,
}
fn from_binds_excluded(from: &FromClause) -> bool {
match from {
FromClause::Table { name, alias, .. } => match alias {
Some(a) => a.eq_ignore_ascii_case("excluded"),
None => name.eq_ignore_ascii_case("excluded"),
},
FromClause::Join { left, right, alias, .. } => {
alias.as_ref().is_some_and(|a| a.eq_ignore_ascii_case("excluded"))
|| from_binds_excluded(left)
|| from_binds_excluded(right)
}
FromClause::Subquery { alias, .. } | FromClause::Values { alias, .. } => {
alias.eq_ignore_ascii_case("excluded")
}
}
}
fn select_binds_excluded(select: &SelectStmt) -> bool {
select.from.as_ref().is_some_and(from_binds_excluded)
}
impl ExpressionMutVisitor for UpsertColumnSubstituter<'_> {
fn pre_visit_expression(&mut self, expr: &Expression) -> VisitResult {
if self.error.is_some() {
return VisitResult::Skip;
}
let subquery = match expr {
Expression::ScalarSubquery(select) => Some(select.as_ref()),
Expression::Exists { subquery, .. } => Some(subquery.as_ref()),
Expression::In { subquery, .. } => Some(subquery.as_ref()),
Expression::QuantifiedComparison { subquery, .. } => Some(subquery.as_ref()),
_ => None,
};
match subquery {
Some(select) if select_binds_excluded(select) => VisitResult::Skip,
_ => VisitResult::Continue,
}
}
fn post_visit_expression(&mut self, expr: Expression) -> Expression {
if self.error.is_some() {
return expr;
}
if let Expression::ColumnRef(ref id) = expr {
let is_excluded = id
.table_canonical()
.map(|q| q.eq_ignore_ascii_case("excluded"))
.unwrap_or(false);
if !is_excluded {
return expr;
}
match self.schema.get_column_index(id.column_canonical()) {
Some(idx) => return Expression::Literal(self.inserted[idx].clone()),
None => {
self.error = Some(ExecutorError::SqliteCompatError(format!(
"no such column: excluded.{}",
id.column_canonical()
)));
return expr;
}
}
}
expr
}
}
fn substitute_excluded_refs(
expr: Expression,
schema: &vibesql_catalog::TableSchema,
inserted: &[SqlValue],
) -> Result<Expression, ExecutorError> {
let mut substituter = UpsertColumnSubstituter { schema, inserted, error: None };
let result = transform_expression(&mut substituter, expr);
if let Some(err) = substituter.error {
return Err(err);
}
Ok(result)
}