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