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
6use crate::{
7    traits::RuntimeValueKind,
8    types::{Decimal, EntityTag},
9    value::Value,
10};
11use std::{borrow::Cow, cmp::Ordering};
12
13///
14/// FieldStorageDecode
15///
16/// FieldStorageDecode captures how one persisted field payload must be
17/// interpreted at structural decode boundaries.
18/// Semantic `FieldKind` alone is not always authoritative for persisted decode:
19/// some fields intentionally store raw `Value` payloads even when their planner
20/// shape is narrower.
21///
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum FieldStorageDecode {
25    /// Decode the persisted field payload according to semantic `FieldKind`.
26    ByKind,
27    /// Decode the persisted field payload directly into `Value`.
28    Value,
29}
30
31///
32/// ScalarCodec
33///
34/// ScalarCodec identifies the canonical binary leaf encoding used for one
35/// scalar persisted field payload.
36/// These codecs are fixed-width or span-bounded by the surrounding row slot
37/// container; they do not perform map/array/value dispatch.
38///
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum ScalarCodec {
42    Blob,
43    Bool,
44    Date,
45    Duration,
46    Float32,
47    Float64,
48    Int64,
49    Principal,
50    Subaccount,
51    Text,
52    Timestamp,
53    Uint64,
54    Ulid,
55    Unit,
56}
57
58///
59/// LeafCodec
60///
61/// LeafCodec declares whether one persisted field payload uses a dedicated
62/// scalar codec or falls back to structural leaf decoding.
63/// The row container consults this metadata before deciding whether a slot can
64/// stay on the scalar fast path.
65///
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum LeafCodec {
69    Scalar(ScalarCodec),
70    StructuralFallback,
71}
72
73///
74/// EnumVariantModel
75///
76/// EnumVariantModel carries structural decode metadata for one generated enum
77/// variant payload.
78/// Runtime structural decode uses this to stay on the field-kind contract for
79/// enum payloads instead of falling back to generic untyped structural decode.
80///
81
82#[derive(Clone, Copy, Debug)]
83pub struct EnumVariantModel {
84    /// Stable schema variant tag.
85    pub(crate) ident: &'static str,
86    /// Declared payload kind when this variant carries data.
87    pub(crate) payload_kind: Option<&'static FieldKind>,
88    /// Persisted payload decode contract for the carried data.
89    pub(crate) payload_storage_decode: FieldStorageDecode,
90}
91
92impl EnumVariantModel {
93    /// Build one enum variant structural decode descriptor.
94    #[must_use]
95    pub const fn new(
96        ident: &'static str,
97        payload_kind: Option<&'static FieldKind>,
98        payload_storage_decode: FieldStorageDecode,
99    ) -> Self {
100        Self {
101            ident,
102            payload_kind,
103            payload_storage_decode,
104        }
105    }
106
107    /// Return the stable schema variant tag.
108    #[must_use]
109    pub const fn ident(&self) -> &'static str {
110        self.ident
111    }
112
113    /// Return the declared payload kind when this variant carries data.
114    #[must_use]
115    pub const fn payload_kind(&self) -> Option<&'static FieldKind> {
116        self.payload_kind
117    }
118
119    /// Return the persisted payload decode contract for this variant.
120    #[must_use]
121    pub const fn payload_storage_decode(&self) -> FieldStorageDecode {
122        self.payload_storage_decode
123    }
124}
125
126///
127/// FieldModel
128///
129/// Runtime field metadata surfaced by macro-generated `EntityModel` values.
130///
131/// This is the smallest unit consumed by predicate validation, planning,
132/// and executor-side plan checks.
133///
134
135#[derive(Debug)]
136pub struct FieldModel {
137    /// Field name as used in predicates and indexing.
138    pub(crate) name: &'static str,
139    /// Runtime type shape (no schema-layer graph nodes).
140    pub(crate) kind: FieldKind,
141    /// Known nested fields when this field stores a generated structured record.
142    pub(crate) nested_fields: &'static [Self],
143    /// Whether the field may persist an explicit `NULL` payload.
144    pub(crate) nullable: bool,
145    /// Persisted field decode contract used by structural runtime decoders.
146    pub(crate) storage_decode: FieldStorageDecode,
147    /// Leaf payload codec used by slot readers and writers.
148    pub(crate) leaf_codec: LeafCodec,
149    /// Insert-time generation contract admitted on reduced SQL write lanes.
150    pub(crate) insert_generation: Option<FieldInsertGeneration>,
151    /// Auto-managed write contract emitted for derive-owned system fields.
152    pub(crate) write_management: Option<FieldWriteManagement>,
153    /// Database-level default contract used by persisted schema authority.
154    pub(crate) database_default: FieldDatabaseDefault,
155}
156
157///
158/// FieldDatabaseDefault
159///
160/// FieldDatabaseDefault declares the database-level default contract for one
161/// runtime field. It is intentionally separate from Rust `Default` so schema
162/// reconciliation never treats construction defaults as persisted-row policy.
163///
164
165#[derive(Clone, Copy, Debug, Eq, PartialEq)]
166pub enum FieldDatabaseDefault {
167    /// No database-level default is declared for this field.
168    None,
169}
170
171///
172/// FieldInsertGeneration
173///
174/// FieldInsertGeneration declares whether one runtime field may be synthesized
175/// by the reduced SQL insert boundary when the user omits that field.
176/// This stays separate from typed-Rust `Default` behavior so write-time
177/// generation remains an explicit schema contract.
178///
179
180#[derive(Clone, Copy, Debug, Eq, PartialEq)]
181pub enum FieldInsertGeneration {
182    /// Generate one fresh `Ulid` value at insert time.
183    Ulid,
184    /// Generate one current wall-clock `Timestamp` value at insert time.
185    Timestamp,
186}
187
188///
189/// FieldWriteManagement
190///
191/// FieldWriteManagement declares whether one runtime field is owned by the
192/// write boundary during insert or update synthesis.
193/// This keeps auto-managed system fields explicit in schema/runtime metadata
194/// instead of relying on literal field names in write paths.
195///
196
197#[derive(Clone, Copy, Debug, Eq, PartialEq)]
198pub enum FieldWriteManagement {
199    /// Fill only on insert when the row is first created.
200    CreatedAt,
201    /// Refresh on insert and every update.
202    UpdatedAt,
203}
204
205impl FieldModel {
206    /// Build one generated runtime field descriptor.
207    ///
208    /// This constructor exists for derive/codegen output and trusted test
209    /// fixtures. Runtime planning and execution treat `FieldModel` values as
210    /// build-time-validated metadata.
211    #[must_use]
212    #[doc(hidden)]
213    pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
214        Self::generated_with_storage_decode_and_nullability(
215            name,
216            kind,
217            FieldStorageDecode::ByKind,
218            false,
219        )
220    }
221
222    /// Build one runtime field descriptor with an explicit persisted decode contract.
223    #[must_use]
224    #[doc(hidden)]
225    pub const fn generated_with_storage_decode(
226        name: &'static str,
227        kind: FieldKind,
228        storage_decode: FieldStorageDecode,
229    ) -> Self {
230        Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
231    }
232
233    /// Build one runtime field descriptor with an explicit decode contract and nullability.
234    #[must_use]
235    #[doc(hidden)]
236    pub const fn generated_with_storage_decode_and_nullability(
237        name: &'static str,
238        kind: FieldKind,
239        storage_decode: FieldStorageDecode,
240        nullable: bool,
241    ) -> Self {
242        Self::generated_with_storage_decode_nullability_and_write_policies(
243            name,
244            kind,
245            storage_decode,
246            nullable,
247            None,
248            None,
249        )
250    }
251
252    /// Build one runtime field descriptor with an explicit decode contract, nullability,
253    /// and insert-time generation contract.
254    #[must_use]
255    #[doc(hidden)]
256    pub const fn generated_with_storage_decode_nullability_and_insert_generation(
257        name: &'static str,
258        kind: FieldKind,
259        storage_decode: FieldStorageDecode,
260        nullable: bool,
261        insert_generation: Option<FieldInsertGeneration>,
262    ) -> Self {
263        Self::generated_with_storage_decode_nullability_and_write_policies(
264            name,
265            kind,
266            storage_decode,
267            nullable,
268            insert_generation,
269            None,
270        )
271    }
272
273    /// Build one runtime field descriptor with explicit insert-generation and
274    /// write-management policies.
275    #[must_use]
276    #[doc(hidden)]
277    pub const fn generated_with_storage_decode_nullability_and_write_policies(
278        name: &'static str,
279        kind: FieldKind,
280        storage_decode: FieldStorageDecode,
281        nullable: bool,
282        insert_generation: Option<FieldInsertGeneration>,
283        write_management: Option<FieldWriteManagement>,
284    ) -> Self {
285        Self {
286            name,
287            kind,
288            nested_fields: &[],
289            nullable,
290            storage_decode,
291            leaf_codec: leaf_codec_for(kind, storage_decode),
292            insert_generation,
293            write_management,
294            database_default: FieldDatabaseDefault::None,
295        }
296    }
297
298    /// Build one runtime field descriptor with nested generated-record field metadata.
299    #[must_use]
300    #[doc(hidden)]
301    pub const fn generated_with_storage_decode_nullability_write_policies_and_nested_fields(
302        name: &'static str,
303        kind: FieldKind,
304        storage_decode: FieldStorageDecode,
305        nullable: bool,
306        insert_generation: Option<FieldInsertGeneration>,
307        write_management: Option<FieldWriteManagement>,
308        nested_fields: &'static [Self],
309    ) -> Self {
310        Self::generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
311            name,
312            kind,
313            storage_decode,
314            nullable,
315            insert_generation,
316            write_management,
317            FieldDatabaseDefault::None,
318            nested_fields,
319        )
320    }
321
322    /// Build one runtime field descriptor with explicit write policies,
323    /// database default metadata, and nested generated-record field metadata.
324    #[must_use]
325    #[doc(hidden)]
326    #[expect(
327        clippy::too_many_arguments,
328        reason = "generated schema metadata keeps every field contract explicit"
329    )]
330    pub const fn generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
331        name: &'static str,
332        kind: FieldKind,
333        storage_decode: FieldStorageDecode,
334        nullable: bool,
335        insert_generation: Option<FieldInsertGeneration>,
336        write_management: Option<FieldWriteManagement>,
337        database_default: FieldDatabaseDefault,
338        nested_fields: &'static [Self],
339    ) -> Self {
340        Self {
341            name,
342            kind,
343            nested_fields,
344            nullable,
345            storage_decode,
346            leaf_codec: leaf_codec_for(kind, storage_decode),
347            insert_generation,
348            write_management,
349            database_default,
350        }
351    }
352
353    /// Return the stable field name.
354    #[must_use]
355    pub const fn name(&self) -> &'static str {
356        self.name
357    }
358
359    /// Return the runtime type-kind descriptor.
360    #[must_use]
361    pub const fn kind(&self) -> FieldKind {
362        self.kind
363    }
364
365    /// Return known nested fields for generated structured records.
366    #[must_use]
367    pub const fn nested_fields(&self) -> &'static [Self] {
368        self.nested_fields
369    }
370
371    /// Return whether the persisted field contract permits explicit `NULL`.
372    #[must_use]
373    pub const fn nullable(&self) -> bool {
374        self.nullable
375    }
376
377    /// Return the persisted field decode contract.
378    #[must_use]
379    pub const fn storage_decode(&self) -> FieldStorageDecode {
380        self.storage_decode
381    }
382
383    /// Return the persisted leaf payload codec.
384    #[must_use]
385    pub const fn leaf_codec(&self) -> LeafCodec {
386        self.leaf_codec
387    }
388
389    /// Return the reduced-SQL insert-time generation contract for this field.
390    #[must_use]
391    pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
392        self.insert_generation
393    }
394
395    /// Return the write-boundary management contract for this field.
396    #[must_use]
397    pub const fn write_management(&self) -> Option<FieldWriteManagement> {
398        self.write_management
399    }
400
401    /// Return the database-level default contract for this field.
402    #[must_use]
403    pub const fn database_default(&self) -> FieldDatabaseDefault {
404        self.database_default
405    }
406
407    /// Validate one runtime value against this field's persisted storage contract.
408    ///
409    /// This is the model-owned compatibility gate used before row bytes are
410    /// emitted. It intentionally checks storage compatibility, not query
411    /// predicate compatibility, so `FieldStorageDecode::Value` can accept
412    /// open-ended structured payloads while still enforcing outer collection
413    /// shape, decimal scale, and deterministic set/map ordering.
414    pub(crate) fn validate_runtime_value_for_storage(&self, value: &Value) -> Result<(), String> {
415        if matches!(value, Value::Null) {
416            if self.nullable() {
417                return Ok(());
418            }
419
420            return Err("required field cannot store null".into());
421        }
422
423        let accepts = match self.storage_decode() {
424            FieldStorageDecode::Value => {
425                value_storage_kind_accepts_runtime_value(self.kind(), value)
426            }
427            FieldStorageDecode::ByKind => {
428                by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
429            }
430        };
431        if !accepts {
432            return Err(format!(
433                "field kind {:?} does not accept runtime value {value:?}",
434                self.kind()
435            ));
436        }
437
438        ensure_decimal_scale_matches(self.kind(), value)?;
439        ensure_text_max_len_matches(self.kind(), value)?;
440        ensure_value_is_deterministic_for_storage(self.kind(), value)
441    }
442
443    // Normalize decimal payloads to this field's fixed scale before storage
444    // encoding. Validation still runs after this step, so malformed shapes and
445    // deterministic collection rules remain owned by the normal field contract.
446    pub(crate) fn normalize_runtime_value_for_storage<'a>(
447        &self,
448        value: &'a Value,
449    ) -> Result<Cow<'a, Value>, String> {
450        normalize_decimal_scale_for_storage(self.kind(), value)
451    }
452}
453
454// Resolve the canonical leaf codec from semantic field kind plus storage
455// contract. Fields that intentionally persist as `Value` or that still require
456// recursive payload decoding remain on the shared structural fallback.
457const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
458    if matches!(storage_decode, FieldStorageDecode::Value) {
459        return LeafCodec::StructuralFallback;
460    }
461
462    match kind {
463        FieldKind::Blob => LeafCodec::Scalar(ScalarCodec::Blob),
464        FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
465        FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
466        FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
467        FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
468        FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
469        FieldKind::Int => LeafCodec::Scalar(ScalarCodec::Int64),
470        FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
471        FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
472        FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
473        FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
474        FieldKind::Uint => LeafCodec::Scalar(ScalarCodec::Uint64),
475        FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
476        FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
477        FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
478        FieldKind::Account
479        | FieldKind::Decimal { .. }
480        | FieldKind::Enum { .. }
481        | FieldKind::Int128
482        | FieldKind::IntBig
483        | FieldKind::List(_)
484        | FieldKind::Map { .. }
485        | FieldKind::Set(_)
486        | FieldKind::Structured { .. }
487        | FieldKind::Uint128
488        | FieldKind::UintBig => LeafCodec::StructuralFallback,
489    }
490}
491
492///
493/// RelationStrength
494///
495/// Explicit relation intent for save-time referential integrity.
496///
497
498#[derive(Clone, Copy, Debug, Eq, PartialEq)]
499pub enum RelationStrength {
500    Strong,
501    Weak,
502}
503
504///
505/// FieldKind
506///
507/// Minimal runtime type surface needed by planning, validation, and execution.
508///
509/// This is aligned with `Value` variants and intentionally lossy: it encodes
510/// only the shape required for predicate compatibility and index planning.
511///
512
513#[derive(Clone, Copy, Debug)]
514pub enum FieldKind {
515    // Scalar primitives
516    Account,
517    Blob,
518    Bool,
519    Date,
520    Decimal {
521        /// Required schema-declared fractional scale for decimal fields.
522        scale: u32,
523    },
524    Duration,
525    Enum {
526        /// Fully-qualified enum type path used for strict filter normalization.
527        path: &'static str,
528        /// Declared per-variant payload decode metadata.
529        variants: &'static [EnumVariantModel],
530    },
531    Float32,
532    Float64,
533    Int,
534    Int128,
535    IntBig,
536    Principal,
537    Subaccount,
538    Text {
539        /// Optional schema-declared maximum Unicode scalar count for text fields.
540        max_len: Option<u32>,
541    },
542    Timestamp,
543    Uint,
544    Uint128,
545    UintBig,
546    Ulid,
547    Unit,
548
549    /// Typed relation; `key_kind` reflects the referenced key type.
550    /// `strength` encodes strong vs. weak relation intent.
551    Relation {
552        /// Fully-qualified Rust type path for diagnostics.
553        target_path: &'static str,
554        /// Stable external name used in storage keys.
555        target_entity_name: &'static str,
556        /// Stable runtime identity used on hot execution paths.
557        target_entity_tag: EntityTag,
558        /// Data store path where the target entity is persisted.
559        target_store_path: &'static str,
560        key_kind: &'static Self,
561        strength: RelationStrength,
562    },
563
564    // Collections
565    List(&'static Self),
566    Set(&'static Self),
567    /// Deterministic, unordered key/value collection.
568    ///
569    /// Map fields are persistable and patchable, but not queryable or indexable.
570    Map {
571        key: &'static Self,
572        value: &'static Self,
573    },
574
575    /// Structured (non-atomic) value.
576    /// Queryability here controls whether predicates may target this field,
577    /// not whether it may be stored or updated.
578    Structured {
579        queryable: bool,
580    },
581}
582
583impl FieldKind {
584    #[must_use]
585    pub const fn value_kind(&self) -> RuntimeValueKind {
586        match self {
587            Self::Account
588            | Self::Blob
589            | Self::Bool
590            | Self::Date
591            | Self::Duration
592            | Self::Enum { .. }
593            | Self::Float32
594            | Self::Float64
595            | Self::Int
596            | Self::Int128
597            | Self::IntBig
598            | Self::Principal
599            | Self::Subaccount
600            | Self::Text { .. }
601            | Self::Timestamp
602            | Self::Uint
603            | Self::Uint128
604            | Self::UintBig
605            | Self::Ulid
606            | Self::Unit
607            | Self::Decimal { .. }
608            | Self::Relation { .. } => RuntimeValueKind::Atomic,
609            Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
610            Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
611            Self::Structured { queryable } => RuntimeValueKind::Structured {
612                queryable: *queryable,
613            },
614        }
615    }
616
617    /// Returns `true` if this field shape is permitted in
618    /// persisted or query-visible schemas under the current
619    /// determinism policy.
620    ///
621    /// This shape-level check is structural only; query-time policy
622    /// enforcement (for example, map predicate fencing) is applied at
623    /// query construction and validation boundaries.
624    #[must_use]
625    pub const fn is_deterministic_collection_shape(&self) -> bool {
626        match self {
627            Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
628
629            Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
630
631            Self::Map { key, value } => {
632                key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
633            }
634
635            _ => true,
636        }
637    }
638
639    /// Return true when this planner-frozen grouped field kind can stay on the
640    /// borrowed grouped-key probe path without owned canonical materialization.
641    #[must_use]
642    pub(crate) fn supports_group_probe(&self) -> bool {
643        match self {
644            Self::Enum { variants, .. } => variants.iter().all(|variant| {
645                variant
646                    .payload_kind()
647                    .is_none_or(Self::supports_group_probe)
648            }),
649            Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
650            Self::List(_)
651            | Self::Set(_)
652            | Self::Map { .. }
653            | Self::Structured { .. }
654            | Self::Unit => false,
655            Self::Account
656            | Self::Blob
657            | Self::Bool
658            | Self::Date
659            | Self::Decimal { .. }
660            | Self::Duration
661            | Self::Float32
662            | Self::Float64
663            | Self::Int
664            | Self::Int128
665            | Self::IntBig
666            | Self::Principal
667            | Self::Subaccount
668            | Self::Text { .. }
669            | Self::Timestamp
670            | Self::Uint
671            | Self::Uint128
672            | Self::UintBig
673            | Self::Ulid => true,
674        }
675    }
676
677    /// Match one runtime value against this field kind contract.
678    ///
679    /// This is the shared recursive field-kind acceptance boundary used by
680    /// persisted-row encoding, mutation-save validation, and aggregate field
681    /// extraction.
682    #[must_use]
683    pub(crate) fn accepts_value(&self, value: &Value) -> bool {
684        match (self, value) {
685            (Self::Account, Value::Account(_))
686            | (Self::Blob, Value::Blob(_))
687            | (Self::Bool, Value::Bool(_))
688            | (Self::Date, Value::Date(_))
689            | (Self::Decimal { .. }, Value::Decimal(_))
690            | (Self::Duration, Value::Duration(_))
691            | (Self::Enum { .. }, Value::Enum(_))
692            | (Self::Float32, Value::Float32(_))
693            | (Self::Float64, Value::Float64(_))
694            | (Self::Int, Value::Int(_))
695            | (Self::Int128, Value::Int128(_))
696            | (Self::IntBig, Value::IntBig(_))
697            | (Self::Principal, Value::Principal(_))
698            | (Self::Subaccount, Value::Subaccount(_))
699            | (Self::Text { .. }, Value::Text(_))
700            | (Self::Timestamp, Value::Timestamp(_))
701            | (Self::Uint, Value::Uint(_))
702            | (Self::Uint128, Value::Uint128(_))
703            | (Self::UintBig, Value::UintBig(_))
704            | (Self::Ulid, Value::Ulid(_))
705            | (Self::Unit, Value::Unit)
706            | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
707            (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
708            (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
709                items.iter().all(|item| inner.accepts_value(item))
710            }
711            (Self::Map { key, value }, Value::Map(entries)) => {
712                if Value::validate_map_entries(entries.as_slice()).is_err() {
713                    return false;
714                }
715
716                entries.iter().all(|(entry_key, entry_value)| {
717                    key.accepts_value(entry_key) && value.accepts_value(entry_value)
718                })
719            }
720            _ => false,
721        }
722    }
723}
724
725// `FieldStorageDecode::ByKind` follows the same literal compatibility rule as
726// the schema predicate layer without routing the storage model through
727// `db::schema`. Structured field kinds are intentionally not accepted here;
728// fields that persist open-ended structured payloads use
729// `FieldStorageDecode::Value` instead.
730fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
731    match (kind, value) {
732        (FieldKind::Relation { key_kind, .. }, value) => {
733            by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
734        }
735        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
736            .iter()
737            .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
738        (
739            FieldKind::Map {
740                key,
741                value: value_kind,
742            },
743            Value::Map(entries),
744        ) => {
745            if Value::validate_map_entries(entries.as_slice()).is_err() {
746                return false;
747            }
748
749            entries.iter().all(|(entry_key, entry_value)| {
750                by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
751                    && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
752            })
753        }
754        (FieldKind::Structured { .. }, _) => false,
755        _ => kind.accepts_value(value),
756    }
757}
758
759// `FieldStorageDecode::Value` fields persist an opaque runtime `Value` envelope,
760// so `FieldKind::Structured` must stay open-ended while outer collection/map
761// shapes still enforce the recursive structure the model owns.
762fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
763    match (kind, value) {
764        (FieldKind::Structured { .. }, _) => true,
765        (FieldKind::Relation { key_kind, .. }, value) => {
766            value_storage_kind_accepts_runtime_value(*key_kind, value)
767        }
768        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
769            .iter()
770            .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
771        (
772            FieldKind::Map {
773                key,
774                value: value_kind,
775            },
776            Value::Map(entries),
777        ) => {
778            if Value::validate_map_entries(entries.as_slice()).is_err() {
779                return false;
780            }
781
782            entries.iter().all(|(entry_key, entry_value)| {
783                value_storage_kind_accepts_runtime_value(*key, entry_key)
784                    && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
785            })
786        }
787        _ => kind.accepts_value(value),
788    }
789}
790
791// Enforce fixed decimal scales through nested collection/map shapes before a
792// field-level runtime value is persisted.
793fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
794    if matches!(value, Value::Null) {
795        return Ok(());
796    }
797
798    match (kind, value) {
799        (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
800            if decimal.scale() != scale {
801                return Err(format!(
802                    "decimal scale mismatch: expected {scale}, found {}",
803                    decimal.scale()
804                ));
805            }
806
807            Ok(())
808        }
809        (FieldKind::Relation { key_kind, .. }, value) => {
810            ensure_decimal_scale_matches(*key_kind, value)
811        }
812        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
813            for item in items {
814                ensure_decimal_scale_matches(*inner, item)?;
815            }
816
817            Ok(())
818        }
819        (
820            FieldKind::Map {
821                key,
822                value: map_value,
823            },
824            Value::Map(entries),
825        ) => {
826            for (entry_key, entry_value) in entries {
827                ensure_decimal_scale_matches(*key, entry_key)?;
828                ensure_decimal_scale_matches(*map_value, entry_value)?;
829            }
830
831            Ok(())
832        }
833        _ => Ok(()),
834    }
835}
836
837// Normalize fixed-scale decimal values through nested collection/map shapes
838// before the field-level payload is encoded. This is write-side canonicalization;
839// callers that validate already persisted bytes still use the exact scale check.
840fn normalize_decimal_scale_for_storage(
841    kind: FieldKind,
842    value: &Value,
843) -> Result<Cow<'_, Value>, String> {
844    if matches!(value, Value::Null) {
845        return Ok(Cow::Borrowed(value));
846    }
847
848    match (kind, value) {
849        (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
850            let normalized = decimal_with_storage_scale(*decimal, scale).ok_or_else(|| {
851                format!(
852                    "decimal scale mismatch: expected {scale}, found {}",
853                    decimal.scale()
854                )
855            })?;
856
857            if normalized.scale() == decimal.scale() {
858                Ok(Cow::Borrowed(value))
859            } else {
860                Ok(Cow::Owned(Value::Decimal(normalized)))
861            }
862        }
863        (FieldKind::Relation { key_kind, .. }, value) => {
864            normalize_decimal_scale_for_storage(*key_kind, value)
865        }
866        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
867            normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
868                items.map_or_else(
869                    || Cow::Borrowed(value),
870                    |items| Cow::Owned(Value::List(items)),
871                )
872            })
873        }
874        (
875            FieldKind::Map {
876                key,
877                value: map_value,
878            },
879            Value::Map(entries),
880        ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
881            entries.map_or_else(
882                || Cow::Borrowed(value),
883                |entries| Cow::Owned(Value::Map(entries)),
884            )
885        }),
886        _ => Ok(Cow::Borrowed(value)),
887    }
888}
889
890// Convert one decimal into the exact storage scale. Lower-scale values are
891// padded without changing their numeric value; higher-scale values use the same
892// round-half-away-from-zero policy as SQL/fluent decimal rounding.
893fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
894    match decimal.scale().cmp(&scale) {
895        Ordering::Equal => Some(decimal),
896        Ordering::Less => decimal
897            .scale_to_integer(scale)
898            .map(|mantissa| Decimal::from_i128_with_scale(mantissa, scale)),
899        Ordering::Greater => Some(decimal.round_dp(scale)),
900    }
901}
902
903// Normalize decimal items while preserving the original list allocation when
904// every item is already canonical for its nested field kind.
905fn normalize_decimal_list_items(
906    kind: FieldKind,
907    items: &[Value],
908) -> Result<Option<Vec<Value>>, String> {
909    let mut normalized_items = None;
910
911    for (index, item) in items.iter().enumerate() {
912        let normalized = normalize_decimal_scale_for_storage(kind, item)?;
913        if let Cow::Owned(value) = normalized {
914            let items = normalized_items.get_or_insert_with(|| items.to_vec());
915            items[index] = value;
916        }
917    }
918
919    Ok(normalized_items)
920}
921
922// Normalize decimal keys and values while preserving the original map
923// allocation when every entry is already canonical for its nested field kind.
924fn normalize_decimal_map_entries(
925    key_kind: FieldKind,
926    value_kind: FieldKind,
927    entries: &[(Value, Value)],
928) -> Result<Option<Vec<(Value, Value)>>, String> {
929    let mut normalized_entries = None;
930
931    for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
932        let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
933        let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
934
935        if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
936            let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
937            if let Cow::Owned(value) = normalized_key {
938                entries[index].0 = value;
939            }
940            if let Cow::Owned(value) = normalized_value {
941                entries[index].1 = value;
942            }
943        }
944    }
945
946    Ok(normalized_entries)
947}
948
949// Enforce bounded text length through nested collection/map shapes before a
950// field-level runtime value is persisted.
951fn ensure_text_max_len_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
952    if matches!(value, Value::Null) {
953        return Ok(());
954    }
955
956    match (kind, value) {
957        (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
958            let len = text.chars().count();
959            if len > max as usize {
960                return Err(format!(
961                    "text length exceeds max_len: expected at most {max}, found {len}"
962                ));
963            }
964
965            Ok(())
966        }
967        (FieldKind::Relation { key_kind, .. }, value) => {
968            ensure_text_max_len_matches(*key_kind, value)
969        }
970        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
971            for item in items {
972                ensure_text_max_len_matches(*inner, item)?;
973            }
974
975            Ok(())
976        }
977        (
978            FieldKind::Map {
979                key,
980                value: map_value,
981            },
982            Value::Map(entries),
983        ) => {
984            for (entry_key, entry_value) in entries {
985                ensure_text_max_len_matches(*key, entry_key)?;
986                ensure_text_max_len_matches(*map_value, entry_value)?;
987            }
988
989            Ok(())
990        }
991        _ => Ok(()),
992    }
993}
994
995// Enforce the canonical persisted ordering rules for set/map shapes before one
996// field-level runtime value becomes row bytes.
997fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
998    match (kind, value) {
999        (FieldKind::Set(_), Value::List(items)) => {
1000            for pair in items.windows(2) {
1001                let [left, right] = pair else {
1002                    continue;
1003                };
1004                if Value::canonical_cmp(left, right) != Ordering::Less {
1005                    return Err("set payload must already be canonical and deduplicated".into());
1006                }
1007            }
1008
1009            Ok(())
1010        }
1011        (FieldKind::Map { .. }, Value::Map(entries)) => {
1012            Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
1013
1014            if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1015                return Err("map payload must already be canonical and deduplicated".into());
1016            }
1017
1018            Ok(())
1019        }
1020        _ => Ok(()),
1021    }
1022}
1023
1024///
1025/// TESTS
1026///
1027
1028#[cfg(test)]
1029mod tests {
1030    use crate::{
1031        model::field::{FieldKind, FieldModel},
1032        value::Value,
1033    };
1034
1035    static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1036
1037    #[test]
1038    fn text_max_len_accepts_unbounded_text() {
1039        let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1040
1041        assert!(
1042            field
1043                .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1044                .is_ok()
1045        );
1046    }
1047
1048    #[test]
1049    fn text_max_len_counts_unicode_scalars_not_bytes() {
1050        let field = FieldModel::generated("name", BOUNDED_TEXT);
1051
1052        assert!(
1053            field
1054                .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1055                .is_ok()
1056        );
1057        assert!(
1058            field
1059                .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1060                .is_err()
1061        );
1062    }
1063
1064    #[test]
1065    fn text_max_len_recurses_through_collections() {
1066        static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1067        static TEXT_MAP: FieldKind = FieldKind::Map {
1068            key: &BOUNDED_TEXT,
1069            value: &BOUNDED_TEXT,
1070        };
1071
1072        let list_field = FieldModel::generated("names", TEXT_LIST);
1073        let map_field = FieldModel::generated("labels", TEXT_MAP);
1074
1075        assert!(
1076            list_field
1077                .validate_runtime_value_for_storage(&Value::List(vec![
1078                    Value::Text("Ada".into()),
1079                    Value::Text("Bob".into()),
1080                ]))
1081                .is_ok()
1082        );
1083        assert!(
1084            list_field
1085                .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1086                .is_err()
1087        );
1088        assert!(
1089            map_field
1090                .validate_runtime_value_for_storage(&Value::Map(vec![(
1091                    Value::Text("key".into()),
1092                    Value::Text("val".into()),
1093                )]))
1094                .is_ok()
1095        );
1096        assert!(
1097            map_field
1098                .validate_runtime_value_for_storage(&Value::Map(vec![(
1099                    Value::Text("long".into()),
1100                    Value::Text("val".into()),
1101                )]))
1102                .is_err()
1103        );
1104    }
1105}