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