Skip to main content

icydb_model/node/
entity.rs

1//! Module: node::entity
2//!
3//! Responsibility: entity schema node metadata and relationship validation.
4//! Does not own: runtime data storage or query execution.
5//! Boundary: validates model declarations before catalog/runtime acceptance.
6
7#[cfg(test)]
8mod tests;
9
10use crate::prelude::*;
11use std::any::Any;
12
13///
14/// Entity
15///
16
17#[derive(Clone, Debug, Serialize)]
18pub struct Entity {
19    def: Def,
20    store: &'static str,
21    schema_version: u32,
22    primary_key: PrimaryKey,
23
24    #[serde(skip_serializing_if = "<[_]>::is_empty")]
25    indexes: &'static [Index],
26
27    #[serde(skip_serializing_if = "<[_]>::is_empty")]
28    relations: &'static [RelationEdge],
29
30    #[serde(skip_serializing_if = "<[_]>::is_empty")]
31    constraints: &'static [CheckConstraint],
32
33    fields: FieldList,
34    ty: Type,
35}
36
37impl Entity {
38    #[must_use]
39    #[expect(
40        clippy::too_many_arguments,
41        reason = "schema entity construction keeps store, key, index, relation, field, and type metadata explicit"
42    )]
43    pub const fn new(
44        def: Def,
45        store: &'static str,
46        schema_version: u32,
47        primary_key: PrimaryKey,
48        indexes: &'static [Index],
49        relations: &'static [RelationEdge],
50        constraints: &'static [CheckConstraint],
51        fields: FieldList,
52        ty: Type,
53    ) -> Self {
54        Self {
55            def,
56            store,
57            schema_version,
58            primary_key,
59            indexes,
60            relations,
61            constraints,
62            fields,
63            ty,
64        }
65    }
66
67    #[must_use]
68    pub const fn def(&self) -> &Def {
69        &self.def
70    }
71
72    #[must_use]
73    pub const fn store(&self) -> &'static str {
74        self.store
75    }
76
77    #[must_use]
78    pub const fn schema_version(&self) -> u32 {
79        self.schema_version
80    }
81
82    #[must_use]
83    pub const fn primary_key(&self) -> &PrimaryKey {
84        &self.primary_key
85    }
86
87    #[must_use]
88    pub const fn name(&self) -> &'static str {
89        self.def().ident()
90    }
91
92    #[must_use]
93    pub const fn indexes(&self) -> &'static [Index] {
94        self.indexes
95    }
96
97    #[must_use]
98    pub const fn relations(&self) -> &'static [RelationEdge] {
99        self.relations
100    }
101
102    /// Borrow accepted-check declarations owned by this entity.
103    #[must_use]
104    pub const fn constraints(&self) -> &'static [CheckConstraint] {
105        self.constraints
106    }
107
108    #[must_use]
109    pub const fn fields(&self) -> &FieldList {
110        &self.fields
111    }
112
113    #[must_use]
114    pub const fn ty(&self) -> &Type {
115        &self.ty
116    }
117
118    /// Return the scalar primary key field if this entity uses a scalar
119    /// primary-key contract.
120    #[must_use]
121    pub fn scalar_primary_key_field(&self) -> Option<&Field> {
122        self.fields().get(self.primary_key().scalar_field()?)
123    }
124
125    fn validate_relation_storage_policy(&self, errs: &mut ErrorTree) {
126        for field in self.fields().fields() {
127            if let Some(target) = field.value().item().relation() {
128                self.validate_relation_target_storage_policy(errs, field.name(), target);
129            }
130        }
131
132        for relation in self.relations() {
133            self.validate_relation_target_storage_policy(errs, relation.name(), relation.target());
134        }
135    }
136
137    fn validate_relation_target_storage_policy(
138        &self,
139        errs: &mut ErrorTree,
140        relation_name: &str,
141        target_path: &str,
142    ) {
143        let Some((source_capabilities, target_capabilities, target_store_path)) = ({
144            let schema = schema_read();
145            let Ok(source_store) = schema.cast_node::<Store>(self.store()) else {
146                return;
147            };
148            let Ok(target) = schema.cast_node::<Self>(target_path) else {
149                return;
150            };
151            let Ok(target_store) = schema.cast_node::<Store>(target.store()) else {
152                return;
153            };
154            let source_capabilities = source_store.storage_capabilities();
155            let target_capabilities = target_store.storage_capabilities();
156            let target_store_path = target.store().to_string();
157            drop(schema);
158
159            Some((source_capabilities, target_capabilities, target_store_path))
160        }) else {
161            return;
162        };
163
164        if matches!(
165            source_capabilities.relation_source(),
166            RelationSourceCapability::DurableSource
167        ) && matches!(
168            target_capabilities.relation_target(),
169            RelationTargetCapability::VolatileTarget
170        ) {
171            err!(
172                errs,
173                "relation '{}' from durable store '{}' to volatile target store '{}' is not supported; durable stores cannot own referential integrity against volatile heap targets",
174                relation_name,
175                self.store(),
176                target_store_path,
177            );
178        }
179    }
180}
181
182impl MacroNode for Entity {
183    fn as_any(&self) -> &dyn Any {
184        self
185    }
186}
187
188impl ValidateNode for Entity {
189    fn validate(&self) -> Result<(), ErrorTree> {
190        let mut errs = ErrorTree::new();
191
192        validate_source_name(
193            &mut errs,
194            "entity",
195            self.name(),
196            icydb_schema::EntitySourceKey::try_new,
197        );
198        if self.schema_version() == 0 {
199            err!(errs, "entity schema_version must be a positive integer");
200        }
201
202        {
203            let schema = schema_read();
204
205            // store
206            match schema.cast_node::<Store>(self.store()) {
207                Ok(_) => {}
208                Err(e) => errs.add(e),
209            }
210        }
211
212        for index in self.indexes() {
213            validate_source_name(
214                &mut errs,
215                "index",
216                index.name(),
217                icydb_schema::IndexSourceKey::try_new,
218            );
219        }
220        for relation in self.relations() {
221            validate_source_name(
222                &mut errs,
223                "relation",
224                relation.name(),
225                icydb_schema::RelationSourceKey::try_new,
226            );
227            if let Err(e) = relation.validate_for_source(self) {
228                errs.merge_for(relation.name(), e);
229            }
230        }
231        validate_entity_local_names(self, &mut errs);
232        self.validate_relation_storage_policy(&mut errs);
233
234        errs.result()
235    }
236}
237
238impl VisitableNode for Entity {
239    fn route_key(&self) -> String {
240        self.def().path()
241    }
242
243    fn drive<V: Visitor>(&self, v: &mut V) {
244        self.def().accept(v);
245        self.fields().accept(v);
246        for constraint in self.constraints() {
247            constraint.accept(v);
248        }
249        self.ty().accept(v);
250    }
251}
252
253fn validate_entity_local_names(entity: &Entity, errs: &mut ErrorTree) {
254    validate_unique_local_keys(
255        errs,
256        "field",
257        entity.fields().fields().iter().map(Field::name),
258    );
259    validate_unique_local_keys(errs, "index", entity.indexes().iter().map(Index::name));
260    validate_unique_local_keys(
261        errs,
262        "relation",
263        entity.relations().iter().map(RelationEdge::name),
264    );
265    validate_unique_local_keys(
266        errs,
267        "constraint",
268        entity.constraints().iter().map(CheckConstraint::name),
269    );
270}
271
272fn validate_unique_local_keys<'a>(
273    errs: &mut ErrorTree,
274    kind: &str,
275    names: impl IntoIterator<Item = &'a str>,
276) {
277    let mut seen = std::collections::BTreeSet::new();
278    for name in names {
279        if !seen.insert(name) {
280            err!(errs, "duplicate {kind} name '{name}' within entity",);
281        }
282    }
283}