Skip to main content

icydb_core/model/
field.rs

1//! Module: model::field
2//! Responsibility: runtime field metadata and storage-decode contracts.
3//! Does not own: planner-wide query semantics or row-container orchestration.
4//! Boundary: field-level runtime schema surface used by storage and planning layers.
5
6#[cfg(test)]
7use crate::value::Value;
8use crate::{types::EntityTag, value::RuntimeValueKind};
9#[cfg(test)]
10use std::cmp::Ordering;
11
12#[cfg(test)]
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub(crate) enum FieldStorageValidationError {
15    RequiredFieldNull,
16    StorageContract,
17    DecimalScaleMismatch,
18    ScalarMaxLen,
19    SetCanonicalOrder,
20    MapEntryContract,
21    MapCanonicalOrder,
22}
23
24/// Default `max_bytes` bound for `int_big` and `nat_big` field payloads.
25pub const DEFAULT_BIG_INT_MAX_BYTES: u32 = 256;
26
27///
28/// FieldStorageDecode
29///
30/// FieldStorageDecode captures how one persisted field payload must be
31/// interpreted at structural decode boundaries.
32/// Recursive enum, collection, and composite representations use canonical
33/// value decoding before accepted-schema admission proves exact shape; direct
34/// scalar and collection lanes remain kind-specific.
35///
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum FieldStorageDecode {
39    /// Decode the persisted field payload according to semantic `FieldKind`.
40    ByKind,
41    /// Decode a canonical recursive value, then apply its accepted catalog contract.
42    CatalogValue,
43}
44
45///
46/// ScalarCodec
47///
48/// ScalarCodec identifies the canonical binary leaf encoding used for one
49/// scalar persisted field payload.
50/// These codecs are fixed-width or span-bounded by the surrounding row slot
51/// container; they do not perform map/array/value dispatch.
52///
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub enum ScalarCodec {
56    Blob,
57    Bool,
58    Date,
59    Duration,
60    Float32,
61    Float64,
62    Int64,
63    Principal,
64    Subaccount,
65    Text,
66    Timestamp,
67    Nat64,
68    Ulid,
69    Unit,
70}
71
72///
73/// LeafCodec
74///
75/// LeafCodec declares whether one persisted field payload uses a dedicated
76/// scalar codec or recursive structural decoding.
77/// The row container consults this metadata before deciding whether a slot can
78/// stay on the scalar fast path.
79///
80
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub enum LeafCodec {
83    /// Dedicated scalar field codec selected by exact accepted kind.
84    Scalar(ScalarCodec),
85    /// Recursive structural field codec for accepted collections, enums, and
86    /// composites.
87    Structural,
88}
89
90///
91/// CompositeCodec
92///
93/// CompositeCodec identifies the canonical persisted grammar for one exact
94/// generated composite type. Accepted schema publication freezes this identity
95/// together with the nominal type path and complete member shape.
96///
97
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum CompositeCodec {
100    /// Existing canonical generated record, tuple, and newtype encoding.
101    StructuralV1,
102}
103
104///
105/// CompositeFieldModel
106///
107/// CompositeFieldModel describes one named member of a generated record
108/// proposal. It carries only exact persisted-shape facts; queryable nested
109/// leaves remain a derived view owned by accepted schema publication.
110///
111
112#[derive(Clone, Copy, Debug)]
113pub struct CompositeFieldModel {
114    /// Stable persisted member name.
115    pub(crate) name: &'static str,
116    /// Exact generated kind of the member payload.
117    pub(crate) kind: FieldKind,
118    /// Whether the member may contain an explicit null value.
119    pub(crate) nullable: bool,
120}
121
122impl CompositeFieldModel {
123    /// Build one generated record-member descriptor.
124    #[must_use]
125    #[doc(hidden)]
126    pub const fn generated(name: &'static str, kind: FieldKind, nullable: bool) -> Self {
127        Self {
128            name,
129            kind,
130            nullable,
131        }
132    }
133
134    /// Return the stable persisted member name.
135    #[must_use]
136    pub const fn name(&self) -> &'static str {
137        self.name
138    }
139
140    /// Return the exact generated member kind.
141    #[must_use]
142    pub const fn kind(&self) -> FieldKind {
143        self.kind
144    }
145
146    /// Return whether the member permits an explicit null value.
147    #[must_use]
148    pub const fn nullable(&self) -> bool {
149        self.nullable
150    }
151}
152
153///
154/// CompositeElementModel
155///
156/// CompositeElementModel describes one positional tuple element or the inner
157/// payload of a nominal generated newtype.
158///
159
160#[derive(Clone, Copy, Debug)]
161pub struct CompositeElementModel {
162    /// Exact generated kind of the positional payload.
163    pub(crate) kind: FieldKind,
164    /// Whether the payload may contain an explicit null value.
165    pub(crate) nullable: bool,
166}
167
168impl CompositeElementModel {
169    /// Build one generated tuple-element or newtype-inner descriptor.
170    #[must_use]
171    #[doc(hidden)]
172    pub const fn generated(kind: FieldKind, nullable: bool) -> Self {
173        Self { kind, nullable }
174    }
175
176    /// Return the exact generated payload kind.
177    #[must_use]
178    pub const fn kind(&self) -> FieldKind {
179        self.kind
180    }
181
182    /// Return whether the payload permits an explicit null value.
183    #[must_use]
184    pub const fn nullable(&self) -> bool {
185        self.nullable
186    }
187}
188
189///
190/// CompositeShapeModel
191///
192/// CompositeShapeModel is the complete generated proposal shape for one
193/// nominal record, tuple, or newtype. Empty records and empty tuples remain
194/// distinct variants.
195///
196
197#[derive(Clone, Copy, Debug)]
198pub enum CompositeShapeModel {
199    /// Named record members in persisted declaration order.
200    Record(&'static [CompositeFieldModel]),
201    /// Positional tuple members in persisted declaration order.
202    Tuple(&'static [CompositeElementModel]),
203    /// The single payload of a nominal newtype.
204    Newtype(CompositeElementModel),
205}
206
207///
208/// EnumVariantModel
209///
210/// EnumVariantModel carries structural decode metadata for one generated enum
211/// variant payload.
212/// Runtime structural decode uses this to stay on the exact field-kind
213/// contract for enum payloads.
214///
215
216#[derive(Clone, Copy, Debug)]
217pub struct EnumVariantModel {
218    /// Stable schema variant tag.
219    pub(crate) ident: &'static str,
220    /// Declared payload kind when this variant carries data.
221    payload_kind: Option<EnumPayloadKindModel>,
222    /// Persisted payload decode contract for the carried data.
223    pub(crate) payload_storage_decode: FieldStorageDecode,
224}
225
226impl EnumVariantModel {
227    /// Build one enum variant structural decode descriptor.
228    #[must_use]
229    pub const fn new(
230        ident: &'static str,
231        payload_kind: Option<&'static FieldKind>,
232        payload_storage_decode: FieldStorageDecode,
233    ) -> Self {
234        Self {
235            ident,
236            payload_kind: match payload_kind {
237                Some(kind) => Some(EnumPayloadKindModel::Static(kind)),
238                None => None,
239            },
240            payload_storage_decode,
241        }
242    }
243
244    /// Build one generated variant descriptor whose exact payload kind is
245    /// resolved lazily to avoid recursive associated-constant graphs.
246    #[must_use]
247    #[doc(hidden)]
248    pub const fn generated_with_payload_kind_resolver(
249        ident: &'static str,
250        payload_kind: Option<fn() -> FieldKind>,
251        payload_storage_decode: FieldStorageDecode,
252    ) -> Self {
253        Self {
254            ident,
255            payload_kind: match payload_kind {
256                Some(resolve) => Some(EnumPayloadKindModel::Generated(resolve)),
257                None => None,
258            },
259            payload_storage_decode,
260        }
261    }
262
263    /// Return the stable schema variant tag.
264    #[must_use]
265    pub const fn ident(&self) -> &'static str {
266        self.ident
267    }
268
269    /// Return the declared payload kind when this variant carries data.
270    #[must_use]
271    pub fn payload_kind(&self) -> Option<FieldKind> {
272        self.payload_kind.map(EnumPayloadKindModel::resolve)
273    }
274
275    /// Return the persisted payload decode contract for this variant.
276    #[must_use]
277    pub const fn payload_storage_decode(&self) -> FieldStorageDecode {
278        self.payload_storage_decode
279    }
280}
281
282#[derive(Clone, Copy, Debug)]
283enum EnumPayloadKindModel {
284    Static(&'static FieldKind),
285    Generated(fn() -> FieldKind),
286}
287
288impl EnumPayloadKindModel {
289    fn resolve(self) -> FieldKind {
290        match self {
291            Self::Static(kind) => *kind,
292            Self::Generated(resolve) => resolve(),
293        }
294    }
295}
296
297///
298/// FieldModel
299///
300/// Runtime field metadata surfaced by macro-generated `EntityModel` values.
301///
302/// This is the smallest unit consumed by predicate validation, planning,
303/// and executor-side plan checks.
304///
305
306#[derive(Debug)]
307pub struct FieldModel {
308    /// Field name as used in predicates and indexing.
309    pub(crate) name: &'static str,
310    /// Runtime type shape (no schema-layer graph nodes).
311    pub(crate) kind: FieldKind,
312    /// Generated nested-field projection for a composite record.
313    pub(crate) nested_fields: &'static [Self],
314    /// Whether the field may persist an explicit `NULL` payload.
315    pub(crate) nullable: bool,
316    /// Persisted field decode contract used by structural runtime decoders.
317    pub(crate) storage_decode: FieldStorageDecode,
318    /// Leaf payload codec used by slot readers and writers.
319    pub(crate) leaf_codec: LeafCodec,
320    /// Insert-time generation contract admitted on reduced SQL write lanes.
321    pub(crate) insert_generation: Option<FieldInsertGeneration>,
322    /// Auto-managed write contract emitted for derive-owned system fields.
323    pub(crate) write_management: Option<FieldWriteManagement>,
324    /// Database-level default contract used by persisted schema authority.
325    pub(crate) database_default: FieldDatabaseDefault,
326}
327
328///
329/// FieldDatabaseDefault
330///
331/// FieldDatabaseDefault declares the database-level default contract for one
332/// runtime field. It comes only from explicit schema/DDL default intent so
333/// schema reconciliation never treats Rust `Default` as persisted-row policy.
334///
335
336#[derive(Clone, Copy, Debug, Eq, PartialEq)]
337pub enum FieldDatabaseDefault {
338    /// No database-level default is declared for this field.
339    None,
340    /// Already-encoded persisted slot payload used as a database default.
341    ///
342    /// This is intentionally a field-codec payload, not a runtime `Value`.
343    /// Generated schema code may provide this once database defaults are
344    /// explicitly authored, but implicit Rust defaults must not flow here.
345    EncodedSlotPayload(&'static [u8]),
346    /// Name-based unit-enum default awaiting accepted-catalog admission.
347    ///
348    /// This variant is generated proposal metadata only. It must resolve to
349    /// canonical IDs before an accepted schema snapshot is persisted.
350    AuthoredEnumUnit {
351        /// Generated enum schema path.
352        enum_path: &'static str,
353        /// Generated unit variant name.
354        variant: &'static str,
355    },
356}
357
358///
359/// FieldInsertGeneration
360///
361/// FieldInsertGeneration declares whether one runtime field may be synthesized
362/// by the reduced SQL insert boundary when the user omits that field.
363/// This stays separate from typed-Rust `Default` behavior so write-time
364/// generation remains an explicit schema contract.
365///
366
367#[derive(Clone, Copy, Debug, Eq, PartialEq)]
368pub enum FieldInsertGeneration {
369    /// Generate one fresh `Ulid` value at insert time.
370    Ulid,
371    /// Generate one current wall-clock `Timestamp` value at insert time.
372    Timestamp,
373}
374
375///
376/// FieldWriteManagement
377///
378/// FieldWriteManagement declares whether one runtime field is owned by the
379/// write boundary during insert or update synthesis.
380/// This keeps auto-managed system fields explicit in schema/runtime metadata
381/// instead of relying on literal field names in write paths.
382///
383
384#[derive(Clone, Copy, Debug, Eq, PartialEq)]
385pub enum FieldWriteManagement {
386    /// Fill only on insert when the row is first created.
387    CreatedAt,
388    /// Refresh on insert and every update.
389    UpdatedAt,
390}
391
392impl FieldModel {
393    /// Build one generated runtime field descriptor.
394    ///
395    /// This constructor exists for derive/codegen output and trusted test
396    /// fixtures. Runtime planning and execution treat `FieldModel` values as
397    /// build-time-validated metadata.
398    #[must_use]
399    #[doc(hidden)]
400    pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
401        Self::generated_with_storage_decode_and_nullability(
402            name,
403            kind,
404            FieldStorageDecode::ByKind,
405            false,
406        )
407    }
408
409    /// Build one runtime field descriptor with an explicit persisted decode contract.
410    #[must_use]
411    #[doc(hidden)]
412    pub const fn generated_with_storage_decode(
413        name: &'static str,
414        kind: FieldKind,
415        storage_decode: FieldStorageDecode,
416    ) -> Self {
417        Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
418    }
419
420    /// Build one runtime field descriptor with an explicit decode contract and nullability.
421    #[must_use]
422    #[doc(hidden)]
423    pub const fn generated_with_storage_decode_and_nullability(
424        name: &'static str,
425        kind: FieldKind,
426        storage_decode: FieldStorageDecode,
427        nullable: bool,
428    ) -> Self {
429        Self::generated_with_storage_decode_nullability_and_write_policies(
430            name,
431            kind,
432            storage_decode,
433            nullable,
434            None,
435            None,
436        )
437    }
438
439    /// Build one runtime field descriptor with an explicit decode contract, nullability,
440    /// and insert-time generation contract.
441    #[must_use]
442    #[doc(hidden)]
443    pub const fn generated_with_storage_decode_nullability_and_insert_generation(
444        name: &'static str,
445        kind: FieldKind,
446        storage_decode: FieldStorageDecode,
447        nullable: bool,
448        insert_generation: Option<FieldInsertGeneration>,
449    ) -> Self {
450        Self::generated_with_storage_decode_nullability_and_write_policies(
451            name,
452            kind,
453            storage_decode,
454            nullable,
455            insert_generation,
456            None,
457        )
458    }
459
460    /// Build one runtime field descriptor with explicit insert-generation and
461    /// write-management policies.
462    #[must_use]
463    #[doc(hidden)]
464    pub const fn generated_with_storage_decode_nullability_and_write_policies(
465        name: &'static str,
466        kind: FieldKind,
467        storage_decode: FieldStorageDecode,
468        nullable: bool,
469        insert_generation: Option<FieldInsertGeneration>,
470        write_management: Option<FieldWriteManagement>,
471    ) -> Self {
472        Self {
473            name,
474            kind,
475            nested_fields: &[],
476            nullable,
477            storage_decode,
478            leaf_codec: leaf_codec_for(kind, storage_decode),
479            insert_generation,
480            write_management,
481            database_default: FieldDatabaseDefault::None,
482        }
483    }
484
485    /// Build one runtime field descriptor with nested generated-record field metadata.
486    #[must_use]
487    #[doc(hidden)]
488    pub const fn generated_with_storage_decode_nullability_write_policies_and_nested_fields(
489        name: &'static str,
490        kind: FieldKind,
491        storage_decode: FieldStorageDecode,
492        nullable: bool,
493        insert_generation: Option<FieldInsertGeneration>,
494        write_management: Option<FieldWriteManagement>,
495        nested_fields: &'static [Self],
496    ) -> Self {
497        Self::generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
498            name,
499            kind,
500            storage_decode,
501            nullable,
502            insert_generation,
503            write_management,
504            FieldDatabaseDefault::None,
505            nested_fields,
506        )
507    }
508
509    /// Build one runtime field descriptor with explicit write policies,
510    /// database default metadata, and nested generated-record field metadata.
511    #[must_use]
512    #[doc(hidden)]
513    #[expect(
514        clippy::too_many_arguments,
515        reason = "generated schema metadata keeps every field contract explicit"
516    )]
517    pub const fn generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
518        name: &'static str,
519        kind: FieldKind,
520        storage_decode: FieldStorageDecode,
521        nullable: bool,
522        insert_generation: Option<FieldInsertGeneration>,
523        write_management: Option<FieldWriteManagement>,
524        database_default: FieldDatabaseDefault,
525        nested_fields: &'static [Self],
526    ) -> Self {
527        Self {
528            name,
529            kind,
530            nested_fields,
531            nullable,
532            storage_decode,
533            leaf_codec: leaf_codec_for(kind, storage_decode),
534            insert_generation,
535            write_management,
536            database_default,
537        }
538    }
539
540    /// Return the stable field name.
541    #[must_use]
542    pub const fn name(&self) -> &'static str {
543        self.name
544    }
545
546    /// Return the runtime type-kind descriptor.
547    #[must_use]
548    pub const fn kind(&self) -> FieldKind {
549        self.kind
550    }
551
552    /// Return the generated nested-field view for a composite record.
553    #[must_use]
554    pub const fn nested_fields(&self) -> &'static [Self] {
555        self.nested_fields
556    }
557
558    /// Return whether the persisted field contract permits explicit `NULL`.
559    #[must_use]
560    pub const fn nullable(&self) -> bool {
561        self.nullable
562    }
563
564    /// Return the persisted field decode contract.
565    #[must_use]
566    pub const fn storage_decode(&self) -> FieldStorageDecode {
567        self.storage_decode
568    }
569
570    /// Return the persisted leaf payload codec.
571    #[must_use]
572    pub const fn leaf_codec(&self) -> LeafCodec {
573        self.leaf_codec
574    }
575
576    /// Return the reduced-SQL insert-time generation contract for this field.
577    #[must_use]
578    pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
579        self.insert_generation
580    }
581
582    /// Return the write-boundary management contract for this field.
583    #[must_use]
584    pub const fn write_management(&self) -> Option<FieldWriteManagement> {
585        self.write_management
586    }
587
588    /// Return the database-level default contract for this field.
589    #[must_use]
590    pub const fn database_default(&self) -> FieldDatabaseDefault {
591        self.database_default
592    }
593
594    /// Validate one runtime value against this field's persisted storage contract.
595    ///
596    /// This is the model-owned compatibility gate used before row bytes are
597    /// emitted. It intentionally checks storage compatibility, not query
598    /// predicate compatibility. Catalog-backed values retain recursive storage
599    /// decoding here; accepted-schema admission owns their exact nominal shape.
600    #[cfg(test)]
601    pub(crate) fn validate_runtime_value_for_storage(
602        &self,
603        value: &Value,
604    ) -> Result<(), FieldStorageValidationError> {
605        if matches!(value, Value::Null) {
606            if self.nullable() {
607                return Ok(());
608            }
609
610            return Err(FieldStorageValidationError::RequiredFieldNull);
611        }
612
613        let accepts = match self.storage_decode() {
614            FieldStorageDecode::CatalogValue => {
615                value_storage_kind_accepts_runtime_value(self.kind(), value)
616            }
617            FieldStorageDecode::ByKind => {
618                by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
619            }
620        };
621        if !accepts {
622            return Err(FieldStorageValidationError::StorageContract);
623        }
624
625        ensure_decimal_scale_matches(self.kind(), value)?;
626        ensure_scalar_max_len_matches(self.kind(), value)?;
627        ensure_value_is_deterministic_for_storage(self.kind(), value)
628    }
629}
630
631// Resolve the canonical leaf codec from semantic field kind plus storage
632// contract. Fields that require recursive payload decoding use the shared
633// structural codec; accepted schema remains authoritative for exact shape.
634pub(crate) const fn leaf_codec_for(
635    kind: FieldKind,
636    storage_decode: FieldStorageDecode,
637) -> LeafCodec {
638    if matches!(storage_decode, FieldStorageDecode::CatalogValue) {
639        return LeafCodec::Structural;
640    }
641
642    match kind {
643        FieldKind::Blob { .. } => LeafCodec::Scalar(ScalarCodec::Blob),
644        FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
645        FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
646        FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
647        FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
648        FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
649        FieldKind::Int8 | FieldKind::Int16 | FieldKind::Int32 | FieldKind::Int64 => {
650            LeafCodec::Scalar(ScalarCodec::Int64)
651        }
652        FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
653        FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
654        FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
655        FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
656        FieldKind::Nat8 | FieldKind::Nat16 | FieldKind::Nat32 | FieldKind::Nat64 => {
657            LeafCodec::Scalar(ScalarCodec::Nat64)
658        }
659        FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
660        FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
661        FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
662        FieldKind::Account
663        | FieldKind::Decimal { .. }
664        | FieldKind::Enum { .. }
665        | FieldKind::Int128
666        | FieldKind::IntBig { .. }
667        | FieldKind::List(_)
668        | FieldKind::Map { .. }
669        | FieldKind::Set(_)
670        | FieldKind::Composite { .. }
671        | FieldKind::Nat128
672        | FieldKind::NatBig { .. } => LeafCodec::Structural,
673    }
674}
675
676///
677/// FieldKind
678///
679/// Minimal runtime type surface needed by planning, validation, and execution.
680///
681/// Scalar and collection variants align with runtime `Value` families. Enum
682/// and composite variants additionally carry complete generated proposal
683/// metadata that accepted catalog publication resolves into stable IDs.
684///
685
686#[derive(Clone, Copy, Debug)]
687pub enum FieldKind {
688    // Scalar primitives
689    Account,
690    Blob {
691        /// Optional schema-declared maximum byte length for blob fields.
692        max_len: Option<u32>,
693    },
694    Bool,
695    Date,
696    Decimal {
697        /// Required schema-declared fractional scale for decimal fields.
698        scale: u32,
699    },
700    Duration,
701    Enum {
702        /// Fully-qualified enum type path used for strict filter normalization.
703        path: &'static str,
704        /// Declared per-variant payload decode metadata.
705        variants: &'static [EnumVariantModel],
706    },
707    Float32,
708    Float64,
709    Int8,
710    Int16,
711    Int32,
712    Int64,
713    Int128,
714    IntBig {
715        /// Maximum accepted persisted payload byte length.
716        max_bytes: u32,
717    },
718    Principal,
719    Subaccount,
720    Text {
721        /// Optional schema-declared maximum Unicode scalar count for text fields.
722        max_len: Option<u32>,
723    },
724    Timestamp,
725    Nat8,
726    Nat16,
727    Nat32,
728    Nat64,
729    Nat128,
730    NatBig {
731        /// Maximum accepted persisted payload byte length.
732        max_bytes: u32,
733    },
734    Ulid,
735    Unit,
736
737    /// Enforced typed relation; `key_kind` reflects the referenced key type.
738    Relation {
739        /// Fully-qualified Rust type path for diagnostics.
740        target_path: &'static str,
741        /// Stable external name used in storage keys.
742        target_entity_name: &'static str,
743        /// Stable runtime identity used on hot execution paths.
744        target_entity_tag: EntityTag,
745        /// Data store path where the target entity is persisted.
746        target_store_path: &'static str,
747        key_kind: &'static Self,
748    },
749
750    // Collections
751    List(&'static Self),
752    Set(&'static Self),
753    /// Deterministic, unordered key/value collection.
754    ///
755    /// Map fields are persistable and patchable, but not queryable or indexable.
756    Map {
757        key: &'static Self,
758        value: &'static Self,
759    },
760
761    /// Exact nominal generated record, tuple, or newtype.
762    Composite {
763        /// Stable fully-qualified generated type path.
764        path: &'static str,
765        /// Canonical persisted composite grammar.
766        codec: CompositeCodec,
767        /// Complete generated proposal shape.
768        shape: &'static CompositeShapeModel,
769    },
770}
771
772#[cfg(test)]
773static EMPTY_TEST_COMPOSITE_FIELDS: [CompositeFieldModel; 0] = [];
774#[cfg(test)]
775static EMPTY_TEST_COMPOSITE_SHAPE: CompositeShapeModel =
776    CompositeShapeModel::Record(&EMPTY_TEST_COMPOSITE_FIELDS);
777
778impl FieldKind {
779    /// Build one exact empty-record kind for metadata-only unit tests.
780    #[cfg(test)]
781    #[must_use]
782    pub(crate) const fn empty_test_composite(path: &'static str) -> Self {
783        Self::Composite {
784            path,
785            codec: CompositeCodec::StructuralV1,
786            shape: &EMPTY_TEST_COMPOSITE_SHAPE,
787        }
788    }
789
790    #[must_use]
791    pub const fn value_kind(&self) -> RuntimeValueKind {
792        match self {
793            Self::Account
794            | Self::Blob { .. }
795            | Self::Bool
796            | Self::Date
797            | Self::Duration
798            | Self::Enum { .. }
799            | Self::Float32
800            | Self::Float64
801            | Self::Int8
802            | Self::Int16
803            | Self::Int32
804            | Self::Int64
805            | Self::Int128
806            | Self::IntBig { .. }
807            | Self::Principal
808            | Self::Subaccount
809            | Self::Text { .. }
810            | Self::Timestamp
811            | Self::Nat8
812            | Self::Nat16
813            | Self::Nat32
814            | Self::Nat64
815            | Self::Nat128
816            | Self::NatBig { .. }
817            | Self::Ulid
818            | Self::Unit
819            | Self::Decimal { .. }
820            | Self::Relation { .. } => RuntimeValueKind::Atomic,
821            Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
822            Self::Map { .. } | Self::Composite { .. } => {
823                RuntimeValueKind::Structured { queryable: false }
824            }
825        }
826    }
827
828    /// Returns `true` if this field shape is permitted in
829    /// persisted or query-visible schemas under the current
830    /// determinism policy.
831    ///
832    /// This shape-level check is structural only; query-time policy
833    /// enforcement (for example, map predicate fencing) is applied at
834    /// query construction and validation boundaries.
835    #[must_use]
836    pub const fn is_deterministic_collection_shape(&self) -> bool {
837        match self {
838            Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
839
840            Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
841
842            Self::Map { key, value } => {
843                key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
844            }
845
846            _ => true,
847        }
848    }
849
850    /// Return true when this planner-frozen grouped field kind can stay on the
851    /// borrowed grouped-key probe path without owned canonical materialization.
852    #[cfg(test)]
853    #[must_use]
854    pub(crate) fn supports_group_probe(&self) -> bool {
855        match self {
856            Self::Enum { variants, .. } => variants.iter().all(|variant| {
857                variant
858                    .payload_kind()
859                    .is_none_or(|kind| Self::supports_group_probe(&kind))
860            }),
861            Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
862            Self::Decimal { .. } => true,
863            _ => super::field_kind_semantics::field_kind_has_identity_group_canonical_form(*self),
864        }
865    }
866
867    /// Match one runtime value against generated model metadata in tests.
868    #[cfg(test)]
869    #[must_use]
870    pub(crate) fn accepts_value(&self, value: &Value) -> bool {
871        match (self, value) {
872            (Self::Account, Value::Account(_))
873            | (Self::Blob { .. }, Value::Blob(_))
874            | (Self::Bool, Value::Bool(_))
875            | (Self::Date, Value::Date(_))
876            | (Self::Decimal { .. }, Value::Decimal(_))
877            | (Self::Duration, Value::Duration(_))
878            | (Self::Enum { .. }, Value::Enum(_))
879            | (Self::Float32, Value::Float32(_))
880            | (Self::Float64, Value::Float64(_))
881            | (Self::Int64, Value::Int64(_))
882            | (Self::Int128, Value::Int128(_))
883            | (Self::Nat64, Value::Nat64(_))
884            | (Self::Principal, Value::Principal(_))
885            | (Self::Subaccount, Value::Subaccount(_))
886            | (Self::Text { .. }, Value::Text(_))
887            | (Self::Timestamp, Value::Timestamp(_))
888            | (Self::Nat128, Value::Nat128(_))
889            | (Self::Ulid, Value::Ulid(_))
890            | (Self::Unit, Value::Unit) => true,
891            (Self::Int8, Value::Int64(value)) => i8::try_from(*value).is_ok(),
892            (Self::Int16, Value::Int64(value)) => i16::try_from(*value).is_ok(),
893            (Self::Int32, Value::Int64(value)) => i32::try_from(*value).is_ok(),
894            (Self::Nat8, Value::Nat64(value)) => u8::try_from(*value).is_ok(),
895            (Self::Nat16, Value::Nat64(value)) => u16::try_from(*value).is_ok(),
896            (Self::Nat32, Value::Nat64(value)) => u32::try_from(*value).is_ok(),
897            (Self::IntBig { max_bytes }, Value::IntBig(value)) => {
898                value.to_leb128().len() <= *max_bytes as usize
899            }
900            (Self::NatBig { max_bytes }, Value::NatBig(value)) => {
901                value.to_leb128().len() <= *max_bytes as usize
902            }
903            (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
904            (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
905                items.iter().all(|item| inner.accepts_value(item))
906            }
907            (Self::Map { key, value }, Value::Map(entries)) => {
908                Value::validate_map_entries(entries.as_slice()).is_ok()
909                    && entries.iter().all(|(entry_key, entry_value)| {
910                        key.accepts_value(entry_key) && value.accepts_value(entry_value)
911                    })
912            }
913            (Self::Composite { shape, .. }, value) => shape.accepts_value(value),
914            _ => false,
915        }
916    }
917}
918
919#[cfg(test)]
920impl CompositeShapeModel {
921    fn accepts_value(self, value: &Value) -> bool {
922        match (self, value) {
923            (Self::Record(fields), Value::Map(entries)) => {
924                entries.len() == fields.len()
925                    && entries.iter().zip(fields).all(|((key, value), field)| {
926                        matches!(key, Value::Text(name) if name == field.name())
927                            && accepts_nullable_value(field.kind(), field.nullable(), value)
928                    })
929            }
930            (Self::Tuple(elements), Value::List(values)) => {
931                values.len() == elements.len()
932                    && values.iter().zip(elements).all(|(value, element)| {
933                        accepts_nullable_value(element.kind(), element.nullable(), value)
934                    })
935            }
936            (Self::Newtype(inner), value) => {
937                accepts_nullable_value(inner.kind(), inner.nullable(), value)
938            }
939            _ => false,
940        }
941    }
942}
943
944#[cfg(test)]
945fn accepts_nullable_value(kind: FieldKind, nullable: bool, value: &Value) -> bool {
946    matches!(value, Value::Null) && nullable || kind.accepts_value(value)
947}
948
949// `FieldStorageDecode::ByKind` follows the same literal compatibility rule as
950// the schema predicate layer without routing the storage model through
951// `db::schema`.
952#[cfg(test)]
953fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
954    match (kind, value) {
955        (FieldKind::Relation { key_kind, .. }, value) => {
956            by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
957        }
958        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
959            .iter()
960            .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
961        (
962            FieldKind::Map {
963                key,
964                value: value_kind,
965            },
966            Value::Map(entries),
967        ) => {
968            if Value::validate_map_entries(entries.as_slice()).is_err() {
969                return false;
970            }
971
972            entries.iter().all(|(entry_key, entry_value)| {
973                by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
974                    && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
975            })
976        }
977        _ => kind.accepts_value(value),
978    }
979}
980
981// `FieldStorageDecode::CatalogValue` fields use the canonical recursive value envelope
982// while exact `FieldKind` metadata still owns every admitted shape.
983#[cfg(test)]
984fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
985    match (kind, value) {
986        (FieldKind::Relation { key_kind, .. }, value) => {
987            value_storage_kind_accepts_runtime_value(*key_kind, value)
988        }
989        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
990            .iter()
991            .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
992        (
993            FieldKind::Map {
994                key,
995                value: value_kind,
996            },
997            Value::Map(entries),
998        ) => {
999            if Value::validate_map_entries(entries.as_slice()).is_err() {
1000                return false;
1001            }
1002
1003            entries.iter().all(|(entry_key, entry_value)| {
1004                value_storage_kind_accepts_runtime_value(*key, entry_key)
1005                    && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
1006            })
1007        }
1008        _ => kind.accepts_value(value),
1009    }
1010}
1011
1012// Enforce fixed decimal scales through nested collection/map shapes before a
1013// field-level runtime value is persisted.
1014#[cfg(test)]
1015fn ensure_decimal_scale_matches(
1016    kind: FieldKind,
1017    value: &Value,
1018) -> Result<(), FieldStorageValidationError> {
1019    if matches!(value, Value::Null) {
1020        return Ok(());
1021    }
1022
1023    match (kind, value) {
1024        (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
1025            if decimal.scale() != scale {
1026                return Err(FieldStorageValidationError::DecimalScaleMismatch);
1027            }
1028
1029            Ok(())
1030        }
1031        (FieldKind::Relation { key_kind, .. }, value) => {
1032            ensure_decimal_scale_matches(*key_kind, value)
1033        }
1034        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
1035            for item in items {
1036                ensure_decimal_scale_matches(*inner, item)?;
1037            }
1038
1039            Ok(())
1040        }
1041        (
1042            FieldKind::Map {
1043                key,
1044                value: map_value,
1045            },
1046            Value::Map(entries),
1047        ) => {
1048            for (entry_key, entry_value) in entries {
1049                ensure_decimal_scale_matches(*key, entry_key)?;
1050                ensure_decimal_scale_matches(*map_value, entry_value)?;
1051            }
1052
1053            Ok(())
1054        }
1055        _ => Ok(()),
1056    }
1057}
1058
1059// Enforce bounded text/blob length through nested collection/map shapes before
1060// a field-level runtime value is persisted.
1061#[cfg(test)]
1062fn ensure_scalar_max_len_matches(
1063    kind: FieldKind,
1064    value: &Value,
1065) -> Result<(), FieldStorageValidationError> {
1066    if matches!(value, Value::Null) {
1067        return Ok(());
1068    }
1069
1070    match (kind, value) {
1071        (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
1072            let len = text.chars().count();
1073            if len > max as usize {
1074                return Err(FieldStorageValidationError::ScalarMaxLen);
1075            }
1076
1077            Ok(())
1078        }
1079        (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
1080            let len = bytes.len();
1081            if len > max as usize {
1082                return Err(FieldStorageValidationError::ScalarMaxLen);
1083            }
1084
1085            Ok(())
1086        }
1087        (FieldKind::Relation { key_kind, .. }, value) => {
1088            ensure_scalar_max_len_matches(*key_kind, value)
1089        }
1090        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
1091            for item in items {
1092                ensure_scalar_max_len_matches(*inner, item)?;
1093            }
1094
1095            Ok(())
1096        }
1097        (
1098            FieldKind::Map {
1099                key,
1100                value: map_value,
1101            },
1102            Value::Map(entries),
1103        ) => {
1104            for (entry_key, entry_value) in entries {
1105                ensure_scalar_max_len_matches(*key, entry_key)?;
1106                ensure_scalar_max_len_matches(*map_value, entry_value)?;
1107            }
1108
1109            Ok(())
1110        }
1111        _ => Ok(()),
1112    }
1113}
1114
1115// Enforce the canonical persisted ordering rules for set/map shapes before one
1116// field-level runtime value becomes row bytes.
1117#[cfg(test)]
1118fn ensure_value_is_deterministic_for_storage(
1119    kind: FieldKind,
1120    value: &Value,
1121) -> Result<(), FieldStorageValidationError> {
1122    match (kind, value) {
1123        (FieldKind::Set(_), Value::List(items)) => {
1124            for pair in items.windows(2) {
1125                let [left, right] = pair else {
1126                    continue;
1127                };
1128                if Value::canonical_cmp(left, right) != Ordering::Less {
1129                    return Err(FieldStorageValidationError::SetCanonicalOrder);
1130                }
1131            }
1132
1133            Ok(())
1134        }
1135        (FieldKind::Map { .. }, Value::Map(entries)) => {
1136            Value::validate_map_entries(entries.as_slice())
1137                .map_err(|_| FieldStorageValidationError::MapEntryContract)?;
1138
1139            if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1140                return Err(FieldStorageValidationError::MapCanonicalOrder);
1141            }
1142
1143            Ok(())
1144        }
1145        _ => Ok(()),
1146    }
1147}
1148
1149///
1150/// TESTS
1151///
1152
1153#[cfg(test)]
1154mod tests {
1155    use crate::{
1156        model::field::{FieldKind, FieldModel},
1157        value::Value,
1158    };
1159
1160    static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1161    static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
1162
1163    #[test]
1164    fn text_max_len_accepts_unbounded_text() {
1165        let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1166
1167        assert!(
1168            field
1169                .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1170                .is_ok()
1171        );
1172    }
1173
1174    #[test]
1175    fn text_max_len_counts_unicode_scalars_not_bytes() {
1176        let field = FieldModel::generated("name", BOUNDED_TEXT);
1177
1178        assert!(
1179            field
1180                .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1181                .is_ok()
1182        );
1183        assert!(
1184            field
1185                .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1186                .is_err()
1187        );
1188    }
1189
1190    #[test]
1191    fn text_max_len_recurses_through_collections() {
1192        static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1193        static TEXT_MAP: FieldKind = FieldKind::Map {
1194            key: &BOUNDED_TEXT,
1195            value: &BOUNDED_TEXT,
1196        };
1197
1198        let list_field = FieldModel::generated("names", TEXT_LIST);
1199        let map_field = FieldModel::generated("labels", TEXT_MAP);
1200
1201        assert!(
1202            list_field
1203                .validate_runtime_value_for_storage(&Value::List(vec![
1204                    Value::Text("Ada".into()),
1205                    Value::Text("Bob".into()),
1206                ]))
1207                .is_ok()
1208        );
1209        assert!(
1210            list_field
1211                .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1212                .is_err()
1213        );
1214        assert!(
1215            map_field
1216                .validate_runtime_value_for_storage(&Value::Map(vec![(
1217                    Value::Text("key".into()),
1218                    Value::Text("val".into()),
1219                )]))
1220                .is_ok()
1221        );
1222        assert!(
1223            map_field
1224                .validate_runtime_value_for_storage(&Value::Map(vec![(
1225                    Value::Text("long".into()),
1226                    Value::Text("val".into()),
1227                )]))
1228                .is_err()
1229        );
1230    }
1231
1232    #[test]
1233    fn blob_max_len_counts_bytes() {
1234        let field = FieldModel::generated("payload", BOUNDED_BLOB);
1235
1236        assert!(
1237            field
1238                .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1239                .is_ok()
1240        );
1241        assert!(
1242            field
1243                .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1244                .is_err()
1245        );
1246    }
1247
1248    #[test]
1249    fn blob_max_len_recurses_through_collections() {
1250        static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1251
1252        let field = FieldModel::generated("payloads", BLOB_LIST);
1253
1254        assert!(
1255            field
1256                .validate_runtime_value_for_storage(&Value::List(vec![
1257                    Value::Blob(vec![1, 2, 3]),
1258                    Value::Blob(vec![4, 5]),
1259                ]))
1260                .is_ok()
1261        );
1262        assert!(
1263            field
1264                .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1265                    1, 2, 3, 4
1266                ])]))
1267                .is_err()
1268        );
1269    }
1270}