#[cfg(test)]
mod tests;
use crate::prelude::*;
use std::any::Any;
#[derive(Clone, Debug, Serialize)]
pub struct Entity {
def: Def,
store: &'static str,
schema_version: u32,
primary_key: PrimaryKey,
#[serde(skip_serializing_if = "<[_]>::is_empty")]
indexes: &'static [Index],
#[serde(skip_serializing_if = "<[_]>::is_empty")]
relations: &'static [RelationEdge],
#[serde(skip_serializing_if = "<[_]>::is_empty")]
constraints: &'static [CheckConstraint],
fields: FieldList,
ty: Type,
}
impl Entity {
#[must_use]
#[expect(
clippy::too_many_arguments,
reason = "schema entity construction keeps store, key, index, relation, field, and type metadata explicit"
)]
pub const fn new(
def: Def,
store: &'static str,
schema_version: u32,
primary_key: PrimaryKey,
indexes: &'static [Index],
relations: &'static [RelationEdge],
constraints: &'static [CheckConstraint],
fields: FieldList,
ty: Type,
) -> Self {
Self {
def,
store,
schema_version,
primary_key,
indexes,
relations,
constraints,
fields,
ty,
}
}
#[must_use]
pub const fn def(&self) -> &Def {
&self.def
}
#[must_use]
pub const fn store(&self) -> &'static str {
self.store
}
#[must_use]
pub const fn schema_version(&self) -> u32 {
self.schema_version
}
#[must_use]
pub const fn primary_key(&self) -> &PrimaryKey {
&self.primary_key
}
#[must_use]
pub const fn name(&self) -> &'static str {
self.def().ident()
}
#[must_use]
pub const fn indexes(&self) -> &'static [Index] {
self.indexes
}
#[must_use]
pub const fn relations(&self) -> &'static [RelationEdge] {
self.relations
}
#[must_use]
pub const fn constraints(&self) -> &'static [CheckConstraint] {
self.constraints
}
#[must_use]
pub const fn fields(&self) -> &FieldList {
&self.fields
}
#[must_use]
pub const fn ty(&self) -> &Type {
&self.ty
}
#[must_use]
pub fn scalar_primary_key_field(&self) -> Option<&Field> {
self.fields().get(self.primary_key().scalar_field()?)
}
fn validate_relation_storage_policy(&self, errs: &mut ErrorTree) {
for field in self.fields().fields() {
if let Some(target) = field.value().item().relation() {
self.validate_relation_target_storage_policy(errs, field.name(), target);
}
}
for relation in self.relations() {
self.validate_relation_target_storage_policy(errs, relation.name(), relation.target());
}
}
fn validate_relation_target_storage_policy(
&self,
errs: &mut ErrorTree,
relation_name: &str,
target_path: &str,
) {
let Some((source_capabilities, target_capabilities, target_store_path)) = ({
let schema = schema_read();
let Ok(source_store) = schema.cast_node::<Store>(self.store()) else {
return;
};
let Ok(target) = schema.cast_node::<Self>(target_path) else {
return;
};
let Ok(target_store) = schema.cast_node::<Store>(target.store()) else {
return;
};
let source_capabilities = source_store.storage_capabilities();
let target_capabilities = target_store.storage_capabilities();
let target_store_path = target.store().to_string();
drop(schema);
Some((source_capabilities, target_capabilities, target_store_path))
}) else {
return;
};
if matches!(
source_capabilities.relation_source(),
RelationSourceCapability::DurableSource
) && matches!(
target_capabilities.relation_target(),
RelationTargetCapability::VolatileTarget
) {
err!(
errs,
"relation '{}' from durable store '{}' to volatile target store '{}' is not supported; durable stores cannot own referential integrity against volatile heap targets",
relation_name,
self.store(),
target_store_path,
);
}
}
}
impl MacroNode for Entity {
fn as_any(&self) -> &dyn Any {
self
}
}
impl ValidateNode for Entity {
fn validate(&self) -> Result<(), ErrorTree> {
let mut errs = ErrorTree::new();
validate_source_name(
&mut errs,
"entity",
self.name(),
icydb_schema::EntitySourceKey::try_new,
);
if self.schema_version() == 0 {
err!(errs, "entity schema_version must be a positive integer");
}
{
let schema = schema_read();
match schema.cast_node::<Store>(self.store()) {
Ok(_) => {}
Err(e) => errs.add(e),
}
}
for index in self.indexes() {
validate_source_name(
&mut errs,
"index",
index.name(),
icydb_schema::IndexSourceKey::try_new,
);
}
for relation in self.relations() {
validate_source_name(
&mut errs,
"relation",
relation.name(),
icydb_schema::RelationSourceKey::try_new,
);
if let Err(e) = relation.validate_for_source(self) {
errs.merge_for(relation.name(), e);
}
}
validate_entity_local_names(self, &mut errs);
self.validate_relation_storage_policy(&mut errs);
errs.result()
}
}
impl VisitableNode for Entity {
fn route_key(&self) -> String {
self.def().path()
}
fn drive<V: Visitor>(&self, v: &mut V) {
self.def().accept(v);
self.fields().accept(v);
for constraint in self.constraints() {
constraint.accept(v);
}
self.ty().accept(v);
}
}
fn validate_entity_local_names(entity: &Entity, errs: &mut ErrorTree) {
validate_unique_local_keys(
errs,
"field",
entity.fields().fields().iter().map(Field::name),
);
validate_unique_local_keys(errs, "index", entity.indexes().iter().map(Index::name));
validate_unique_local_keys(
errs,
"relation",
entity.relations().iter().map(RelationEdge::name),
);
validate_unique_local_keys(
errs,
"constraint",
entity.constraints().iter().map(CheckConstraint::name),
);
}
fn validate_unique_local_keys<'a>(
errs: &mut ErrorTree,
kind: &str,
names: impl IntoIterator<Item = &'a str>,
) {
let mut seen = std::collections::BTreeSet::new();
for name in names {
if !seen.insert(name) {
err!(errs, "duplicate {kind} name '{name}' within entity",);
}
}
}