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 Some((source_capabilities, target_capabilities, target_store_path)) = ({
144 let schema = schema_read();
145 let Ok(source_store) = schema.cast_node::<Store>(self.store()) else {
146 return;
147 };
148 let Ok(target) = schema.cast_node::<Self>(target_path) else {
149 return;
150 };
151 let Ok(target_store) = schema.cast_node::<Store>(target.store()) else {
152 return;
153 };
154 let source_capabilities = source_store.storage_capabilities();
155 let target_capabilities = target_store.storage_capabilities();
156 let target_store_path = target.store().to_string();
157 drop(schema);
158
159 Some((source_capabilities, target_capabilities, target_store_path))
160 }) else {
161 return;
162 };
163
164 if matches!(
165 source_capabilities.relation_source(),
166 RelationSourceCapability::DurableSource
167 ) && matches!(
168 target_capabilities.relation_target(),
169 RelationTargetCapability::VolatileTarget
170 ) {
171 err!(
172 errs,
173 "relation '{}' from durable store '{}' to volatile target store '{}' is not supported; durable stores cannot own referential integrity against volatile heap targets",
174 relation_name,
175 self.store(),
176 target_store_path,
177 );
178 }
179 }
180}
181
182impl MacroNode for Entity {
183 fn as_any(&self) -> &dyn Any {
184 self
185 }
186}
187
188impl ValidateNode for Entity {
189 fn validate(&self) -> Result<(), ErrorTree> {
190 let mut errs = ErrorTree::new();
191
192 if self.schema_version() == 0 {
193 err!(errs, "entity schema_version must be a positive integer");
194 }
195
196 {
197 let schema = schema_read();
198
199 match schema.cast_node::<Store>(self.store()) {
201 Ok(_) => {}
202 Err(e) => errs.add(e),
203 }
204 }
205
206 for relation in self.relations() {
207 if let Err(e) = relation.validate_for_source(self) {
208 errs.merge_for(relation.ident(), e);
209 }
210 }
211 self.validate_relation_storage_policy(&mut errs);
212
213 errs.result()
214 }
215}
216
217impl VisitableNode for Entity {
218 fn route_key(&self) -> String {
219 self.def().path()
220 }
221
222 fn drive<V: Visitor>(&self, v: &mut V) {
223 self.def().accept(v);
224 self.fields().accept(v);
225 self.ty().accept(v);
226 }
227}