use crate::{
db::{
PersistedRow,
data::{CanonicalSlotReader, DataKey, RawRow, SlotReader, StructuralSlotReader},
executor::mutation::save::SaveExecutor,
predicate::canonical_cmp,
relation::{model_has_strong_relation_targets, validate_save_strong_relations},
schema::{SchemaInfo, literal_matches_type},
},
error::{ErrorClass, ErrorOrigin, InternalError},
model::{entity::resolve_primary_key_slot, field::FieldKind},
sanitize::sanitize,
traits::{EntityKind, EntityValue},
validate::validate,
value::Value,
};
use std::{
cmp::Ordering,
collections::BTreeMap,
sync::{Mutex, OnceLock},
};
impl<E: PersistedRow + EntityValue> SaveExecutor<E> {
pub(super) fn preflight_entity(&self, entity: &mut E) -> Result<(), InternalError> {
let schema = Self::schema_info()?;
let validate_relations = model_has_strong_relation_targets(E::MODEL);
self.preflight_entity_with_cached_schema(entity, schema, validate_relations)
}
pub(in crate::db::executor::mutation) fn ensure_persisted_row_invariants(
data_key: &DataKey,
row: &RawRow,
) -> Result<(), InternalError> {
let schema = Self::schema_info()?;
let row_fields = StructuralSlotReader::from_raw_row(row, E::MODEL)?;
row_fields.validate_storage_key(data_key)?;
Self::validate_structural_row_invariants(&row_fields, schema)
}
pub(in crate::db::executor::mutation) fn schema_info()
-> Result<&'static SchemaInfo, InternalError> {
type SchemaCache = BTreeMap<&'static str, Result<&'static SchemaInfo, CachedInvariant>>;
static CACHE: OnceLock<Mutex<SchemaCache>> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(BTreeMap::new()));
let mut cache_guard = cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let entry = cache_guard.entry(E::PATH).or_insert_with(|| {
SchemaInfo::from_entity_model(E::MODEL)
.map(|schema| Box::leak(Box::new(schema)) as &'static SchemaInfo)
.map_err(|err| {
CachedInvariant::from_error(InternalError::mutation_entity_schema_invalid(
E::PATH,
err,
))
})
});
match entry {
Ok(schema) => Ok(*schema),
Err(err) => Err(err.to_error()),
}
}
pub(in crate::db::executor::mutation) fn preflight_entity_with_cached_schema(
&self,
entity: &mut E,
schema: &SchemaInfo,
validate_relations: bool,
) -> Result<(), InternalError> {
sanitize(entity)?;
validate(entity)?;
Self::validate_entity_invariants(entity, schema)?;
if validate_relations {
validate_save_strong_relations::<E>(&self.db, entity)?;
}
Ok(())
}
fn validate_entity_invariants(entity: &E, schema: &SchemaInfo) -> Result<(), InternalError> {
let primary_key_name = E::MODEL.primary_key().name();
let Some(pk_field_index) = resolve_primary_key_slot(E::MODEL) else {
return Err(InternalError::mutation_entity_primary_key_missing(
E::PATH,
primary_key_name,
));
};
let pk_value = entity.get_value_by_index(pk_field_index).ok_or_else(|| {
InternalError::mutation_entity_primary_key_missing(E::PATH, primary_key_name)
})?;
if matches!(pk_value, Value::Null) {
return Err(InternalError::mutation_entity_primary_key_invalid_value(
E::PATH,
primary_key_name,
&pk_value,
));
}
if let Some(pk_type) = schema.field(primary_key_name)
&& !literal_matches_type(&pk_value, pk_type)
{
return Err(InternalError::mutation_entity_primary_key_type_mismatch(
E::PATH,
primary_key_name,
&pk_value,
));
}
let identity_pk = crate::traits::FieldValue::to_value(&entity.id().key());
if pk_value != identity_pk {
return Err(InternalError::mutation_entity_primary_key_mismatch(
E::PATH,
primary_key_name,
&pk_value,
&identity_pk,
));
}
for (field_index, field) in E::MODEL.fields.iter().enumerate() {
let value = entity.get_value_by_index(field_index).ok_or_else(|| {
InternalError::mutation_entity_field_missing(
E::PATH,
field.name,
field_is_indexed::<E>(field.name),
)
})?;
if matches!(value, Value::Null | Value::Unit) {
continue;
}
if !field.kind.value_kind().is_queryable() {
continue;
}
let Some(field_type) = schema.field(field.name) else {
continue;
};
if !literal_matches_type(&value, field_type) {
return Err(InternalError::mutation_entity_field_type_mismatch(
E::PATH,
field.name,
&value,
));
}
Self::validate_decimal_scale(field.name, &field.kind, &value)?;
Self::validate_deterministic_field_value(field.name, &field.kind, &value)?;
}
Ok(())
}
fn validate_structural_row_invariants(
row_fields: &StructuralSlotReader<'_>,
schema: &SchemaInfo,
) -> Result<(), InternalError> {
for (field_index, field) in E::MODEL.fields.iter().enumerate() {
if !row_fields.has(field_index) {
return Err(InternalError::mutation_entity_field_missing(
E::PATH,
field.name,
field_is_indexed::<E>(field.name),
));
}
let value = row_fields.required_value_by_contract_cow(field_index)?;
if matches!(value.as_ref(), Value::Null | Value::Unit) {
continue;
}
if !field.kind.value_kind().is_queryable() {
continue;
}
let Some(field_type) = schema.field(field.name) else {
continue;
};
if !literal_matches_type(value.as_ref(), field_type) {
return Err(InternalError::mutation_entity_field_type_mismatch(
E::PATH,
field.name,
value.as_ref(),
));
}
Self::validate_decimal_scale(field.name, &field.kind, value.as_ref())?;
Self::validate_deterministic_field_value(field.name, &field.kind, value.as_ref())?;
}
Ok(())
}
fn validate_decimal_scale(
field_name: &str,
kind: &FieldKind,
value: &Value,
) -> Result<(), InternalError> {
if matches!(value, Value::Null | Value::Unit) {
return Ok(());
}
match (kind, value) {
(FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
if decimal.scale() != *scale {
return Err(InternalError::mutation_decimal_scale_mismatch(
E::PATH,
field_name,
scale,
decimal.scale(),
));
}
Ok(())
}
(FieldKind::Relation { key_kind, .. }, value) => {
Self::validate_decimal_scale(field_name, key_kind, value)
}
(FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
for item in items {
Self::validate_decimal_scale(field_name, inner, item)?;
}
Ok(())
}
(
FieldKind::Map {
key,
value: map_value,
},
Value::Map(entries),
) => {
for (entry_key, entry_value) in entries {
Self::validate_decimal_scale(field_name, key, entry_key)?;
Self::validate_decimal_scale(field_name, map_value, entry_value)?;
}
Ok(())
}
_ => Ok(()),
}
}
pub(in crate::db::executor) fn validate_deterministic_field_value(
field_name: &str,
kind: &FieldKind,
value: &Value,
) -> Result<(), InternalError> {
match kind {
FieldKind::Set(_) => Self::validate_set_encoding(field_name, value),
FieldKind::Map { .. } => Self::validate_map_encoding(field_name, value),
_ => Ok(()),
}
}
fn validate_set_encoding(field_name: &str, value: &Value) -> Result<(), InternalError> {
if matches!(value, Value::Null) {
return Ok(());
}
let Value::List(items) = value else {
return Err(InternalError::mutation_set_field_list_required(
E::PATH,
field_name,
));
};
for pair in items.windows(2) {
let [left, right] = pair else {
continue;
};
let ordering = canonical_cmp(left, right);
if ordering != Ordering::Less {
return Err(InternalError::mutation_set_field_not_canonical(
E::PATH,
field_name,
));
}
}
Ok(())
}
fn validate_map_encoding(field_name: &str, value: &Value) -> Result<(), InternalError> {
if matches!(value, Value::Null) {
return Ok(());
}
let Value::Map(entries) = value else {
return Err(InternalError::mutation_map_field_map_required(
E::PATH,
field_name,
));
};
Value::validate_map_entries(entries.as_slice()).map_err(|err| {
InternalError::mutation_map_field_entries_invalid(E::PATH, field_name, err)
})?;
if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
return Err(InternalError::mutation_map_field_entries_not_canonical(
E::PATH,
field_name,
));
}
Ok(())
}
}
struct CachedInvariant {
class: ErrorClass,
origin: ErrorOrigin,
message: String,
}
impl CachedInvariant {
fn from_error(err: InternalError) -> Self {
Self {
class: err.class,
origin: err.origin,
message: err.message,
}
}
fn to_error(&self) -> InternalError {
InternalError::classified(self.class, self.origin, self.message.clone())
}
}
fn field_is_indexed<E: EntityKind>(field_name: &str) -> bool {
E::MODEL
.indexes()
.iter()
.any(|index| index.fields().contains(&field_name))
}