Skip to main content

icydb_core/model/
entity.rs

1//! Module: model::entity
2//! Responsibility: runtime entity metadata emitted by derives and used by the engine.
3//! Does not own: full schema graphs, validators, or registry orchestration.
4//! Boundary: authoritative entity-level runtime contract for planning and execution.
5
6use crate::model::{field::FieldModel, index::IndexModel};
7
8///
9/// PrimaryKeyModel
10///
11/// Ordered primary-key field metadata for one entity. The current execution
12/// engine consumes scalar projections, while this model carries the ordered
13/// field-list shape needed for composite primary keys.
14///
15
16#[derive(Debug)]
17pub struct PrimaryKeyModel {
18    fields: PrimaryKeyModelFields,
19}
20
21impl PrimaryKeyModel {
22    /// Build scalar primary-key metadata for existing generated/test models.
23    #[must_use]
24    pub const fn scalar(field: &'static FieldModel) -> Self {
25        Self {
26            fields: PrimaryKeyModelFields::Scalar(field),
27        }
28    }
29
30    /// Build ordered primary-key metadata from generated field references.
31    ///
32    /// # Panics
33    ///
34    /// Panics when `fields` is empty.
35    #[must_use]
36    pub const fn ordered(fields: &'static [&'static FieldModel]) -> Self {
37        assert!(!fields.is_empty(), "primary key model requires fields");
38        Self {
39            fields: PrimaryKeyModelFields::Ordered(fields),
40        }
41    }
42
43    /// Return the number of fields in this primary key.
44    #[must_use]
45    pub const fn len(&self) -> usize {
46        match self.fields {
47            PrimaryKeyModelFields::Scalar(_) => 1,
48            PrimaryKeyModelFields::Ordered(fields) => fields.len(),
49        }
50    }
51
52    /// Return whether this primary key has no fields.
53    #[must_use]
54    pub const fn is_empty(&self) -> bool {
55        self.len() == 0
56    }
57
58    /// Return whether this primary key is the scalar one-field case.
59    #[must_use]
60    pub const fn is_scalar(&self) -> bool {
61        self.len() == 1
62    }
63
64    /// Return the first primary-key field.
65    ///
66    /// Composite-aware code should consume `fields()` when it needs full row
67    /// identity. This helper exists only for metadata surfaces that still carry
68    /// one primary-key field pointer alongside ordered primary-key metadata.
69    #[must_use]
70    pub const fn first_field(&self) -> &'static FieldModel {
71        match self.fields {
72            PrimaryKeyModelFields::Scalar(field) => field,
73            PrimaryKeyModelFields::Ordered(fields) => fields[0],
74        }
75    }
76
77    /// Iterate over ordered primary-key fields.
78    #[must_use]
79    pub const fn fields(&self) -> PrimaryKeyModelFields {
80        self.fields
81    }
82}
83
84///
85/// PrimaryKeyModelFields
86///
87/// Borrowed primary-key field list without allocating on hot metadata paths.
88///
89
90#[derive(Clone, Copy, Debug)]
91pub enum PrimaryKeyModelFields {
92    Scalar(&'static FieldModel),
93    Ordered(&'static [&'static FieldModel]),
94}
95
96impl PrimaryKeyModelFields {
97    /// Return the number of fields represented by this view.
98    #[must_use]
99    pub const fn len(self) -> usize {
100        match self {
101            Self::Scalar(_) => 1,
102            Self::Ordered(fields) => fields.len(),
103        }
104    }
105
106    /// Return whether this view has no fields.
107    #[must_use]
108    pub const fn is_empty(self) -> bool {
109        self.len() == 0
110    }
111
112    /// Return the field at `index`.
113    #[must_use]
114    pub fn get(self, index: usize) -> Option<&'static FieldModel> {
115        match self {
116            Self::Scalar(field) => (index == 0).then_some(field),
117            Self::Ordered(fields) => fields.get(index).copied(),
118        }
119    }
120
121    /// Iterate over ordered primary-key fields.
122    #[must_use]
123    pub const fn iter(self) -> PrimaryKeyModelFieldIter {
124        PrimaryKeyModelFieldIter {
125            fields: self,
126            index: 0,
127        }
128    }
129}
130
131///
132/// PrimaryKeyModelFieldIter
133///
134/// Iterator over primary-key field model references.
135///
136
137#[derive(Clone, Debug)]
138pub struct PrimaryKeyModelFieldIter {
139    fields: PrimaryKeyModelFields,
140    index: usize,
141}
142
143impl Iterator for PrimaryKeyModelFieldIter {
144    type Item = &'static FieldModel;
145
146    fn next(&mut self) -> Option<Self::Item> {
147        let item = self.fields.get(self.index)?;
148        self.index += 1;
149        Some(item)
150    }
151}
152
153#[cfg(test)]
154mod primary_key_model_tests {
155    use super::{PrimaryKeyModel, PrimaryKeyModelFields};
156    use crate::model::FieldModel;
157
158    static ID_FIELD: FieldModel = FieldModel::generated("id", crate::model::FieldKind::Nat64);
159    static TENANT_FIELD: FieldModel =
160        FieldModel::generated("tenant_id", crate::model::FieldKind::Nat64);
161    static ORDERED_FIELDS: [&FieldModel; 2] = [&ID_FIELD, &TENANT_FIELD];
162
163    #[test]
164    fn scalar_primary_key_model_exposes_one_field() {
165        let model = PrimaryKeyModel::scalar(&ID_FIELD);
166
167        assert_eq!(model.len(), 1);
168        assert!(model.is_scalar());
169        assert_eq!(model.first_field().name(), "id");
170        assert_eq!(
171            model
172                .fields()
173                .iter()
174                .map(FieldModel::name)
175                .collect::<Vec<_>>(),
176            ["id"]
177        );
178    }
179
180    #[test]
181    fn ordered_primary_key_model_preserves_field_order() {
182        let model = PrimaryKeyModel::ordered(&ORDERED_FIELDS);
183
184        assert_eq!(model.len(), 2);
185        assert!(!model.is_scalar());
186        assert_eq!(model.first_field().name(), "id");
187        assert_eq!(
188            model
189                .fields()
190                .iter()
191                .map(FieldModel::name)
192                .collect::<Vec<_>>(),
193            ["id", "tenant_id"],
194        );
195        std::assert_matches!(model.fields(), PrimaryKeyModelFields::Ordered(_));
196    }
197}
198
199///
200/// RelationEdgeModel
201///
202/// Generated relation-edge proposal metadata. Runtime accepted-schema paths
203/// must still reconcile this into persisted catalog authority before
204/// execution consumes it.
205///
206
207#[derive(Debug)]
208pub struct RelationEdgeModel {
209    name: &'static str,
210    target_path: &'static str,
211    local_fields: &'static [&'static FieldModel],
212}
213
214impl RelationEdgeModel {
215    /// Build one generated relation-edge proposal from ordered local field
216    /// metadata.
217    #[must_use]
218    pub const fn generated(
219        name: &'static str,
220        target_path: &'static str,
221        local_fields: &'static [&'static FieldModel],
222    ) -> Self {
223        Self {
224            name,
225            target_path,
226            local_fields,
227        }
228    }
229
230    /// Borrow the generated relation edge name.
231    #[must_use]
232    pub const fn name(&self) -> &'static str {
233        self.name
234    }
235
236    /// Borrow the declared target entity path.
237    #[must_use]
238    pub const fn target_path(&self) -> &'static str {
239        self.target_path
240    }
241
242    /// Borrow ordered local relation component field metadata.
243    #[must_use]
244    pub const fn local_fields(&self) -> &'static [&'static FieldModel] {
245        self.local_fields
246    }
247}
248
249#[cfg(test)]
250mod relation_edge_model_tests {
251    use super::RelationEdgeModel;
252    use crate::model::{FieldKind, FieldModel};
253
254    static TENANT_FIELD: FieldModel = FieldModel::generated("tenant_id", FieldKind::Nat64);
255    static USER_FIELD: FieldModel = FieldModel::generated("user_id", FieldKind::Ulid);
256    static LOCAL_FIELDS: [&FieldModel; 2] = [&TENANT_FIELD, &USER_FIELD];
257
258    #[test]
259    fn relation_edge_model_preserves_ordered_local_fields() {
260        let relation = RelationEdgeModel::generated("author", "example::User", &LOCAL_FIELDS);
261
262        assert_eq!(relation.name(), "author");
263        assert_eq!(relation.target_path(), "example::User");
264        assert_eq!(
265            relation
266                .local_fields()
267                .iter()
268                .map(|field| field.name())
269                .collect::<Vec<_>>(),
270            ["tenant_id", "user_id"],
271        );
272    }
273}
274
275///
276/// EntityModel
277///
278/// Macro-generated runtime schema snapshot for a single entity.
279/// The planner and predicate validator consume this model directly.
280///
281
282#[derive(Debug)]
283pub struct EntityModel {
284    /// Fully-qualified Rust type path (for diagnostics).
285    pub(crate) path: &'static str,
286
287    /// Stable external name used in keys and routing.
288    pub(crate) entity_name: &'static str,
289
290    /// Source-declared schema version carried by generated proposals.
291    pub(crate) schema_version: u32,
292
293    /// Primary key field (points at an entry in `fields`).
294    pub(crate) primary_key: &'static FieldModel,
295
296    /// Stable primary-key slot within `fields`.
297    pub(crate) primary_key_slot: usize,
298
299    /// Ordered primary-key field metadata.
300    pub(crate) primary_key_model: PrimaryKeyModel,
301
302    /// Ordered field list (authoritative for runtime planning).
303    pub(crate) fields: &'static [FieldModel],
304
305    /// Index definitions (field order is significant).
306    pub(crate) indexes: &'static [&'static IndexModel],
307
308    /// Generated relation-edge proposal metadata.
309    pub(crate) relations: &'static [RelationEdgeModel],
310}
311
312impl EntityModel {
313    /// Construct one generated runtime entity descriptor.
314    ///
315    /// This constructor exists for derive/codegen output. Runtime query and
316    /// executor code treat `EntityModel` values as already validated build-time
317    /// artifacts and do not perform defensive model-shape validation per call.
318    #[must_use]
319    #[doc(hidden)]
320    pub const fn generated(
321        path: &'static str,
322        entity_name: &'static str,
323        schema_version: u32,
324        primary_key: &'static FieldModel,
325        primary_key_slot: usize,
326        fields: &'static [FieldModel],
327        indexes: &'static [&'static IndexModel],
328    ) -> Self {
329        Self {
330            path,
331            entity_name,
332            schema_version,
333            primary_key,
334            primary_key_slot,
335            primary_key_model: PrimaryKeyModel::scalar(primary_key),
336            fields,
337            indexes,
338            relations: &[],
339        }
340    }
341
342    /// Construct one generated runtime entity descriptor with explicit
343    /// ordered primary-key metadata.
344    #[must_use]
345    #[doc(hidden)]
346    pub const fn generated_with_primary_key_model(
347        path: &'static str,
348        entity_name: &'static str,
349        schema_version: u32,
350        primary_key_model: PrimaryKeyModel,
351        primary_key_slot: usize,
352        fields: &'static [FieldModel],
353        indexes: &'static [&'static IndexModel],
354    ) -> Self {
355        Self::generated_with_primary_key_model_and_relations(
356            path,
357            entity_name,
358            schema_version,
359            primary_key_model,
360            primary_key_slot,
361            fields,
362            indexes,
363            &[],
364        )
365    }
366
367    /// Construct one generated runtime entity descriptor with explicit
368    /// ordered primary-key metadata and relation-edge proposal metadata.
369    #[must_use]
370    #[doc(hidden)]
371    #[expect(
372        clippy::too_many_arguments,
373        reason = "generated entity model construction keeps path, declared version, key, field, index, and relation metadata explicit"
374    )]
375    pub const fn generated_with_primary_key_model_and_relations(
376        path: &'static str,
377        entity_name: &'static str,
378        schema_version: u32,
379        primary_key_model: PrimaryKeyModel,
380        primary_key_slot: usize,
381        fields: &'static [FieldModel],
382        indexes: &'static [&'static IndexModel],
383        relations: &'static [RelationEdgeModel],
384    ) -> Self {
385        assert!(
386            schema_version > 0,
387            "generated schema_version must be positive"
388        );
389
390        Self {
391            path,
392            entity_name,
393            schema_version,
394            primary_key: primary_key_model.first_field(),
395            primary_key_slot,
396            primary_key_model,
397            fields,
398            indexes,
399            relations,
400        }
401    }
402
403    /// Return the fully-qualified Rust path for this entity.
404    #[must_use]
405    pub const fn path(&self) -> &'static str {
406        self.path
407    }
408
409    /// Return the stable external entity name.
410    #[must_use]
411    pub const fn name(&self) -> &'static str {
412        self.entity_name
413    }
414
415    /// Return the source-declared generated schema version.
416    ///
417    /// This is proposal metadata only. Runtime accepted-schema authority still
418    /// comes from accepted catalog snapshots and identity/header facts.
419    #[must_use]
420    pub const fn declared_schema_version(&self) -> u32 {
421        self.schema_version
422    }
423
424    /// Return the primary-key field descriptor.
425    #[must_use]
426    pub const fn primary_key(&self) -> &'static FieldModel {
427        self.primary_key
428    }
429
430    /// Return ordered primary-key field metadata.
431    #[must_use]
432    pub const fn primary_key_model(&self) -> &PrimaryKeyModel {
433        &self.primary_key_model
434    }
435
436    /// Return ordered primary-key field names.
437    #[must_use]
438    pub fn primary_key_names(&self) -> Vec<&'static str> {
439        self.primary_key_model()
440            .fields()
441            .iter()
442            .map(crate::model::field::FieldModel::name)
443            .collect()
444    }
445
446    /// Return the stable primary-key slot within the ordered field table.
447    #[must_use]
448    pub const fn primary_key_slot(&self) -> usize {
449        self.primary_key_slot
450    }
451
452    /// Return the ordered runtime field descriptors.
453    #[must_use]
454    pub const fn fields(&self) -> &'static [FieldModel] {
455        self.fields
456    }
457
458    /// Return the runtime index descriptors.
459    #[must_use]
460    pub const fn indexes(&self) -> &'static [&'static IndexModel] {
461        self.indexes
462    }
463
464    /// Return generated relation-edge proposal metadata.
465    #[must_use]
466    pub const fn relations(&self) -> &'static [RelationEdgeModel] {
467        self.relations
468    }
469
470    /// Resolve one schema field name into its stable slot index.
471    #[must_use]
472    pub(crate) fn resolve_field_slot(&self, field_name: &str) -> Option<usize> {
473        self.fields
474            .iter()
475            .position(|field| field.name == field_name)
476    }
477}