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