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 {
311            name,
312            kind,
313            nested_fields,
314            nullable,
315            storage_decode,
316            leaf_codec: leaf_codec_for(kind, storage_decode),
317            insert_generation,
318            write_management,
319            database_default: FieldDatabaseDefault::None,
320        }
321    }
322
323    /// Return the stable field name.
324    #[must_use]
325    pub const fn name(&self) -> &'static str {
326        self.name
327    }
328
329    /// Return the runtime type-kind descriptor.
330    #[must_use]
331    pub const fn kind(&self) -> FieldKind {
332        self.kind
333    }
334
335    /// Return known nested fields for generated structured records.
336    #[must_use]
337    pub const fn nested_fields(&self) -> &'static [Self] {
338        self.nested_fields
339    }
340
341    /// Return whether the persisted field contract permits explicit `NULL`.
342    #[must_use]
343    pub const fn nullable(&self) -> bool {
344        self.nullable
345    }
346
347    /// Return the persisted field decode contract.
348    #[must_use]
349    pub const fn storage_decode(&self) -> FieldStorageDecode {
350        self.storage_decode
351    }
352
353    /// Return the persisted leaf payload codec.
354    #[must_use]
355    pub const fn leaf_codec(&self) -> LeafCodec {
356        self.leaf_codec
357    }
358
359    /// Return the reduced-SQL insert-time generation contract for this field.
360    #[must_use]
361    pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
362        self.insert_generation
363    }
364
365    /// Return the write-boundary management contract for this field.
366    #[must_use]
367    pub const fn write_management(&self) -> Option<FieldWriteManagement> {
368        self.write_management
369    }
370
371    /// Return the database-level default contract for this field.
372    #[must_use]
373    pub const fn database_default(&self) -> FieldDatabaseDefault {
374        self.database_default
375    }
376
377    /// Validate one runtime value against this field's persisted storage contract.
378    ///
379    /// This is the model-owned compatibility gate used before row bytes are
380    /// emitted. It intentionally checks storage compatibility, not query
381    /// predicate compatibility, so `FieldStorageDecode::Value` can accept
382    /// open-ended structured payloads while still enforcing outer collection
383    /// shape, decimal scale, and deterministic set/map ordering.
384    pub(crate) fn validate_runtime_value_for_storage(&self, value: &Value) -> Result<(), String> {
385        if matches!(value, Value::Null) {
386            if self.nullable() {
387                return Ok(());
388            }
389
390            return Err("required field cannot store null".into());
391        }
392
393        let accepts = match self.storage_decode() {
394            FieldStorageDecode::Value => {
395                value_storage_kind_accepts_runtime_value(self.kind(), value)
396            }
397            FieldStorageDecode::ByKind => {
398                by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
399            }
400        };
401        if !accepts {
402            return Err(format!(
403                "field kind {:?} does not accept runtime value {value:?}",
404                self.kind()
405            ));
406        }
407
408        ensure_decimal_scale_matches(self.kind(), value)?;
409        ensure_text_max_len_matches(self.kind(), value)?;
410        ensure_value_is_deterministic_for_storage(self.kind(), value)
411    }
412
413    // Normalize decimal payloads to this field's fixed scale before storage
414    // encoding. Validation still runs after this step, so malformed shapes and
415    // deterministic collection rules remain owned by the normal field contract.
416    pub(crate) fn normalize_runtime_value_for_storage<'a>(
417        &self,
418        value: &'a Value,
419    ) -> Result<Cow<'a, Value>, String> {
420        normalize_decimal_scale_for_storage(self.kind(), value)
421    }
422}
423
424// Resolve the canonical leaf codec from semantic field kind plus storage
425// contract. Fields that intentionally persist as `Value` or that still require
426// recursive payload decoding remain on the shared structural fallback.
427const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
428    if matches!(storage_decode, FieldStorageDecode::Value) {
429        return LeafCodec::StructuralFallback;
430    }
431
432    match kind {
433        FieldKind::Blob => LeafCodec::Scalar(ScalarCodec::Blob),
434        FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
435        FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
436        FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
437        FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
438        FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
439        FieldKind::Int => LeafCodec::Scalar(ScalarCodec::Int64),
440        FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
441        FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
442        FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
443        FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
444        FieldKind::Uint => LeafCodec::Scalar(ScalarCodec::Uint64),
445        FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
446        FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
447        FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
448        FieldKind::Account
449        | FieldKind::Decimal { .. }
450        | FieldKind::Enum { .. }
451        | FieldKind::Int128
452        | FieldKind::IntBig
453        | FieldKind::List(_)
454        | FieldKind::Map { .. }
455        | FieldKind::Set(_)
456        | FieldKind::Structured { .. }
457        | FieldKind::Uint128
458        | FieldKind::UintBig => LeafCodec::StructuralFallback,
459    }
460}
461
462///
463/// RelationStrength
464///
465/// Explicit relation intent for save-time referential integrity.
466///
467
468#[derive(Clone, Copy, Debug, Eq, PartialEq)]
469pub enum RelationStrength {
470    Strong,
471    Weak,
472}
473
474///
475/// FieldKind
476///
477/// Minimal runtime type surface needed by planning, validation, and execution.
478///
479/// This is aligned with `Value` variants and intentionally lossy: it encodes
480/// only the shape required for predicate compatibility and index planning.
481///
482
483#[derive(Clone, Copy, Debug)]
484pub enum FieldKind {
485    // Scalar primitives
486    Account,
487    Blob,
488    Bool,
489    Date,
490    Decimal {
491        /// Required schema-declared fractional scale for decimal fields.
492        scale: u32,
493    },
494    Duration,
495    Enum {
496        /// Fully-qualified enum type path used for strict filter normalization.
497        path: &'static str,
498        /// Declared per-variant payload decode metadata.
499        variants: &'static [EnumVariantModel],
500    },
501    Float32,
502    Float64,
503    Int,
504    Int128,
505    IntBig,
506    Principal,
507    Subaccount,
508    Text {
509        /// Optional schema-declared maximum Unicode scalar count for text fields.
510        max_len: Option<u32>,
511    },
512    Timestamp,
513    Uint,
514    Uint128,
515    UintBig,
516    Ulid,
517    Unit,
518
519    /// Typed relation; `key_kind` reflects the referenced key type.
520    /// `strength` encodes strong vs. weak relation intent.
521    Relation {
522        /// Fully-qualified Rust type path for diagnostics.
523        target_path: &'static str,
524        /// Stable external name used in storage keys.
525        target_entity_name: &'static str,
526        /// Stable runtime identity used on hot execution paths.
527        target_entity_tag: EntityTag,
528        /// Data store path where the target entity is persisted.
529        target_store_path: &'static str,
530        key_kind: &'static Self,
531        strength: RelationStrength,
532    },
533
534    // Collections
535    List(&'static Self),
536    Set(&'static Self),
537    /// Deterministic, unordered key/value collection.
538    ///
539    /// Map fields are persistable and patchable, but not queryable or indexable.
540    Map {
541        key: &'static Self,
542        value: &'static Self,
543    },
544
545    /// Structured (non-atomic) value.
546    /// Queryability here controls whether predicates may target this field,
547    /// not whether it may be stored or updated.
548    Structured {
549        queryable: bool,
550    },
551}
552
553impl FieldKind {
554    #[must_use]
555    pub const fn value_kind(&self) -> RuntimeValueKind {
556        match self {
557            Self::Account
558            | Self::Blob
559            | Self::Bool
560            | Self::Date
561            | Self::Duration
562            | Self::Enum { .. }
563            | Self::Float32
564            | Self::Float64
565            | Self::Int
566            | Self::Int128
567            | Self::IntBig
568            | Self::Principal
569            | Self::Subaccount
570            | Self::Text { .. }
571            | Self::Timestamp
572            | Self::Uint
573            | Self::Uint128
574            | Self::UintBig
575            | Self::Ulid
576            | Self::Unit
577            | Self::Decimal { .. }
578            | Self::Relation { .. } => RuntimeValueKind::Atomic,
579            Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
580            Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
581            Self::Structured { queryable } => RuntimeValueKind::Structured {
582                queryable: *queryable,
583            },
584        }
585    }
586
587    /// Returns `true` if this field shape is permitted in
588    /// persisted or query-visible schemas under the current
589    /// determinism policy.
590    ///
591    /// This shape-level check is structural only; query-time policy
592    /// enforcement (for example, map predicate fencing) is applied at
593    /// query construction and validation boundaries.
594    #[must_use]
595    pub const fn is_deterministic_collection_shape(&self) -> bool {
596        match self {
597            Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
598
599            Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
600
601            Self::Map { key, value } => {
602                key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
603            }
604
605            _ => true,
606        }
607    }
608
609    /// Return true when this planner-frozen grouped field kind can stay on the
610    /// borrowed grouped-key probe path without owned canonical materialization.
611    #[must_use]
612    pub(crate) fn supports_group_probe(&self) -> bool {
613        match self {
614            Self::Enum { variants, .. } => variants.iter().all(|variant| {
615                variant
616                    .payload_kind()
617                    .is_none_or(Self::supports_group_probe)
618            }),
619            Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
620            Self::List(_)
621            | Self::Set(_)
622            | Self::Map { .. }
623            | Self::Structured { .. }
624            | Self::Unit => false,
625            Self::Account
626            | Self::Blob
627            | Self::Bool
628            | Self::Date
629            | Self::Decimal { .. }
630            | Self::Duration
631            | Self::Float32
632            | Self::Float64
633            | Self::Int
634            | Self::Int128
635            | Self::IntBig
636            | Self::Principal
637            | Self::Subaccount
638            | Self::Text { .. }
639            | Self::Timestamp
640            | Self::Uint
641            | Self::Uint128
642            | Self::UintBig
643            | Self::Ulid => true,
644        }
645    }
646
647    /// Match one runtime value against this field kind contract.
648    ///
649    /// This is the shared recursive field-kind acceptance boundary used by
650    /// persisted-row encoding, mutation-save validation, and aggregate field
651    /// extraction.
652    #[must_use]
653    pub(crate) fn accepts_value(&self, value: &Value) -> bool {
654        match (self, value) {
655            (Self::Account, Value::Account(_))
656            | (Self::Blob, Value::Blob(_))
657            | (Self::Bool, Value::Bool(_))
658            | (Self::Date, Value::Date(_))
659            | (Self::Decimal { .. }, Value::Decimal(_))
660            | (Self::Duration, Value::Duration(_))
661            | (Self::Enum { .. }, Value::Enum(_))
662            | (Self::Float32, Value::Float32(_))
663            | (Self::Float64, Value::Float64(_))
664            | (Self::Int, Value::Int(_))
665            | (Self::Int128, Value::Int128(_))
666            | (Self::IntBig, Value::IntBig(_))
667            | (Self::Principal, Value::Principal(_))
668            | (Self::Subaccount, Value::Subaccount(_))
669            | (Self::Text { .. }, Value::Text(_))
670            | (Self::Timestamp, Value::Timestamp(_))
671            | (Self::Uint, Value::Uint(_))
672            | (Self::Uint128, Value::Uint128(_))
673            | (Self::UintBig, Value::UintBig(_))
674            | (Self::Ulid, Value::Ulid(_))
675            | (Self::Unit, Value::Unit)
676            | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
677            (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
678            (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
679                items.iter().all(|item| inner.accepts_value(item))
680            }
681            (Self::Map { key, value }, Value::Map(entries)) => {
682                if Value::validate_map_entries(entries.as_slice()).is_err() {
683                    return false;
684                }
685
686                entries.iter().all(|(entry_key, entry_value)| {
687                    key.accepts_value(entry_key) && value.accepts_value(entry_value)
688                })
689            }
690            _ => false,
691        }
692    }
693}
694
695// `FieldStorageDecode::ByKind` follows the same literal compatibility rule as
696// the schema predicate layer without routing the storage model through
697// `db::schema`. Structured field kinds are intentionally not accepted here;
698// fields that persist open-ended structured payloads use
699// `FieldStorageDecode::Value` instead.
700fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
701    match (kind, value) {
702        (FieldKind::Relation { key_kind, .. }, value) => {
703            by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
704        }
705        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
706            .iter()
707            .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
708        (
709            FieldKind::Map {
710                key,
711                value: value_kind,
712            },
713            Value::Map(entries),
714        ) => {
715            if Value::validate_map_entries(entries.as_slice()).is_err() {
716                return false;
717            }
718
719            entries.iter().all(|(entry_key, entry_value)| {
720                by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
721                    && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
722            })
723        }
724        (FieldKind::Structured { .. }, _) => false,
725        _ => kind.accepts_value(value),
726    }
727}
728
729// `FieldStorageDecode::Value` fields persist an opaque runtime `Value` envelope,
730// so `FieldKind::Structured` must stay open-ended while outer collection/map
731// shapes still enforce the recursive structure the model owns.
732fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
733    match (kind, value) {
734        (FieldKind::Structured { .. }, _) => true,
735        (FieldKind::Relation { key_kind, .. }, value) => {
736            value_storage_kind_accepts_runtime_value(*key_kind, value)
737        }
738        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
739            .iter()
740            .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
741        (
742            FieldKind::Map {
743                key,
744                value: value_kind,
745            },
746            Value::Map(entries),
747        ) => {
748            if Value::validate_map_entries(entries.as_slice()).is_err() {
749                return false;
750            }
751
752            entries.iter().all(|(entry_key, entry_value)| {
753                value_storage_kind_accepts_runtime_value(*key, entry_key)
754                    && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
755            })
756        }
757        _ => kind.accepts_value(value),
758    }
759}
760
761// Enforce fixed decimal scales through nested collection/map shapes before a
762// field-level runtime value is persisted.
763fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
764    if matches!(value, Value::Null) {
765        return Ok(());
766    }
767
768    match (kind, value) {
769        (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
770            if decimal.scale() != scale {
771                return Err(format!(
772                    "decimal scale mismatch: expected {scale}, found {}",
773                    decimal.scale()
774                ));
775            }
776
777            Ok(())
778        }
779        (FieldKind::Relation { key_kind, .. }, value) => {
780            ensure_decimal_scale_matches(*key_kind, value)
781        }
782        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
783            for item in items {
784                ensure_decimal_scale_matches(*inner, item)?;
785            }
786
787            Ok(())
788        }
789        (
790            FieldKind::Map {
791                key,
792                value: map_value,
793            },
794            Value::Map(entries),
795        ) => {
796            for (entry_key, entry_value) in entries {
797                ensure_decimal_scale_matches(*key, entry_key)?;
798                ensure_decimal_scale_matches(*map_value, entry_value)?;
799            }
800
801            Ok(())
802        }
803        _ => Ok(()),
804    }
805}
806
807// Normalize fixed-scale decimal values through nested collection/map shapes
808// before the field-level payload is encoded. This is write-side canonicalization;
809// callers that validate already persisted bytes still use the exact scale check.
810fn normalize_decimal_scale_for_storage(
811    kind: FieldKind,
812    value: &Value,
813) -> Result<Cow<'_, Value>, String> {
814    if matches!(value, Value::Null) {
815        return Ok(Cow::Borrowed(value));
816    }
817
818    match (kind, value) {
819        (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
820            let normalized = decimal_with_storage_scale(*decimal, scale).ok_or_else(|| {
821                format!(
822                    "decimal scale mismatch: expected {scale}, found {}",
823                    decimal.scale()
824                )
825            })?;
826
827            if normalized.scale() == decimal.scale() {
828                Ok(Cow::Borrowed(value))
829            } else {
830                Ok(Cow::Owned(Value::Decimal(normalized)))
831            }
832        }
833        (FieldKind::Relation { key_kind, .. }, value) => {
834            normalize_decimal_scale_for_storage(*key_kind, value)
835        }
836        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
837            normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
838                items.map_or_else(
839                    || Cow::Borrowed(value),
840                    |items| Cow::Owned(Value::List(items)),
841                )
842            })
843        }
844        (
845            FieldKind::Map {
846                key,
847                value: map_value,
848            },
849            Value::Map(entries),
850        ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
851            entries.map_or_else(
852                || Cow::Borrowed(value),
853                |entries| Cow::Owned(Value::Map(entries)),
854            )
855        }),
856        _ => Ok(Cow::Borrowed(value)),
857    }
858}
859
860// Convert one decimal into the exact storage scale. Lower-scale values are
861// padded without changing their numeric value; higher-scale values use the same
862// round-half-away-from-zero policy as SQL/fluent decimal rounding.
863fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
864    match decimal.scale().cmp(&scale) {
865        Ordering::Equal => Some(decimal),
866        Ordering::Less => decimal
867            .scale_to_integer(scale)
868            .map(|mantissa| Decimal::from_i128_with_scale(mantissa, scale)),
869        Ordering::Greater => Some(decimal.round_dp(scale)),
870    }
871}
872
873// Normalize decimal items while preserving the original list allocation when
874// every item is already canonical for its nested field kind.
875fn normalize_decimal_list_items(
876    kind: FieldKind,
877    items: &[Value],
878) -> Result<Option<Vec<Value>>, String> {
879    let mut normalized_items = None;
880
881    for (index, item) in items.iter().enumerate() {
882        let normalized = normalize_decimal_scale_for_storage(kind, item)?;
883        if let Cow::Owned(value) = normalized {
884            let items = normalized_items.get_or_insert_with(|| items.to_vec());
885            items[index] = value;
886        }
887    }
888
889    Ok(normalized_items)
890}
891
892// Normalize decimal keys and values while preserving the original map
893// allocation when every entry is already canonical for its nested field kind.
894fn normalize_decimal_map_entries(
895    key_kind: FieldKind,
896    value_kind: FieldKind,
897    entries: &[(Value, Value)],
898) -> Result<Option<Vec<(Value, Value)>>, String> {
899    let mut normalized_entries = None;
900
901    for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
902        let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
903        let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
904
905        if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
906            let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
907            if let Cow::Owned(value) = normalized_key {
908                entries[index].0 = value;
909            }
910            if let Cow::Owned(value) = normalized_value {
911                entries[index].1 = value;
912            }
913        }
914    }
915
916    Ok(normalized_entries)
917}
918
919// Enforce bounded text length through nested collection/map shapes before a
920// field-level runtime value is persisted.
921fn ensure_text_max_len_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
922    if matches!(value, Value::Null) {
923        return Ok(());
924    }
925
926    match (kind, value) {
927        (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
928            let len = text.chars().count();
929            if len > max as usize {
930                return Err(format!(
931                    "text length exceeds max_len: expected at most {max}, found {len}"
932                ));
933            }
934
935            Ok(())
936        }
937        (FieldKind::Relation { key_kind, .. }, value) => {
938            ensure_text_max_len_matches(*key_kind, value)
939        }
940        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
941            for item in items {
942                ensure_text_max_len_matches(*inner, item)?;
943            }
944
945            Ok(())
946        }
947        (
948            FieldKind::Map {
949                key,
950                value: map_value,
951            },
952            Value::Map(entries),
953        ) => {
954            for (entry_key, entry_value) in entries {
955                ensure_text_max_len_matches(*key, entry_key)?;
956                ensure_text_max_len_matches(*map_value, entry_value)?;
957            }
958
959            Ok(())
960        }
961        _ => Ok(()),
962    }
963}
964
965// Enforce the canonical persisted ordering rules for set/map shapes before one
966// field-level runtime value becomes row bytes.
967fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
968    match (kind, value) {
969        (FieldKind::Set(_), Value::List(items)) => {
970            for pair in items.windows(2) {
971                let [left, right] = pair else {
972                    continue;
973                };
974                if Value::canonical_cmp(left, right) != Ordering::Less {
975                    return Err("set payload must already be canonical and deduplicated".into());
976                }
977            }
978
979            Ok(())
980        }
981        (FieldKind::Map { .. }, Value::Map(entries)) => {
982            Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
983
984            if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
985                return Err("map payload must already be canonical and deduplicated".into());
986            }
987
988            Ok(())
989        }
990        _ => Ok(()),
991    }
992}
993
994///
995/// TESTS
996///
997
998#[cfg(test)]
999mod tests {
1000    use crate::{
1001        model::field::{FieldKind, FieldModel},
1002        value::Value,
1003    };
1004
1005    static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1006
1007    #[test]
1008    fn text_max_len_accepts_unbounded_text() {
1009        let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1010
1011        assert!(
1012            field
1013                .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1014                .is_ok()
1015        );
1016    }
1017
1018    #[test]
1019    fn text_max_len_counts_unicode_scalars_not_bytes() {
1020        let field = FieldModel::generated("name", BOUNDED_TEXT);
1021
1022        assert!(
1023            field
1024                .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1025                .is_ok()
1026        );
1027        assert!(
1028            field
1029                .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1030                .is_err()
1031        );
1032    }
1033
1034    #[test]
1035    fn text_max_len_recurses_through_collections() {
1036        static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1037        static TEXT_MAP: FieldKind = FieldKind::Map {
1038            key: &BOUNDED_TEXT,
1039            value: &BOUNDED_TEXT,
1040        };
1041
1042        let list_field = FieldModel::generated("names", TEXT_LIST);
1043        let map_field = FieldModel::generated("labels", TEXT_MAP);
1044
1045        assert!(
1046            list_field
1047                .validate_runtime_value_for_storage(&Value::List(vec![
1048                    Value::Text("Ada".into()),
1049                    Value::Text("Bob".into()),
1050                ]))
1051                .is_ok()
1052        );
1053        assert!(
1054            list_field
1055                .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1056                .is_err()
1057        );
1058        assert!(
1059            map_field
1060                .validate_runtime_value_for_storage(&Value::Map(vec![(
1061                    Value::Text("key".into()),
1062                    Value::Text("val".into()),
1063                )]))
1064                .is_ok()
1065        );
1066        assert!(
1067            map_field
1068                .validate_runtime_value_for_storage(&Value::Map(vec![(
1069                    Value::Text("long".into()),
1070                    Value::Text("val".into()),
1071                )]))
1072                .is_err()
1073        );
1074    }
1075}