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
109impl MacroNode for Entity {
110    fn as_any(&self) -> &dyn Any {
111        self
112    }
113}
114
115impl ValidateNode for Entity {
116    fn validate(&self) -> Result<(), ErrorTree> {
117        let mut errs = ErrorTree::new();
118        let schema = schema_read();
119
120        // store
121        match schema.cast_node::<Store>(self.store()) {
122            Ok(_) => {}
123            Err(e) => errs.add(e),
124        }
125
126        for relation in self.relations() {
127            if let Err(e) = relation.validate_for_source(self) {
128                errs.merge_for(relation.ident(), e);
129            }
130        }
131
132        errs.result()
133    }
134}
135
136impl VisitableNode for Entity {
137    fn route_key(&self) -> String {
138        self.def().path()
139    }
140
141    fn drive<V: Visitor>(&self, v: &mut V) {
142        self.def().accept(v);
143        self.fields().accept(v);
144        self.ty().accept(v);
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::build::schema_write;
152
153    fn primitive_item(primitive: Primitive) -> Item {
154        Item::new(
155            ItemTarget::Primitive(primitive),
156            None,
157            None,
158            None,
159            None,
160            &[],
161            &[],
162            false,
163        )
164    }
165
166    fn field(ident: &'static str, primitive: Primitive) -> Field {
167        Field::new(
168            ident,
169            Value::new(Cardinality::One, primitive_item(primitive)),
170            None,
171            None,
172            None,
173        )
174    }
175
176    fn store(path: &'static str) -> Store {
177        Store::new_stable(
178            Def::new("schema_entity_relation_edge", "Store"),
179            "STORE",
180            "schema_entity_relation_edge_store",
181            path,
182            StoreStableMemoryConfig::new(110, 111, 112),
183        )
184    }
185
186    fn entity(
187        ident: &'static str,
188        store_path: &'static str,
189        pk_fields: &'static [&'static str],
190        relations: &'static [RelationEdge],
191        fields: &'static [Field],
192    ) -> Entity {
193        Entity::new(
194            Def::new("schema_entity_relation_edge", ident),
195            store_path,
196            PrimaryKey::new(pk_fields, PrimaryKeySource::External),
197            None,
198            &[],
199            relations,
200            FieldList::new(fields),
201            Type::new(&[], &[]),
202        )
203    }
204
205    #[test]
206    fn entity_validation_checks_owned_relation_edges() {
207        let store_path = "schema_entity_relation_edge::Store";
208        schema_write().insert_node(SchemaNode::Store(store(store_path)));
209        let target_fields = Box::leak(
210            vec![
211                field("tenant_id", Primitive::Nat64),
212                field("id", Primitive::Ulid),
213            ]
214            .into_boxed_slice(),
215        );
216        schema_write().insert_node(SchemaNode::Entity(entity(
217            "User",
218            store_path,
219            &["tenant_id", "id"],
220            &[],
221            target_fields,
222        )));
223
224        let source_fields = Box::leak(
225            vec![
226                field("author_tenant_id", Primitive::Nat64),
227                field("author_id", Primitive::Ulid),
228            ]
229            .into_boxed_slice(),
230        );
231        let source_relations = Box::leak(
232            vec![RelationEdge::new(
233                "author",
234                "schema_entity_relation_edge::User",
235                &["author_tenant_id", "author_id"],
236            )]
237            .into_boxed_slice(),
238        );
239        let source = entity(
240            "Post",
241            store_path,
242            &["author_id"],
243            source_relations,
244            source_fields,
245        );
246
247        source
248            .validate()
249            .expect("entity-owned matching relation edge should validate");
250    }
251
252    #[test]
253    fn entity_validation_reports_relation_edge_errors_under_relation_name() {
254        let store_path = "schema_entity_relation_edge_error::Store";
255        schema_write().insert_node(SchemaNode::Store(Store::new_stable(
256            Def::new("schema_entity_relation_edge_error", "Store"),
257            "STORE",
258            "schema_entity_relation_edge_error_store",
259            store_path,
260            StoreStableMemoryConfig::new(113, 114, 115),
261        )));
262        let target_fields = Box::leak(
263            vec![
264                field("tenant_id", Primitive::Nat64),
265                field("id", Primitive::Ulid),
266            ]
267            .into_boxed_slice(),
268        );
269        schema_write().insert_node(SchemaNode::Entity(entity(
270            "User",
271            store_path,
272            &["tenant_id", "id"],
273            &[],
274            target_fields,
275        )));
276
277        let source_fields = Box::leak(vec![field("author_id", Primitive::Ulid)].into_boxed_slice());
278        let source_relations = Box::leak(
279            vec![RelationEdge::new(
280                "author",
281                "schema_entity_relation_edge_error::User",
282                &["author_id"],
283            )]
284            .into_boxed_slice(),
285        );
286        let source = entity(
287            "BrokenPost",
288            store_path,
289            &["author_id"],
290            source_relations,
291            source_fields,
292        );
293
294        let err = source
295            .validate()
296            .expect_err("entity validation should reject invalid relation edge");
297
298        assert!(
299            err.children().contains_key("author"),
300            "relation edge errors should be nested under relation name: {err}",
301        );
302    }
303}