icydb_schema/node/
entity.rs1#[cfg(test)]
8mod tests;
9
10use crate::prelude::*;
11use std::any::Any;
12
13#[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 = "Option::is_none")]
25 name: Option<&'static str>,
26
27 #[serde(skip_serializing_if = "<[_]>::is_empty")]
28 indexes: &'static [Index],
29
30 #[serde(skip_serializing_if = "<[_]>::is_empty")]
31 relations: &'static [RelationEdge],
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 name: Option<&'static str>,
49 indexes: &'static [Index],
50 relations: &'static [RelationEdge],
51 fields: FieldList,
52 ty: Type,
53 ) -> Self {
54 Self {
55 def,
56 store,
57 schema_version,
58 primary_key,
59 name,
60 indexes,
61 relations,
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) -> Option<&'static str> {
89 self.name
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 #[must_use]
103 pub const fn fields(&self) -> &FieldList {
104 &self.fields
105 }
106
107 #[must_use]
108 pub const fn ty(&self) -> &Type {
109 &self.ty
110 }
111
112 #[must_use]
115 pub fn scalar_primary_key_field(&self) -> Option<&Field> {
116 self.fields().get(self.primary_key().scalar_field()?)
117 }
118
119 #[must_use]
121 pub fn resolved_name(&self) -> &'static str {
122 self.name().unwrap_or_else(|| self.def().ident())
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.ident(), target);
129 }
130 }
131
132 for relation in self.relations() {
133 self.validate_relation_target_storage_policy(errs, relation.ident(), 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 schema = schema_read();
144 let Ok(source_store) = schema.cast_node::<Store>(self.store()) else {
145 return;
146 };
147 let Ok(target) = schema.cast_node::<Self>(target_path) else {
148 return;
149 };
150 let Ok(target_store) = schema.cast_node::<Store>(target.store()) else {
151 return;
152 };
153
154 let source_capabilities = source_store.storage_capabilities();
155 let target_capabilities = target_store.storage_capabilities();
156 if matches!(
157 source_capabilities.relation_source(),
158 RelationSourceCapability::DurableSource
159 ) && matches!(
160 target_capabilities.relation_target(),
161 RelationTargetCapability::VolatileTarget
162 ) {
163 err!(
164 errs,
165 "relation '{}' from durable store '{}' to volatile target store '{}' is not supported; durable stores cannot own referential integrity against volatile heap targets",
166 relation_name,
167 self.store(),
168 target.store(),
169 );
170 }
171 }
172}
173
174impl MacroNode for Entity {
175 fn as_any(&self) -> &dyn Any {
176 self
177 }
178}
179
180impl ValidateNode for Entity {
181 fn validate(&self) -> Result<(), ErrorTree> {
182 let mut errs = ErrorTree::new();
183 let schema = schema_read();
184
185 if self.schema_version() == 0 {
186 err!(errs, "entity schema_version must be a positive integer");
187 }
188
189 match schema.cast_node::<Store>(self.store()) {
191 Ok(_) => {}
192 Err(e) => errs.add(e),
193 }
194
195 for relation in self.relations() {
196 if let Err(e) = relation.validate_for_source(self) {
197 errs.merge_for(relation.ident(), e);
198 }
199 }
200 self.validate_relation_storage_policy(&mut errs);
201
202 errs.result()
203 }
204}
205
206impl VisitableNode for Entity {
207 fn route_key(&self) -> String {
208 self.def().path()
209 }
210
211 fn drive<V: Visitor>(&self, v: &mut V) {
212 self.def().accept(v);
213 self.fields().accept(v);
214 self.ty().accept(v);
215 }
216}