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