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