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