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_scalar_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        /// Optional schema-declared maximum byte length for blob fields.
519        max_len: Option<u32>,
520    },
521    Bool,
522    Date,
523    Decimal {
524        /// Required schema-declared fractional scale for decimal fields.
525        scale: u32,
526    },
527    Duration,
528    Enum {
529        /// Fully-qualified enum type path used for strict filter normalization.
530        path: &'static str,
531        /// Declared per-variant payload decode metadata.
532        variants: &'static [EnumVariantModel],
533    },
534    Float32,
535    Float64,
536    Int,
537    Int128,
538    IntBig,
539    Principal,
540    Subaccount,
541    Text {
542        /// Optional schema-declared maximum Unicode scalar count for text fields.
543        max_len: Option<u32>,
544    },
545    Timestamp,
546    Uint,
547    Uint128,
548    UintBig,
549    Ulid,
550    Unit,
551
552    /// Typed relation; `key_kind` reflects the referenced key type.
553    /// `strength` encodes strong vs. weak relation intent.
554    Relation {
555        /// Fully-qualified Rust type path for diagnostics.
556        target_path: &'static str,
557        /// Stable external name used in storage keys.
558        target_entity_name: &'static str,
559        /// Stable runtime identity used on hot execution paths.
560        target_entity_tag: EntityTag,
561        /// Data store path where the target entity is persisted.
562        target_store_path: &'static str,
563        key_kind: &'static Self,
564        strength: RelationStrength,
565    },
566
567    // Collections
568    List(&'static Self),
569    Set(&'static Self),
570    /// Deterministic, unordered key/value collection.
571    ///
572    /// Map fields are persistable and patchable, but not queryable or indexable.
573    Map {
574        key: &'static Self,
575        value: &'static Self,
576    },
577
578    /// Structured (non-atomic) value.
579    /// Queryability here controls whether predicates may target this field,
580    /// not whether it may be stored or updated.
581    Structured {
582        queryable: bool,
583    },
584}
585
586impl FieldKind {
587    #[must_use]
588    pub const fn value_kind(&self) -> RuntimeValueKind {
589        match self {
590            Self::Account
591            | Self::Blob { .. }
592            | Self::Bool
593            | Self::Date
594            | Self::Duration
595            | Self::Enum { .. }
596            | Self::Float32
597            | Self::Float64
598            | Self::Int
599            | Self::Int128
600            | Self::IntBig
601            | Self::Principal
602            | Self::Subaccount
603            | Self::Text { .. }
604            | Self::Timestamp
605            | Self::Uint
606            | Self::Uint128
607            | Self::UintBig
608            | Self::Ulid
609            | Self::Unit
610            | Self::Decimal { .. }
611            | Self::Relation { .. } => RuntimeValueKind::Atomic,
612            Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
613            Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
614            Self::Structured { queryable } => RuntimeValueKind::Structured {
615                queryable: *queryable,
616            },
617        }
618    }
619
620    /// Returns `true` if this field shape is permitted in
621    /// persisted or query-visible schemas under the current
622    /// determinism policy.
623    ///
624    /// This shape-level check is structural only; query-time policy
625    /// enforcement (for example, map predicate fencing) is applied at
626    /// query construction and validation boundaries.
627    #[must_use]
628    pub const fn is_deterministic_collection_shape(&self) -> bool {
629        match self {
630            Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
631
632            Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
633
634            Self::Map { key, value } => {
635                key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
636            }
637
638            _ => true,
639        }
640    }
641
642    /// Return true when this planner-frozen grouped field kind can stay on the
643    /// borrowed grouped-key probe path without owned canonical materialization.
644    #[must_use]
645    pub(crate) fn supports_group_probe(&self) -> bool {
646        match self {
647            Self::Enum { variants, .. } => variants.iter().all(|variant| {
648                variant
649                    .payload_kind()
650                    .is_none_or(Self::supports_group_probe)
651            }),
652            Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
653            Self::List(_)
654            | Self::Set(_)
655            | Self::Map { .. }
656            | Self::Structured { .. }
657            | Self::Unit => false,
658            Self::Account
659            | Self::Blob { .. }
660            | Self::Bool
661            | Self::Date
662            | Self::Decimal { .. }
663            | Self::Duration
664            | Self::Float32
665            | Self::Float64
666            | Self::Int
667            | Self::Int128
668            | Self::IntBig
669            | Self::Principal
670            | Self::Subaccount
671            | Self::Text { .. }
672            | Self::Timestamp
673            | Self::Uint
674            | Self::Uint128
675            | Self::UintBig
676            | Self::Ulid => true,
677        }
678    }
679
680    /// Match one runtime value against this field kind contract.
681    ///
682    /// This is the shared recursive field-kind acceptance boundary used by
683    /// persisted-row encoding, mutation-save validation, and aggregate field
684    /// extraction.
685    #[must_use]
686    pub(crate) fn accepts_value(&self, value: &Value) -> bool {
687        match (self, value) {
688            (Self::Account, Value::Account(_))
689            | (Self::Blob { .. }, Value::Blob(_))
690            | (Self::Bool, Value::Bool(_))
691            | (Self::Date, Value::Date(_))
692            | (Self::Decimal { .. }, Value::Decimal(_))
693            | (Self::Duration, Value::Duration(_))
694            | (Self::Enum { .. }, Value::Enum(_))
695            | (Self::Float32, Value::Float32(_))
696            | (Self::Float64, Value::Float64(_))
697            | (Self::Int, Value::Int(_))
698            | (Self::Int128, Value::Int128(_))
699            | (Self::IntBig, Value::IntBig(_))
700            | (Self::Principal, Value::Principal(_))
701            | (Self::Subaccount, Value::Subaccount(_))
702            | (Self::Text { .. }, Value::Text(_))
703            | (Self::Timestamp, Value::Timestamp(_))
704            | (Self::Uint, Value::Uint(_))
705            | (Self::Uint128, Value::Uint128(_))
706            | (Self::UintBig, Value::UintBig(_))
707            | (Self::Ulid, Value::Ulid(_))
708            | (Self::Unit, Value::Unit)
709            | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
710            (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
711            (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
712                items.iter().all(|item| inner.accepts_value(item))
713            }
714            (Self::Map { key, value }, Value::Map(entries)) => {
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                    key.accepts_value(entry_key) && value.accepts_value(entry_value)
721                })
722            }
723            _ => false,
724        }
725    }
726}
727
728// `FieldStorageDecode::ByKind` follows the same literal compatibility rule as
729// the schema predicate layer without routing the storage model through
730// `db::schema`. Structured field kinds are intentionally not accepted here;
731// fields that persist open-ended structured payloads use
732// `FieldStorageDecode::Value` instead.
733fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
734    match (kind, value) {
735        (FieldKind::Relation { key_kind, .. }, value) => {
736            by_kind_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| by_kind_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                by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
754                    && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
755            })
756        }
757        (FieldKind::Structured { .. }, _) => false,
758        _ => kind.accepts_value(value),
759    }
760}
761
762// `FieldStorageDecode::Value` fields persist an opaque runtime `Value` envelope,
763// so `FieldKind::Structured` must stay open-ended while outer collection/map
764// shapes still enforce the recursive structure the model owns.
765fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
766    match (kind, value) {
767        (FieldKind::Structured { .. }, _) => true,
768        (FieldKind::Relation { key_kind, .. }, value) => {
769            value_storage_kind_accepts_runtime_value(*key_kind, value)
770        }
771        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
772            .iter()
773            .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
774        (
775            FieldKind::Map {
776                key,
777                value: value_kind,
778            },
779            Value::Map(entries),
780        ) => {
781            if Value::validate_map_entries(entries.as_slice()).is_err() {
782                return false;
783            }
784
785            entries.iter().all(|(entry_key, entry_value)| {
786                value_storage_kind_accepts_runtime_value(*key, entry_key)
787                    && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
788            })
789        }
790        _ => kind.accepts_value(value),
791    }
792}
793
794// Enforce fixed decimal scales through nested collection/map shapes before a
795// field-level runtime value is persisted.
796fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
797    if matches!(value, Value::Null) {
798        return Ok(());
799    }
800
801    match (kind, value) {
802        (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
803            if decimal.scale() != scale {
804                return Err(format!(
805                    "decimal scale mismatch: expected {scale}, found {}",
806                    decimal.scale()
807                ));
808            }
809
810            Ok(())
811        }
812        (FieldKind::Relation { key_kind, .. }, value) => {
813            ensure_decimal_scale_matches(*key_kind, value)
814        }
815        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
816            for item in items {
817                ensure_decimal_scale_matches(*inner, item)?;
818            }
819
820            Ok(())
821        }
822        (
823            FieldKind::Map {
824                key,
825                value: map_value,
826            },
827            Value::Map(entries),
828        ) => {
829            for (entry_key, entry_value) in entries {
830                ensure_decimal_scale_matches(*key, entry_key)?;
831                ensure_decimal_scale_matches(*map_value, entry_value)?;
832            }
833
834            Ok(())
835        }
836        _ => Ok(()),
837    }
838}
839
840// Normalize fixed-scale decimal values through nested collection/map shapes
841// before the field-level payload is encoded. This is write-side canonicalization;
842// callers that validate already persisted bytes still use the exact scale check.
843fn normalize_decimal_scale_for_storage(
844    kind: FieldKind,
845    value: &Value,
846) -> Result<Cow<'_, Value>, String> {
847    if matches!(value, Value::Null) {
848        return Ok(Cow::Borrowed(value));
849    }
850
851    match (kind, value) {
852        (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
853            let normalized = decimal_with_storage_scale(*decimal, scale).ok_or_else(|| {
854                format!(
855                    "decimal scale mismatch: expected {scale}, found {}",
856                    decimal.scale()
857                )
858            })?;
859
860            if normalized.scale() == decimal.scale() {
861                Ok(Cow::Borrowed(value))
862            } else {
863                Ok(Cow::Owned(Value::Decimal(normalized)))
864            }
865        }
866        (FieldKind::Relation { key_kind, .. }, value) => {
867            normalize_decimal_scale_for_storage(*key_kind, value)
868        }
869        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
870            normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
871                items.map_or_else(
872                    || Cow::Borrowed(value),
873                    |items| Cow::Owned(Value::List(items)),
874                )
875            })
876        }
877        (
878            FieldKind::Map {
879                key,
880                value: map_value,
881            },
882            Value::Map(entries),
883        ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
884            entries.map_or_else(
885                || Cow::Borrowed(value),
886                |entries| Cow::Owned(Value::Map(entries)),
887            )
888        }),
889        _ => Ok(Cow::Borrowed(value)),
890    }
891}
892
893// Convert one decimal into the exact storage scale. Lower-scale values are
894// padded without changing their numeric value; higher-scale values use the same
895// round-half-away-from-zero policy as SQL/fluent decimal rounding.
896fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
897    match decimal.scale().cmp(&scale) {
898        Ordering::Equal => Some(decimal),
899        Ordering::Less => decimal
900            .scale_to_integer(scale)
901            .map(|mantissa| Decimal::from_i128_with_scale(mantissa, scale)),
902        Ordering::Greater => Some(decimal.round_dp(scale)),
903    }
904}
905
906// Normalize decimal items while preserving the original list allocation when
907// every item is already canonical for its nested field kind.
908fn normalize_decimal_list_items(
909    kind: FieldKind,
910    items: &[Value],
911) -> Result<Option<Vec<Value>>, String> {
912    let mut normalized_items = None;
913
914    for (index, item) in items.iter().enumerate() {
915        let normalized = normalize_decimal_scale_for_storage(kind, item)?;
916        if let Cow::Owned(value) = normalized {
917            let items = normalized_items.get_or_insert_with(|| items.to_vec());
918            items[index] = value;
919        }
920    }
921
922    Ok(normalized_items)
923}
924
925// Normalize decimal keys and values while preserving the original map
926// allocation when every entry is already canonical for its nested field kind.
927fn normalize_decimal_map_entries(
928    key_kind: FieldKind,
929    value_kind: FieldKind,
930    entries: &[(Value, Value)],
931) -> Result<Option<Vec<(Value, Value)>>, String> {
932    let mut normalized_entries = None;
933
934    for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
935        let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
936        let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
937
938        if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
939            let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
940            if let Cow::Owned(value) = normalized_key {
941                entries[index].0 = value;
942            }
943            if let Cow::Owned(value) = normalized_value {
944                entries[index].1 = value;
945            }
946        }
947    }
948
949    Ok(normalized_entries)
950}
951
952// Enforce bounded text/blob length through nested collection/map shapes before
953// a field-level runtime value is persisted.
954fn ensure_scalar_max_len_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
955    if matches!(value, Value::Null) {
956        return Ok(());
957    }
958
959    match (kind, value) {
960        (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
961            let len = text.chars().count();
962            if len > max as usize {
963                return Err(format!(
964                    "text length exceeds max_len: expected at most {max}, found {len}"
965                ));
966            }
967
968            Ok(())
969        }
970        (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
971            let len = bytes.len();
972            if len > max as usize {
973                return Err(format!(
974                    "blob length exceeds max_len: expected at most {max}, found {len}"
975                ));
976            }
977
978            Ok(())
979        }
980        (FieldKind::Relation { key_kind, .. }, value) => {
981            ensure_scalar_max_len_matches(*key_kind, value)
982        }
983        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
984            for item in items {
985                ensure_scalar_max_len_matches(*inner, item)?;
986            }
987
988            Ok(())
989        }
990        (
991            FieldKind::Map {
992                key,
993                value: map_value,
994            },
995            Value::Map(entries),
996        ) => {
997            for (entry_key, entry_value) in entries {
998                ensure_scalar_max_len_matches(*key, entry_key)?;
999                ensure_scalar_max_len_matches(*map_value, entry_value)?;
1000            }
1001
1002            Ok(())
1003        }
1004        _ => Ok(()),
1005    }
1006}
1007
1008// Enforce the canonical persisted ordering rules for set/map shapes before one
1009// field-level runtime value becomes row bytes.
1010fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
1011    match (kind, value) {
1012        (FieldKind::Set(_), Value::List(items)) => {
1013            for pair in items.windows(2) {
1014                let [left, right] = pair else {
1015                    continue;
1016                };
1017                if Value::canonical_cmp(left, right) != Ordering::Less {
1018                    return Err("set payload must already be canonical and deduplicated".into());
1019                }
1020            }
1021
1022            Ok(())
1023        }
1024        (FieldKind::Map { .. }, Value::Map(entries)) => {
1025            Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
1026
1027            if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1028                return Err("map payload must already be canonical and deduplicated".into());
1029            }
1030
1031            Ok(())
1032        }
1033        _ => Ok(()),
1034    }
1035}
1036
1037///
1038/// TESTS
1039///
1040
1041#[cfg(test)]
1042mod tests {
1043    use crate::{
1044        model::field::{FieldKind, FieldModel},
1045        value::Value,
1046    };
1047
1048    static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1049    static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
1050
1051    #[test]
1052    fn text_max_len_accepts_unbounded_text() {
1053        let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1054
1055        assert!(
1056            field
1057                .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1058                .is_ok()
1059        );
1060    }
1061
1062    #[test]
1063    fn text_max_len_counts_unicode_scalars_not_bytes() {
1064        let field = FieldModel::generated("name", BOUNDED_TEXT);
1065
1066        assert!(
1067            field
1068                .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1069                .is_ok()
1070        );
1071        assert!(
1072            field
1073                .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1074                .is_err()
1075        );
1076    }
1077
1078    #[test]
1079    fn text_max_len_recurses_through_collections() {
1080        static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1081        static TEXT_MAP: FieldKind = FieldKind::Map {
1082            key: &BOUNDED_TEXT,
1083            value: &BOUNDED_TEXT,
1084        };
1085
1086        let list_field = FieldModel::generated("names", TEXT_LIST);
1087        let map_field = FieldModel::generated("labels", TEXT_MAP);
1088
1089        assert!(
1090            list_field
1091                .validate_runtime_value_for_storage(&Value::List(vec![
1092                    Value::Text("Ada".into()),
1093                    Value::Text("Bob".into()),
1094                ]))
1095                .is_ok()
1096        );
1097        assert!(
1098            list_field
1099                .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1100                .is_err()
1101        );
1102        assert!(
1103            map_field
1104                .validate_runtime_value_for_storage(&Value::Map(vec![(
1105                    Value::Text("key".into()),
1106                    Value::Text("val".into()),
1107                )]))
1108                .is_ok()
1109        );
1110        assert!(
1111            map_field
1112                .validate_runtime_value_for_storage(&Value::Map(vec![(
1113                    Value::Text("long".into()),
1114                    Value::Text("val".into()),
1115                )]))
1116                .is_err()
1117        );
1118    }
1119
1120    #[test]
1121    fn blob_max_len_counts_bytes() {
1122        let field = FieldModel::generated("payload", BOUNDED_BLOB);
1123
1124        assert!(
1125            field
1126                .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1127                .is_ok()
1128        );
1129        assert!(
1130            field
1131                .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1132                .is_err()
1133        );
1134    }
1135
1136    #[test]
1137    fn blob_max_len_recurses_through_collections() {
1138        static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1139
1140        let field = FieldModel::generated("payloads", BLOB_LIST);
1141
1142        assert!(
1143            field
1144                .validate_runtime_value_for_storage(&Value::List(vec![
1145                    Value::Blob(vec![1, 2, 3]),
1146                    Value::Blob(vec![4, 5]),
1147                ]))
1148                .is_ok()
1149        );
1150        assert!(
1151            field
1152                .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1153                    1, 2, 3, 4
1154                ])]))
1155                .is_err()
1156        );
1157    }
1158}