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    schema_version: u32,
13    primary_key: PrimaryKey,
14
15    #[serde(skip_serializing_if = "Option::is_none")]
16    name: Option<&'static str>,
17
18    #[serde(skip_serializing_if = "<[_]>::is_empty")]
19    indexes: &'static [Index],
20
21    #[serde(skip_serializing_if = "<[_]>::is_empty")]
22    relations: &'static [RelationEdge],
23
24    fields: FieldList,
25    ty: Type,
26}
27
28impl Entity {
29    #[must_use]
30    #[expect(
31        clippy::too_many_arguments,
32        reason = "schema entity construction keeps store, key, index, relation, field, and type metadata explicit"
33    )]
34    pub const fn new(
35        def: Def,
36        store: &'static str,
37        schema_version: u32,
38        primary_key: PrimaryKey,
39        name: Option<&'static str>,
40        indexes: &'static [Index],
41        relations: &'static [RelationEdge],
42        fields: FieldList,
43        ty: Type,
44    ) -> Self {
45        Self {
46            def,
47            store,
48            schema_version,
49            primary_key,
50            name,
51            indexes,
52            relations,
53            fields,
54            ty,
55        }
56    }
57
58    #[must_use]
59    pub const fn def(&self) -> &Def {
60        &self.def
61    }
62
63    #[must_use]
64    pub const fn store(&self) -> &'static str {
65        self.store
66    }
67
68    #[must_use]
69    pub const fn schema_version(&self) -> u32 {
70        self.schema_version
71    }
72
73    #[must_use]
74    pub const fn primary_key(&self) -> &PrimaryKey {
75        &self.primary_key
76    }
77
78    #[must_use]
79    pub const fn name(&self) -> Option<&'static str> {
80        self.name
81    }
82
83    #[must_use]
84    pub const fn indexes(&self) -> &'static [Index] {
85        self.indexes
86    }
87
88    #[must_use]
89    pub const fn relations(&self) -> &'static [RelationEdge] {
90        self.relations
91    }
92
93    #[must_use]
94    pub const fn fields(&self) -> &FieldList {
95        &self.fields
96    }
97
98    #[must_use]
99    pub const fn ty(&self) -> &Type {
100        &self.ty
101    }
102
103    /// Return the scalar primary key field if this entity uses a scalar
104    /// primary-key contract.
105    #[must_use]
106    pub fn scalar_primary_key_field(&self) -> Option<&Field> {
107        self.fields().get(self.primary_key().scalar_field()?)
108    }
109
110    /// Resolve the entity name used for schema identity.
111    #[must_use]
112    pub fn resolved_name(&self) -> &'static str {
113        self.name().unwrap_or_else(|| self.def().ident())
114    }
115
116    fn validate_relation_storage_policy(&self, errs: &mut ErrorTree) {
117        for field in self.fields().fields() {
118            if let Some(target) = field.value().item().relation() {
119                self.validate_relation_target_storage_policy(errs, field.ident(), target);
120            }
121        }
122
123        for relation in self.relations() {
124            self.validate_relation_target_storage_policy(errs, relation.ident(), relation.target());
125        }
126    }
127
128    fn validate_relation_target_storage_policy(
129        &self,
130        errs: &mut ErrorTree,
131        relation_name: &str,
132        target_path: &str,
133    ) {
134        let schema = schema_read();
135        let Ok(source_store) = schema.cast_node::<Store>(self.store()) else {
136            return;
137        };
138        let Ok(target) = schema.cast_node::<Self>(target_path) else {
139            return;
140        };
141        let Ok(target_store) = schema.cast_node::<Store>(target.store()) else {
142            return;
143        };
144
145        let source_capabilities = source_store.storage_capabilities();
146        let target_capabilities = target_store.storage_capabilities();
147        if matches!(
148            source_capabilities.relation_source(),
149            RelationSourceCapability::DurableSource
150        ) && matches!(
151            target_capabilities.relation_target(),
152            RelationTargetCapability::VolatileTarget
153        ) {
154            err!(
155                errs,
156                "relation '{}' from durable store '{}' to volatile target store '{}' is not supported; durable stores cannot own referential integrity against volatile heap targets",
157                relation_name,
158                self.store(),
159                target.store(),
160            );
161        }
162    }
163}
164
165impl MacroNode for Entity {
166    fn as_any(&self) -> &dyn Any {
167        self
168    }
169}
170
171impl ValidateNode for Entity {
172    fn validate(&self) -> Result<(), ErrorTree> {
173        let mut errs = ErrorTree::new();
174        let schema = schema_read();
175
176        if self.schema_version() == 0 {
177            err!(errs, "entity schema_version must be a positive integer");
178        }
179
180        // store
181        match schema.cast_node::<Store>(self.store()) {
182            Ok(_) => {}
183            Err(e) => errs.add(e),
184        }
185
186        for relation in self.relations() {
187            if let Err(e) = relation.validate_for_source(self) {
188                errs.merge_for(relation.ident(), e);
189            }
190        }
191        self.validate_relation_storage_policy(&mut errs);
192
193        errs.result()
194    }
195}
196
197impl VisitableNode for Entity {
198    fn route_key(&self) -> String {
199        self.def().path()
200    }
201
202    fn drive<V: Visitor>(&self, v: &mut V) {
203        self.def().accept(v);
204        self.fields().accept(v);
205        self.ty().accept(v);
206    }
207}
208
209#[cfg(test)]
210mod tests;