1#[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 source_key: &'static str,
21 store: &'static str,
22 schema_version: u32,
23 primary_key: PrimaryKey,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
26 name: Option<&'static str>,
27
28 #[serde(skip_serializing_if = "<[_]>::is_empty")]
29 indexes: &'static [Index],
30
31 #[serde(skip_serializing_if = "<[_]>::is_empty")]
32 relations: &'static [RelationEdge],
33
34 #[serde(skip_serializing_if = "<[_]>::is_empty")]
35 constraints: &'static [CheckConstraint],
36
37 fields: FieldList,
38 ty: Type,
39}
40
41impl Entity {
42 #[must_use]
43 #[expect(
44 clippy::too_many_arguments,
45 reason = "schema entity construction keeps store, key, index, relation, field, and type metadata explicit"
46 )]
47 pub const fn new(
48 def: Def,
49 source_key: &'static str,
50 store: &'static str,
51 schema_version: u32,
52 primary_key: PrimaryKey,
53 name: Option<&'static str>,
54 indexes: &'static [Index],
55 relations: &'static [RelationEdge],
56 constraints: &'static [CheckConstraint],
57 fields: FieldList,
58 ty: Type,
59 ) -> Self {
60 Self {
61 def,
62 source_key,
63 store,
64 schema_version,
65 primary_key,
66 name,
67 indexes,
68 relations,
69 constraints,
70 fields,
71 ty,
72 }
73 }
74
75 #[must_use]
77 pub const fn source_key(&self) -> &'static str {
78 self.source_key
79 }
80
81 #[must_use]
82 pub const fn def(&self) -> &Def {
83 &self.def
84 }
85
86 #[must_use]
87 pub const fn store(&self) -> &'static str {
88 self.store
89 }
90
91 #[must_use]
92 pub const fn schema_version(&self) -> u32 {
93 self.schema_version
94 }
95
96 #[must_use]
97 pub const fn primary_key(&self) -> &PrimaryKey {
98 &self.primary_key
99 }
100
101 #[must_use]
102 pub const fn name(&self) -> Option<&'static str> {
103 self.name
104 }
105
106 #[must_use]
107 pub const fn indexes(&self) -> &'static [Index] {
108 self.indexes
109 }
110
111 #[must_use]
112 pub const fn relations(&self) -> &'static [RelationEdge] {
113 self.relations
114 }
115
116 #[must_use]
118 pub const fn constraints(&self) -> &'static [CheckConstraint] {
119 self.constraints
120 }
121
122 #[must_use]
123 pub const fn fields(&self) -> &FieldList {
124 &self.fields
125 }
126
127 #[must_use]
128 pub const fn ty(&self) -> &Type {
129 &self.ty
130 }
131
132 #[must_use]
135 pub fn scalar_primary_key_field(&self) -> Option<&Field> {
136 self.fields().get(self.primary_key().scalar_field()?)
137 }
138
139 #[must_use]
141 pub fn resolved_name(&self) -> &'static str {
142 self.name().unwrap_or_else(|| self.def().ident())
143 }
144
145 fn validate_relation_storage_policy(&self, errs: &mut ErrorTree) {
146 for field in self.fields().fields() {
147 if let Some(target) = field.value().item().relation() {
148 self.validate_relation_target_storage_policy(errs, field.ident(), target);
149 }
150 }
151
152 for relation in self.relations() {
153 self.validate_relation_target_storage_policy(errs, relation.ident(), relation.target());
154 }
155 }
156
157 fn validate_relation_target_storage_policy(
158 &self,
159 errs: &mut ErrorTree,
160 relation_name: &str,
161 target_path: &str,
162 ) {
163 let Some((source_capabilities, target_capabilities, target_store_path)) = ({
164 let schema = schema_read();
165 let Ok(source_store) = schema.cast_node::<Store>(self.store()) else {
166 return;
167 };
168 let Ok(target) = schema.cast_node::<Self>(target_path) else {
169 return;
170 };
171 let Ok(target_store) = schema.cast_node::<Store>(target.store()) else {
172 return;
173 };
174 let source_capabilities = source_store.storage_capabilities();
175 let target_capabilities = target_store.storage_capabilities();
176 let target_store_path = target.store().to_string();
177 drop(schema);
178
179 Some((source_capabilities, target_capabilities, target_store_path))
180 }) else {
181 return;
182 };
183
184 if matches!(
185 source_capabilities.relation_source(),
186 RelationSourceCapability::DurableSource
187 ) && matches!(
188 target_capabilities.relation_target(),
189 RelationTargetCapability::VolatileTarget
190 ) {
191 err!(
192 errs,
193 "relation '{}' from durable store '{}' to volatile target store '{}' is not supported; durable stores cannot own referential integrity against volatile heap targets",
194 relation_name,
195 self.store(),
196 target_store_path,
197 );
198 }
199 }
200}
201
202impl MacroNode for Entity {
203 fn as_any(&self) -> &dyn Any {
204 self
205 }
206}
207
208impl ValidateNode for Entity {
209 fn validate(&self) -> Result<(), ErrorTree> {
210 let mut errs = ErrorTree::new();
211
212 validate_source_key(
213 &mut errs,
214 "entity",
215 self.source_key(),
216 icydb_schema::EntitySourceKey::try_new,
217 );
218 if self.schema_version() == 0 {
219 err!(errs, "entity schema_version must be a positive integer");
220 }
221
222 {
223 let schema = schema_read();
224
225 match schema.cast_node::<Store>(self.store()) {
227 Ok(_) => {}
228 Err(e) => errs.add(e),
229 }
230 }
231
232 for index in self.indexes() {
233 validate_source_key(
234 &mut errs,
235 "index",
236 index.source_key(),
237 icydb_schema::IndexSourceKey::try_new,
238 );
239 }
240 for relation in self.relations() {
241 validate_source_key(
242 &mut errs,
243 "relation",
244 relation.source_key(),
245 icydb_schema::RelationSourceKey::try_new,
246 );
247 if let Err(e) = relation.validate_for_source(self) {
248 errs.merge_for(relation.ident(), e);
249 }
250 }
251 validate_entity_local_source_keys(self, &mut errs);
252 self.validate_relation_storage_policy(&mut errs);
253
254 errs.result()
255 }
256}
257
258impl VisitableNode for Entity {
259 fn route_key(&self) -> String {
260 self.def().path()
261 }
262
263 fn drive<V: Visitor>(&self, v: &mut V) {
264 self.def().accept(v);
265 self.fields().accept(v);
266 for constraint in self.constraints() {
267 constraint.accept(v);
268 }
269 self.ty().accept(v);
270 }
271}
272
273fn validate_entity_local_source_keys(entity: &Entity, errs: &mut ErrorTree) {
274 validate_unique_local_keys(
275 errs,
276 "field",
277 entity.fields().fields().iter().map(Field::source_key),
278 );
279 validate_unique_local_keys(
280 errs,
281 "index",
282 entity.indexes().iter().map(Index::source_key),
283 );
284 validate_unique_local_keys(
285 errs,
286 "relation",
287 entity.relations().iter().map(RelationEdge::source_key),
288 );
289 validate_unique_local_keys(
290 errs,
291 "constraint",
292 entity.constraints().iter().map(CheckConstraint::source_key),
293 );
294}
295
296fn validate_unique_local_keys<'a>(
297 errs: &mut ErrorTree,
298 kind: &str,
299 source_keys: impl IntoIterator<Item = &'a str>,
300) {
301 let mut seen = std::collections::BTreeSet::new();
302 for source_key in source_keys {
303 if !seen.insert(source_key) {
304 err!(
305 errs,
306 "duplicate {kind} source key '{source_key}' within entity",
307 );
308 }
309 }
310}