1use crate::prelude::*;
2use std::any::Any;
3
4#[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 #[must_use]
98 pub fn scalar_primary_key_field(&self) -> Option<&Field> {
99 self.fields().get(self.primary_key().scalar_field()?)
100 }
101
102 #[must_use]
104 pub fn resolved_name(&self) -> &'static str {
105 self.name().unwrap_or_else(|| self.def().ident())
106 }
107
108 fn validate_relation_storage_policy(&self, errs: &mut ErrorTree) {
109 for field in self.fields().fields() {
110 if let Some(target) = field.value().item().relation() {
111 self.validate_relation_target_storage_policy(errs, field.ident(), target);
112 }
113 }
114
115 for relation in self.relations() {
116 self.validate_relation_target_storage_policy(errs, relation.ident(), relation.target());
117 }
118 }
119
120 fn validate_relation_target_storage_policy(
121 &self,
122 errs: &mut ErrorTree,
123 relation_name: &str,
124 target_path: &str,
125 ) {
126 let schema = schema_read();
127 let Ok(source_store) = schema.cast_node::<Store>(self.store()) else {
128 return;
129 };
130 let Ok(target) = schema.cast_node::<Self>(target_path) else {
131 return;
132 };
133 let Ok(target_store) = schema.cast_node::<Store>(target.store()) else {
134 return;
135 };
136
137 if source_store.is_stable_storage() && target_store.is_heap_storage() {
138 err!(
139 errs,
140 "relation '{}' from stable store '{}' to heap target store '{}' is not supported in 0.169; stable stores cannot own referential integrity against volatile heap targets",
141 relation_name,
142 self.store(),
143 target.store(),
144 );
145 }
146 }
147}
148
149impl MacroNode for Entity {
150 fn as_any(&self) -> &dyn Any {
151 self
152 }
153}
154
155impl ValidateNode for Entity {
156 fn validate(&self) -> Result<(), ErrorTree> {
157 let mut errs = ErrorTree::new();
158 let schema = schema_read();
159
160 match schema.cast_node::<Store>(self.store()) {
162 Ok(_) => {}
163 Err(e) => errs.add(e),
164 }
165
166 for relation in self.relations() {
167 if let Err(e) = relation.validate_for_source(self) {
168 errs.merge_for(relation.ident(), e);
169 }
170 }
171 self.validate_relation_storage_policy(&mut errs);
172
173 errs.result()
174 }
175}
176
177impl VisitableNode for Entity {
178 fn route_key(&self) -> String {
179 self.def().path()
180 }
181
182 fn drive<V: Visitor>(&self, v: &mut V) {
183 self.def().accept(v);
184 self.fields().accept(v);
185 self.ty().accept(v);
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use crate::build::schema_write;
193
194 fn primitive_item(primitive: Primitive) -> Item {
195 Item::new(
196 ItemTarget::Primitive(primitive),
197 None,
198 None,
199 None,
200 None,
201 &[],
202 &[],
203 false,
204 )
205 }
206
207 fn relation_item(primitive: Primitive, target: &'static str) -> Item {
208 Item::new(
209 ItemTarget::Primitive(primitive),
210 Some(target),
211 None,
212 None,
213 None,
214 &[],
215 &[],
216 false,
217 )
218 }
219
220 fn field(ident: &'static str, primitive: Primitive) -> Field {
221 Field::new(
222 ident,
223 Value::new(Cardinality::One, primitive_item(primitive)),
224 None,
225 None,
226 None,
227 )
228 }
229
230 fn relation_field(ident: &'static str, primitive: Primitive, target: &'static str) -> Field {
231 Field::new(
232 ident,
233 Value::new(Cardinality::One, relation_item(primitive, target)),
234 None,
235 None,
236 None,
237 )
238 }
239
240 fn store(path: &'static str) -> Store {
241 Store::new_stable(
242 Def::new("schema_entity_relation_edge", "Store"),
243 "STORE",
244 "schema_entity_relation_edge_store",
245 path,
246 StoreStableMemoryConfig::new(110, 111, 112),
247 )
248 }
249
250 fn stable_store_in_module(module: &'static str, ident: &'static str) -> Store {
251 Store::new_stable(
252 Def::new(module, ident),
253 "STORE",
254 "schema_entity_relation_edge_store",
255 "schema_entity_relation_edge_store",
256 StoreStableMemoryConfig::new(120, 121, 122),
257 )
258 }
259
260 fn heap_store_in_module(module: &'static str, ident: &'static str) -> Store {
261 Store::new_heap(
262 Def::new(module, ident),
263 "HEAP_STORE",
264 "schema_entity_relation_edge_heap_store",
265 "schema_entity_relation_edge_heap_store",
266 StoreHeapConfig::new(),
267 )
268 }
269
270 fn entity(
271 ident: &'static str,
272 store_path: &'static str,
273 pk_fields: &'static [&'static str],
274 relations: &'static [RelationEdge],
275 fields: &'static [Field],
276 ) -> Entity {
277 entity_in_module(
278 "schema_entity_relation_edge",
279 ident,
280 pk_fields,
281 store_path,
282 relations,
283 fields,
284 )
285 }
286
287 fn entity_in_module(
288 module: &'static str,
289 ident: &'static str,
290 pk_fields: &'static [&'static str],
291 store_path: &'static str,
292 relations: &'static [RelationEdge],
293 fields: &'static [Field],
294 ) -> Entity {
295 Entity::new(
296 Def::new(module, ident),
297 store_path,
298 PrimaryKey::new(pk_fields, PrimaryKeySource::External),
299 None,
300 &[],
301 relations,
302 FieldList::new(fields),
303 Type::new(&[], &[]),
304 )
305 }
306
307 #[test]
308 fn entity_validation_checks_owned_relation_edges() {
309 let store_path = "schema_entity_relation_edge::Store";
310 schema_write().insert_node(SchemaNode::Store(store(store_path)));
311 let target_fields = Box::leak(
312 vec![
313 field("tenant_id", Primitive::Nat64),
314 field("id", Primitive::Ulid),
315 ]
316 .into_boxed_slice(),
317 );
318 schema_write().insert_node(SchemaNode::Entity(entity(
319 "User",
320 store_path,
321 &["tenant_id", "id"],
322 &[],
323 target_fields,
324 )));
325
326 let source_fields = Box::leak(
327 vec![
328 field("author_tenant_id", Primitive::Nat64),
329 field("author_id", Primitive::Ulid),
330 ]
331 .into_boxed_slice(),
332 );
333 let source_relations = Box::leak(
334 vec![RelationEdge::new(
335 "author",
336 "schema_entity_relation_edge::User",
337 &["author_tenant_id", "author_id"],
338 )]
339 .into_boxed_slice(),
340 );
341 let source = entity(
342 "Post",
343 store_path,
344 &["author_id"],
345 source_relations,
346 source_fields,
347 );
348
349 source
350 .validate()
351 .expect("entity-owned matching relation edge should validate");
352 }
353
354 #[test]
355 fn entity_validation_rejects_stable_source_relation_field_to_heap_target() {
356 let module = "schema_entity_relation_field_stable_to_heap";
357 let source_store_path = "schema_entity_relation_field_stable_to_heap::StableStore";
358 let target_store_path = "schema_entity_relation_field_stable_to_heap::HeapStore";
359 let target_path = "schema_entity_relation_field_stable_to_heap::User";
360 schema_write().insert_node(SchemaNode::Store(stable_store_in_module(
361 module,
362 "StableStore",
363 )));
364 schema_write().insert_node(SchemaNode::Store(heap_store_in_module(module, "HeapStore")));
365 schema_write().insert_node(SchemaNode::Entity(entity_in_module(
366 module,
367 "User",
368 &["id"],
369 target_store_path,
370 &[],
371 Box::leak(vec![field("id", Primitive::Ulid)].into_boxed_slice()),
372 )));
373
374 let source = entity_in_module(
375 module,
376 "Post",
377 &["id"],
378 source_store_path,
379 &[],
380 Box::leak(
381 vec![
382 field("id", Primitive::Ulid),
383 relation_field("author_id", Primitive::Ulid, target_path),
384 ]
385 .into_boxed_slice(),
386 ),
387 );
388
389 let err = source
390 .validate()
391 .expect_err("stable source relation into heap target should reject");
392 assert_eq!(err.messages().len(), 1);
393 assert!(err.children().is_empty());
394 }
395
396 #[test]
397 fn entity_validation_allows_heap_source_relation_field_to_heap_target() {
398 let module = "schema_entity_relation_field_heap_to_heap";
399 let store_path = "schema_entity_relation_field_heap_to_heap::HeapStore";
400 let target_path = "schema_entity_relation_field_heap_to_heap::User";
401 schema_write().insert_node(SchemaNode::Store(heap_store_in_module(module, "HeapStore")));
402 schema_write().insert_node(SchemaNode::Entity(entity_in_module(
403 module,
404 "User",
405 &["id"],
406 store_path,
407 &[],
408 Box::leak(vec![field("id", Primitive::Ulid)].into_boxed_slice()),
409 )));
410
411 let source = entity_in_module(
412 module,
413 "Post",
414 &["id"],
415 store_path,
416 &[],
417 Box::leak(
418 vec![
419 field("id", Primitive::Ulid),
420 relation_field("author_id", Primitive::Ulid, target_path),
421 ]
422 .into_boxed_slice(),
423 ),
424 );
425
426 source
427 .validate()
428 .expect("heap source relation into heap target should keep live validation semantics");
429 }
430
431 #[test]
432 fn entity_validation_rejects_stable_source_relation_edge_to_heap_target() {
433 let module = "schema_entity_relation_edge_stable_to_heap";
434 let source_store_path = "schema_entity_relation_edge_stable_to_heap::StableStore";
435 let target_store_path = "schema_entity_relation_edge_stable_to_heap::HeapStore";
436 schema_write().insert_node(SchemaNode::Store(stable_store_in_module(
437 module,
438 "StableStore",
439 )));
440 schema_write().insert_node(SchemaNode::Store(heap_store_in_module(module, "HeapStore")));
441 let target_fields = Box::leak(
442 vec![
443 field("tenant_id", Primitive::Nat64),
444 field("id", Primitive::Ulid),
445 ]
446 .into_boxed_slice(),
447 );
448 schema_write().insert_node(SchemaNode::Entity(entity_in_module(
449 module,
450 "User",
451 &["tenant_id", "id"],
452 target_store_path,
453 &[],
454 target_fields,
455 )));
456
457 let source_relations = Box::leak(
458 vec![RelationEdge::new(
459 "author",
460 "schema_entity_relation_edge_stable_to_heap::User",
461 &["author_tenant_id", "author_id"],
462 )]
463 .into_boxed_slice(),
464 );
465 let source = entity_in_module(
466 module,
467 "Post",
468 &["id"],
469 source_store_path,
470 source_relations,
471 Box::leak(
472 vec![
473 field("id", Primitive::Ulid),
474 field("author_tenant_id", Primitive::Nat64),
475 field("author_id", Primitive::Ulid),
476 ]
477 .into_boxed_slice(),
478 ),
479 );
480
481 let err = source
482 .validate()
483 .expect_err("stable source relation edge into heap target should reject");
484 assert_eq!(err.messages().len(), 1);
485 assert!(err.children().is_empty());
486 }
487
488 #[test]
489 fn entity_validation_reports_relation_edge_errors_under_relation_name() {
490 let store_path = "schema_entity_relation_edge_error::Store";
491 schema_write().insert_node(SchemaNode::Store(Store::new_stable(
492 Def::new("schema_entity_relation_edge_error", "Store"),
493 "STORE",
494 "schema_entity_relation_edge_error_store",
495 store_path,
496 StoreStableMemoryConfig::new(113, 114, 115),
497 )));
498 let target_fields = Box::leak(
499 vec![
500 field("tenant_id", Primitive::Nat64),
501 field("id", Primitive::Ulid),
502 ]
503 .into_boxed_slice(),
504 );
505 schema_write().insert_node(SchemaNode::Entity(entity(
506 "User",
507 store_path,
508 &["tenant_id", "id"],
509 &[],
510 target_fields,
511 )));
512
513 let source_fields = Box::leak(vec![field("author_id", Primitive::Ulid)].into_boxed_slice());
514 let source_relations = Box::leak(
515 vec![RelationEdge::new(
516 "author",
517 "schema_entity_relation_edge_error::User",
518 &["author_id"],
519 )]
520 .into_boxed_slice(),
521 );
522 let source = entity(
523 "BrokenPost",
524 store_path,
525 &["author_id"],
526 source_relations,
527 source_fields,
528 );
529
530 let err = source
531 .validate()
532 .expect_err("entity validation should reject invalid relation edge");
533
534 assert!(
535 err.children().contains_key("author"),
536 "relation edge errors should be nested under relation name: {err}",
537 );
538 }
539}