Skip to main content

icydb_schema/node/
entity.rs

1use crate::prelude::*;
2use std::any::Any;
3
4///
5/// Entity
6///
7
8#[derive(Clone, Debug, Serialize)]
9pub struct Entity {
10    def: Def,
11    store: &'static str,
12    primary_key: PrimaryKey,
13
14    #[serde(skip_serializing_if = "Option::is_none")]
15    name: Option<&'static str>,
16
17    #[serde(skip_serializing_if = "<[_]>::is_empty")]
18    indexes: &'static [Index],
19
20    #[serde(skip_serializing_if = "<[_]>::is_empty")]
21    relations: &'static [RelationEdge],
22
23    fields: FieldList,
24    ty: Type,
25}
26
27impl Entity {
28    #[must_use]
29    #[expect(
30        clippy::too_many_arguments,
31        reason = "schema entity construction keeps store, key, index, relation, field, and type metadata explicit"
32    )]
33    pub const fn new(
34        def: Def,
35        store: &'static str,
36        primary_key: PrimaryKey,
37        name: Option<&'static str>,
38        indexes: &'static [Index],
39        relations: &'static [RelationEdge],
40        fields: FieldList,
41        ty: Type,
42    ) -> Self {
43        Self {
44            def,
45            store,
46            primary_key,
47            name,
48            indexes,
49            relations,
50            fields,
51            ty,
52        }
53    }
54
55    #[must_use]
56    pub const fn def(&self) -> &Def {
57        &self.def
58    }
59
60    #[must_use]
61    pub const fn store(&self) -> &'static str {
62        self.store
63    }
64
65    #[must_use]
66    pub const fn primary_key(&self) -> &PrimaryKey {
67        &self.primary_key
68    }
69
70    #[must_use]
71    pub const fn name(&self) -> Option<&'static str> {
72        self.name
73    }
74
75    #[must_use]
76    pub const fn indexes(&self) -> &'static [Index] {
77        self.indexes
78    }
79
80    #[must_use]
81    pub const fn relations(&self) -> &'static [RelationEdge] {
82        self.relations
83    }
84
85    #[must_use]
86    pub const fn fields(&self) -> &FieldList {
87        &self.fields
88    }
89
90    #[must_use]
91    pub const fn ty(&self) -> &Type {
92        &self.ty
93    }
94
95    /// Return the scalar primary key field if this entity uses a scalar
96    /// primary-key contract.
97    #[must_use]
98    pub fn scalar_primary_key_field(&self) -> Option<&Field> {
99        self.fields().get(self.primary_key().scalar_field()?)
100    }
101
102    /// Resolve the entity name used for schema identity.
103    #[must_use]
104    pub fn resolved_name(&self) -> &'static str {
105        self.name().unwrap_or_else(|| self.def().ident())
106    }
107
108    fn validate_relation_storage_policy(&self, errs: &mut ErrorTree) {
109        for field in self.fields().fields() {
110            if let Some(target) = field.value().item().relation() {
111                self.validate_relation_target_storage_policy(errs, field.ident(), target);
112            }
113        }
114
115        for relation in self.relations() {
116            self.validate_relation_target_storage_policy(errs, relation.ident(), relation.target());
117        }
118    }
119
120    fn validate_relation_target_storage_policy(
121        &self,
122        errs: &mut ErrorTree,
123        relation_name: &str,
124        target_path: &str,
125    ) {
126        let schema = schema_read();
127        let Ok(source_store) = schema.cast_node::<Store>(self.store()) else {
128            return;
129        };
130        let Ok(target) = schema.cast_node::<Self>(target_path) else {
131            return;
132        };
133        let Ok(target_store) = schema.cast_node::<Store>(target.store()) else {
134            return;
135        };
136
137        if source_store.is_stable_storage() && target_store.is_heap_storage() {
138            err!(
139                errs,
140                "relation '{}' from stable store '{}' to heap target store '{}' is not supported in 0.169; stable stores cannot own referential integrity against volatile heap targets",
141                relation_name,
142                self.store(),
143                target.store(),
144            );
145        }
146    }
147}
148
149impl MacroNode for Entity {
150    fn as_any(&self) -> &dyn Any {
151        self
152    }
153}
154
155impl ValidateNode for Entity {
156    fn validate(&self) -> Result<(), ErrorTree> {
157        let mut errs = ErrorTree::new();
158        let schema = schema_read();
159
160        // store
161        match schema.cast_node::<Store>(self.store()) {
162            Ok(_) => {}
163            Err(e) => errs.add(e),
164        }
165
166        for relation in self.relations() {
167            if let Err(e) = relation.validate_for_source(self) {
168                errs.merge_for(relation.ident(), e);
169            }
170        }
171        self.validate_relation_storage_policy(&mut errs);
172
173        errs.result()
174    }
175}
176
177impl VisitableNode for Entity {
178    fn route_key(&self) -> String {
179        self.def().path()
180    }
181
182    fn drive<V: Visitor>(&self, v: &mut V) {
183        self.def().accept(v);
184        self.fields().accept(v);
185        self.ty().accept(v);
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use crate::build::schema_write;
193
194    fn primitive_item(primitive: Primitive) -> Item {
195        Item::new(
196            ItemTarget::Primitive(primitive),
197            None,
198            None,
199            None,
200            None,
201            &[],
202            &[],
203            false,
204        )
205    }
206
207    fn relation_item(primitive: Primitive, target: &'static str) -> Item {
208        Item::new(
209            ItemTarget::Primitive(primitive),
210            Some(target),
211            None,
212            None,
213            None,
214            &[],
215            &[],
216            false,
217        )
218    }
219
220    fn field(ident: &'static str, primitive: Primitive) -> Field {
221        Field::new(
222            ident,
223            Value::new(Cardinality::One, primitive_item(primitive)),
224            None,
225            None,
226            None,
227        )
228    }
229
230    fn relation_field(ident: &'static str, primitive: Primitive, target: &'static str) -> Field {
231        Field::new(
232            ident,
233            Value::new(Cardinality::One, relation_item(primitive, target)),
234            None,
235            None,
236            None,
237        )
238    }
239
240    fn store(path: &'static str) -> Store {
241        Store::new_stable(
242            Def::new("schema_entity_relation_edge", "Store"),
243            "STORE",
244            "schema_entity_relation_edge_store",
245            path,
246            StoreStableMemoryConfig::new(110, 111, 112),
247        )
248    }
249
250    fn stable_store_in_module(module: &'static str, ident: &'static str) -> Store {
251        Store::new_stable(
252            Def::new(module, ident),
253            "STORE",
254            "schema_entity_relation_edge_store",
255            "schema_entity_relation_edge_store",
256            StoreStableMemoryConfig::new(120, 121, 122),
257        )
258    }
259
260    fn heap_store_in_module(module: &'static str, ident: &'static str) -> Store {
261        Store::new_heap(
262            Def::new(module, ident),
263            "HEAP_STORE",
264            "schema_entity_relation_edge_heap_store",
265            "schema_entity_relation_edge_heap_store",
266            StoreHeapConfig::new(),
267        )
268    }
269
270    fn entity(
271        ident: &'static str,
272        store_path: &'static str,
273        pk_fields: &'static [&'static str],
274        relations: &'static [RelationEdge],
275        fields: &'static [Field],
276    ) -> Entity {
277        entity_in_module(
278            "schema_entity_relation_edge",
279            ident,
280            pk_fields,
281            store_path,
282            relations,
283            fields,
284        )
285    }
286
287    fn entity_in_module(
288        module: &'static str,
289        ident: &'static str,
290        pk_fields: &'static [&'static str],
291        store_path: &'static str,
292        relations: &'static [RelationEdge],
293        fields: &'static [Field],
294    ) -> Entity {
295        Entity::new(
296            Def::new(module, ident),
297            store_path,
298            PrimaryKey::new(pk_fields, PrimaryKeySource::External),
299            None,
300            &[],
301            relations,
302            FieldList::new(fields),
303            Type::new(&[], &[]),
304        )
305    }
306
307    #[test]
308    fn entity_validation_checks_owned_relation_edges() {
309        let store_path = "schema_entity_relation_edge::Store";
310        schema_write().insert_node(SchemaNode::Store(store(store_path)));
311        let target_fields = Box::leak(
312            vec![
313                field("tenant_id", Primitive::Nat64),
314                field("id", Primitive::Ulid),
315            ]
316            .into_boxed_slice(),
317        );
318        schema_write().insert_node(SchemaNode::Entity(entity(
319            "User",
320            store_path,
321            &["tenant_id", "id"],
322            &[],
323            target_fields,
324        )));
325
326        let source_fields = Box::leak(
327            vec![
328                field("author_tenant_id", Primitive::Nat64),
329                field("author_id", Primitive::Ulid),
330            ]
331            .into_boxed_slice(),
332        );
333        let source_relations = Box::leak(
334            vec![RelationEdge::new(
335                "author",
336                "schema_entity_relation_edge::User",
337                &["author_tenant_id", "author_id"],
338            )]
339            .into_boxed_slice(),
340        );
341        let source = entity(
342            "Post",
343            store_path,
344            &["author_id"],
345            source_relations,
346            source_fields,
347        );
348
349        source
350            .validate()
351            .expect("entity-owned matching relation edge should validate");
352    }
353
354    #[test]
355    fn entity_validation_rejects_stable_source_relation_field_to_heap_target() {
356        let module = "schema_entity_relation_field_stable_to_heap";
357        let source_store_path = "schema_entity_relation_field_stable_to_heap::StableStore";
358        let target_store_path = "schema_entity_relation_field_stable_to_heap::HeapStore";
359        let target_path = "schema_entity_relation_field_stable_to_heap::User";
360        schema_write().insert_node(SchemaNode::Store(stable_store_in_module(
361            module,
362            "StableStore",
363        )));
364        schema_write().insert_node(SchemaNode::Store(heap_store_in_module(module, "HeapStore")));
365        schema_write().insert_node(SchemaNode::Entity(entity_in_module(
366            module,
367            "User",
368            &["id"],
369            target_store_path,
370            &[],
371            Box::leak(vec![field("id", Primitive::Ulid)].into_boxed_slice()),
372        )));
373
374        let source = entity_in_module(
375            module,
376            "Post",
377            &["id"],
378            source_store_path,
379            &[],
380            Box::leak(
381                vec![
382                    field("id", Primitive::Ulid),
383                    relation_field("author_id", Primitive::Ulid, target_path),
384                ]
385                .into_boxed_slice(),
386            ),
387        );
388
389        let err = source
390            .validate()
391            .expect_err("stable source relation into heap target should reject");
392        assert_eq!(err.messages().len(), 1);
393        assert!(err.children().is_empty());
394    }
395
396    #[test]
397    fn entity_validation_allows_heap_source_relation_field_to_heap_target() {
398        let module = "schema_entity_relation_field_heap_to_heap";
399        let store_path = "schema_entity_relation_field_heap_to_heap::HeapStore";
400        let target_path = "schema_entity_relation_field_heap_to_heap::User";
401        schema_write().insert_node(SchemaNode::Store(heap_store_in_module(module, "HeapStore")));
402        schema_write().insert_node(SchemaNode::Entity(entity_in_module(
403            module,
404            "User",
405            &["id"],
406            store_path,
407            &[],
408            Box::leak(vec![field("id", Primitive::Ulid)].into_boxed_slice()),
409        )));
410
411        let source = entity_in_module(
412            module,
413            "Post",
414            &["id"],
415            store_path,
416            &[],
417            Box::leak(
418                vec![
419                    field("id", Primitive::Ulid),
420                    relation_field("author_id", Primitive::Ulid, target_path),
421                ]
422                .into_boxed_slice(),
423            ),
424        );
425
426        source
427            .validate()
428            .expect("heap source relation into heap target should keep live validation semantics");
429    }
430
431    #[test]
432    fn entity_validation_rejects_stable_source_relation_edge_to_heap_target() {
433        let module = "schema_entity_relation_edge_stable_to_heap";
434        let source_store_path = "schema_entity_relation_edge_stable_to_heap::StableStore";
435        let target_store_path = "schema_entity_relation_edge_stable_to_heap::HeapStore";
436        schema_write().insert_node(SchemaNode::Store(stable_store_in_module(
437            module,
438            "StableStore",
439        )));
440        schema_write().insert_node(SchemaNode::Store(heap_store_in_module(module, "HeapStore")));
441        let target_fields = Box::leak(
442            vec![
443                field("tenant_id", Primitive::Nat64),
444                field("id", Primitive::Ulid),
445            ]
446            .into_boxed_slice(),
447        );
448        schema_write().insert_node(SchemaNode::Entity(entity_in_module(
449            module,
450            "User",
451            &["tenant_id", "id"],
452            target_store_path,
453            &[],
454            target_fields,
455        )));
456
457        let source_relations = Box::leak(
458            vec![RelationEdge::new(
459                "author",
460                "schema_entity_relation_edge_stable_to_heap::User",
461                &["author_tenant_id", "author_id"],
462            )]
463            .into_boxed_slice(),
464        );
465        let source = entity_in_module(
466            module,
467            "Post",
468            &["id"],
469            source_store_path,
470            source_relations,
471            Box::leak(
472                vec![
473                    field("id", Primitive::Ulid),
474                    field("author_tenant_id", Primitive::Nat64),
475                    field("author_id", Primitive::Ulid),
476                ]
477                .into_boxed_slice(),
478            ),
479        );
480
481        let err = source
482            .validate()
483            .expect_err("stable source relation edge into heap target should reject");
484        assert_eq!(err.messages().len(), 1);
485        assert!(err.children().is_empty());
486    }
487
488    #[test]
489    fn entity_validation_reports_relation_edge_errors_under_relation_name() {
490        let store_path = "schema_entity_relation_edge_error::Store";
491        schema_write().insert_node(SchemaNode::Store(Store::new_stable(
492            Def::new("schema_entity_relation_edge_error", "Store"),
493            "STORE",
494            "schema_entity_relation_edge_error_store",
495            store_path,
496            StoreStableMemoryConfig::new(113, 114, 115),
497        )));
498        let target_fields = Box::leak(
499            vec![
500                field("tenant_id", Primitive::Nat64),
501                field("id", Primitive::Ulid),
502            ]
503            .into_boxed_slice(),
504        );
505        schema_write().insert_node(SchemaNode::Entity(entity(
506            "User",
507            store_path,
508            &["tenant_id", "id"],
509            &[],
510            target_fields,
511        )));
512
513        let source_fields = Box::leak(vec![field("author_id", Primitive::Ulid)].into_boxed_slice());
514        let source_relations = Box::leak(
515            vec![RelationEdge::new(
516                "author",
517                "schema_entity_relation_edge_error::User",
518                &["author_id"],
519            )]
520            .into_boxed_slice(),
521        );
522        let source = entity(
523            "BrokenPost",
524            store_path,
525            &["author_id"],
526            source_relations,
527            source_fields,
528        );
529
530        let err = source
531            .validate()
532            .expect_err("entity validation should reject invalid relation edge");
533
534        assert!(
535            err.children().contains_key("author"),
536            "relation edge errors should be nested under relation name: {err}",
537        );
538    }
539}