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