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 store: &'static str,
21 schema_version: u32,
22 primary_key: PrimaryKey,
23
24 #[serde(skip_serializing_if = "<[_]>::is_empty")]
25 indexes: &'static [Index],
26
27 #[serde(skip_serializing_if = "<[_]>::is_empty")]
28 relations: &'static [RelationEdge],
29
30 #[serde(skip_serializing_if = "<[_]>::is_empty")]
31 constraints: &'static [CheckConstraint],
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 indexes: &'static [Index],
49 relations: &'static [RelationEdge],
50 constraints: &'static [CheckConstraint],
51 fields: FieldList,
52 ty: Type,
53 ) -> Self {
54 Self {
55 def,
56 store,
57 schema_version,
58 primary_key,
59 indexes,
60 relations,
61 constraints,
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) -> &'static str {
89 self.def().ident()
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]
104 pub const fn constraints(&self) -> &'static [CheckConstraint] {
105 self.constraints
106 }
107
108 #[must_use]
109 pub const fn fields(&self) -> &FieldList {
110 &self.fields
111 }
112
113 #[must_use]
114 pub const fn ty(&self) -> &Type {
115 &self.ty
116 }
117
118 #[must_use]
121 pub fn scalar_primary_key_field(&self) -> Option<&Field> {
122 self.fields().get(self.primary_key().scalar_field()?)
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.name(), target);
129 }
130 }
131
132 for relation in self.relations() {
133 self.validate_relation_target_storage_policy(errs, relation.name(), 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 validate_source_name(
193 &mut errs,
194 "entity",
195 self.name(),
196 icydb_schema::EntitySourceKey::try_new,
197 );
198 if self.schema_version() == 0 {
199 err!(errs, "entity schema_version must be a positive integer");
200 }
201
202 {
203 let schema = schema_read();
204
205 match schema.cast_node::<Store>(self.store()) {
207 Ok(_) => {}
208 Err(e) => errs.add(e),
209 }
210 }
211
212 for index in self.indexes() {
213 validate_source_name(
214 &mut errs,
215 "index",
216 index.name(),
217 icydb_schema::IndexSourceKey::try_new,
218 );
219 }
220 for relation in self.relations() {
221 validate_source_name(
222 &mut errs,
223 "relation",
224 relation.name(),
225 icydb_schema::RelationSourceKey::try_new,
226 );
227 if let Err(e) = relation.validate_for_source(self) {
228 errs.merge_for(relation.name(), e);
229 }
230 }
231 validate_entity_local_names(self, &mut errs);
232 self.validate_relation_storage_policy(&mut errs);
233
234 errs.result()
235 }
236}
237
238impl VisitableNode for Entity {
239 fn route_key(&self) -> String {
240 self.def().path()
241 }
242
243 fn drive<V: Visitor>(&self, v: &mut V) {
244 self.def().accept(v);
245 self.fields().accept(v);
246 for constraint in self.constraints() {
247 constraint.accept(v);
248 }
249 self.ty().accept(v);
250 }
251}
252
253fn validate_entity_local_names(entity: &Entity, errs: &mut ErrorTree) {
254 validate_unique_local_keys(
255 errs,
256 "field",
257 entity.fields().fields().iter().map(Field::name),
258 );
259 validate_unique_local_keys(errs, "index", entity.indexes().iter().map(Index::name));
260 validate_unique_local_keys(
261 errs,
262 "relation",
263 entity.relations().iter().map(RelationEdge::name),
264 );
265 validate_unique_local_keys(
266 errs,
267 "constraint",
268 entity.constraints().iter().map(CheckConstraint::name),
269 );
270}
271
272fn validate_unique_local_keys<'a>(
273 errs: &mut ErrorTree,
274 kind: &str,
275 names: impl IntoIterator<Item = &'a str>,
276) {
277 let mut seen = std::collections::BTreeSet::new();
278 for name in names {
279 if !seen.insert(name) {
280 err!(errs, "duplicate {kind} name '{name}' within entity",);
281 }
282 }
283}