use radixdb_core::{DataType, Error, Result, Row, Schema, Value};
pub trait PreparedRowValidator: Send {
fn validate(&mut self, row: &Row) -> Result<()>;
}
pub type RowValidatorBinder = fn(&Schema) -> Result<Box<dyn PreparedRowValidator>>;
struct SchemaOnlyRowValidator {
schema: Schema,
}
impl PreparedRowValidator for SchemaOnlyRowValidator {
fn validate(&mut self, row: &Row) -> Result<()> {
validate_row_shape(&self.schema, row)
}
}
#[doc(hidden)]
pub fn bind_schema_only_row_validator(schema: &Schema) -> Result<Box<dyn PreparedRowValidator>> {
if schema
.columns
.iter()
.any(|column| column.check_expr.is_some())
|| !schema.table_checks.is_empty()
{
return Err(Error::NotSupported(format!(
"table '{}' requires a configured CHECK-expression validator",
schema.table_name
)));
}
Ok(Box::new(SchemaOnlyRowValidator {
schema: schema.clone(),
}))
}
pub fn validate_row_shape(schema: &Schema, row: &Row) -> Result<()> {
if row.len() != schema.columns.len() {
return Err(Error::InvalidArgument(format!(
"row for table '{}' has {} values but schema requires {}",
schema.table_name,
row.len(),
schema.columns.len()
)));
}
for (index, column) in schema.columns.iter().enumerate() {
let value = row.get(index).ok_or_else(|| {
Error::InvalidArgument(format!("missing value for column '{}'", column.name))
})?;
if value.is_null() {
if !column.nullable {
return Err(Error::not_null_constraint(column.name.clone()));
}
continue;
}
if value.data_type() != column.data_type {
return Err(Error::Type(format!(
"value for column '{}' has type {:?}, expected {:?}",
column.name,
value.data_type(),
column.data_type
)));
}
if column.data_type == DataType::Vector && column.vector_dimensions > 0 {
let got = match value {
Value::Extension(data) if data.first() == Some(&(DataType::Vector as u8)) => {
u16::try_from((data.len() - 1) / 4).unwrap_or(u16::MAX)
}
_ => u16::MAX,
};
if got != column.vector_dimensions {
return Err(Error::VectorDimensionMismatch {
expected: column.vector_dimensions,
got,
});
}
}
column.validate_declared_value(value)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use radixdb_core::SchemaBuilder;
#[test]
fn schema_only_binder_is_fail_closed_for_check_expressions() {
let mut schema = SchemaBuilder::new("items")
.add_primary_key("id", DataType::Integer)
.build();
schema.table_checks.push("id > 0".to_string());
let error = bind_schema_only_row_validator(&schema)
.err()
.expect("CHECK expressions require an upper binder");
assert!(error.to_string().contains("CHECK-expression validator"));
}
#[test]
fn schema_only_validator_enforces_intrinsic_shape() {
let schema = SchemaBuilder::new("items")
.add_primary_key("id", DataType::Integer)
.add("name", DataType::Text)
.build();
let mut validator = bind_schema_only_row_validator(&schema).unwrap();
validator
.validate(&Row::from_values(vec![
Value::Integer(1),
Value::text("one"),
]))
.unwrap();
assert!(validator
.validate(&Row::from_values(vec![Value::Integer(1)]))
.is_err());
}
}