use radixdb_core::{DataType, Error, Result, Row, Schema, Value};
use radixdb_sql::ast::{Expression, Identifier};
use radixdb_storage::expression::{ComparisonExpr, Expression as StorageExpr};
use radixdb_storage::traits::Table;
pub(super) fn conflict_matches_target(
conflict_target: &[Identifier],
schema: &Schema,
error: &Error,
) -> bool {
if conflict_target.is_empty() {
return true;
}
let target_set: rustc_hash::FxHashSet<&str> = conflict_target
.iter()
.map(|id| id.value_lower.as_str())
.collect();
match error {
Error::PrimaryKeyConstraint { .. } => {
let pk_cols: rustc_hash::FxHashSet<&str> = schema
.columns
.iter()
.filter(|c| c.primary_key)
.map(|c| c.name_lower.as_str())
.collect();
target_set == pk_cols
}
Error::UniqueConstraint { column, .. } => {
let violated_cols: rustc_hash::FxHashSet<&str> =
column.split(", ").map(|s| s.trim()).collect();
target_set == violated_cols
}
_ => false,
}
}
pub(super) fn validate_conflict_target(
conflict_target: &[Identifier],
schema: &Schema,
table: &dyn Table,
) -> Result<()> {
if conflict_target.is_empty() {
return Ok(());
}
let mut target = rustc_hash::FxHashSet::default();
for column in conflict_target {
if schema
.get_column_index(column.value_lower.as_str())
.is_none()
{
return Err(Error::ColumnNotFound(column.value.to_string()));
}
if !target.insert(column.value_lower.as_str()) {
return Err(Error::InvalidArgument(format!(
"ON CONFLICT target column '{}' is duplicated",
column.value
)));
}
}
let primary_key: rustc_hash::FxHashSet<&str> = schema
.columns
.iter()
.filter(|column| column.primary_key)
.map(|column| column.name_lower.as_str())
.collect();
if !primary_key.is_empty() && target == primary_key {
return Ok(());
}
let matches_unique = table.get_unique_non_pk_indexes().iter().any(|index| {
index.partial_predicate().is_none()
&& index.column_names().len() == target.len()
&& index
.column_names()
.iter()
.all(|column| target.contains(column.to_lowercase().as_str()))
});
if matches_unique {
Ok(())
} else {
Err(Error::InvalidArgument(
"ON CONFLICT target does not name a complete PRIMARY KEY or UNIQUE constraint"
.to_string(),
))
}
}
pub(super) fn validate_coercion(
original: &Value,
coerced: &Value,
column_name: &str,
target_type: DataType,
vector_dimensions: u16,
) -> Result<()> {
if !original.is_null() && coerced.is_null() {
return Err(Error::Type(format!(
"cannot convert value '{}' to {:?} for column '{}'",
original, target_type, column_name
)));
}
if target_type == DataType::Vector {
if let Value::Extension(data) = coerced {
if data.first() == Some(&(DataType::Vector as u8)) {
let got_dim = u16::try_from((data.len() - 1) / 4).unwrap_or(u16::MAX);
if vector_dimensions > 0 && got_dim != vector_dimensions {
return Err(Error::VectorDimensionMismatch {
expected: vector_dimensions,
got: got_dim,
});
}
}
}
}
Ok(())
}
#[inline]
pub(super) fn try_extract_literal(expr: &Expression) -> Option<Value> {
match expr {
Expression::IntegerLiteral(lit) => Some(Value::Integer(lit.value)),
Expression::FloatLiteral(lit) => Some(Value::Float(lit.value)),
Expression::StringLiteral(lit) => Some(Value::text(lit.value.as_str())),
Expression::BooleanLiteral(lit) => Some(Value::Boolean(lit.value)),
Expression::NullLiteral(_) => Some(Value::null_unknown()),
Expression::Prefix(prefix) if prefix.operator == "-" => match prefix.right.as_ref() {
Expression::IntegerLiteral(lit) => Some(Value::Integer(-lit.value)),
Expression::FloatLiteral(lit) => Some(Value::Float(-lit.value)),
_ => None,
},
_ => None, }
}
#[doc(hidden)]
pub struct CompiledUpsert {
pub(crate) compiled_updates: Vec<(usize, DataType, u16, crate::expression::SharedProgram)>,
pub(crate) compiled_table_checks: Vec<(String, crate::expression::SharedProgram)>,
}
#[inline]
pub(super) fn auto_increment_pk_index(schema: &Schema) -> Option<usize> {
schema
.pk_column_index()
.filter(|&idx| schema.columns[idx].auto_increment)
}
#[inline]
pub(super) fn capture_last_insert_id(
row: &Row,
auto_increment_pk_idx: Option<usize>,
dest: &mut i64,
) {
if let Some(idx) = auto_increment_pk_idx {
if let Some(id) = row.get(idx).and_then(Value::as_int64) {
*dest = id;
}
}
}
#[inline]
pub(super) fn insert_row_for_command_result(
table: &mut dyn Table,
row: Row,
has_returning: bool,
auto_increment_pk_idx: Option<usize>,
last_insert_id: &mut i64,
) -> Result<Option<Row>> {
if has_returning || auto_increment_pk_idx.is_some() {
let inserted_row = table.insert(row)?;
capture_last_insert_id(&inserted_row, auto_increment_pk_idx, last_insert_id);
Ok(has_returning.then_some(inserted_row))
} else {
table.insert_discard(row)?;
Ok(None)
}
}
pub(crate) fn evaluate_default_expr(default_expr: &str, target_type: DataType) -> Result<Value> {
let sql = format!("SELECT {default_expr}");
let statements = radixdb_sql::parse_sql(&sql)
.map_err(|error| Error::Parse(format!("invalid default expression: {error}")))?;
if statements.is_empty() {
return Err(Error::InvalidArgument(format!(
"default expression '{default_expr}' produced no statement"
)));
}
if let radixdb_sql::ast::Statement::Select(select) = &statements[0] {
if let Some(expression) = select.columns.first() {
let value = crate::expression::ExpressionEval::compile(expression, &[])?
.eval_slice(&Row::new())?;
return value.try_coerce_to_type(target_type);
}
}
Err(Error::InvalidArgument(format!(
"default expression '{default_expr}' is not a SELECT expression"
)))
}
pub(super) fn find_row_by_unique_index(
table: &dyn Table,
schema: &radixdb_core::Schema,
index_name: &str,
column_name: &str,
row_values: &[Value],
) -> Result<Option<i64>> {
if let Some(index) = table.get_index(index_name) {
let col_ids = index.column_ids();
let mut lookup_values: Vec<Value> = Vec::with_capacity(col_ids.len());
for &col_id in col_ids {
let value = row_values
.get(col_id as usize)
.cloned()
.unwrap_or(Value::null_unknown());
lookup_values.push(value);
}
let row_ids = index.get_row_ids_equal(&lookup_values)?;
if let Some(&row_id) = row_ids.first() {
return Ok(Some(row_id));
}
}
if let Some(row_id) = table.find_unique_conflict_row_id(index_name, column_name, row_values)? {
return Ok(Some(row_id));
}
let col_names: Vec<&str> = column_name.split(", ").collect();
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();
let col_idx = match schema.column_index_map().get(&col_lower) {
Some(&idx) => idx,
None => return Ok(None),
};
let value = row_values
.get(col_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));
}
let scan_expr: Box<dyn StorageExpr> = if comparisons.len() == 1 {
comparisons.pop().unwrap()
} else {
use radixdb_storage::expression::AndExpr;
let mut and_expr = AndExpr::new(comparisons);
and_expr.prepare_for_schema(schema);
Box::new(and_expr)
};
let pk_idx = schema.pk_column_index();
let column_indices: Vec<usize> = if let Some(pk) = pk_idx {
vec![pk]
} else {
vec![0]
};
let mut scanner = table.scan(&column_indices, Some(&*scan_expr))?;
let result = if scanner.next() {
let row_id = scanner.current_row_id()?;
if row_id >= 0 {
Some(row_id)
} else if pk_idx.is_some() {
let row = scanner.row();
if let Some(Value::Integer(id)) = row.get(0) {
Some(*id)
} else {
Some(row_id)
}
} else {
Some(row_id)
}
} else {
None
};
scanner.close()?;
Ok(result)
}