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
6#[cfg(test)]
7use crate::value::Value;
8use crate::{types::EntityTag, value::RuntimeValueKind};
9#[cfg(test)]
10use std::cmp::Ordering;
11
12#[cfg(test)]
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    /// Name-based unit-enum default awaiting accepted-catalog admission.
190    ///
191    /// This variant is generated proposal metadata only. It must resolve to
192    /// canonical IDs before an accepted schema snapshot is persisted.
193    AuthoredEnumUnit {
194        /// Generated enum schema path.
195        enum_path: &'static str,
196        /// Generated unit variant name.
197        variant: &'static str,
198    },
199}
200
201///
202/// FieldInsertGeneration
203///
204/// FieldInsertGeneration declares whether one runtime field may be synthesized
205/// by the reduced SQL insert boundary when the user omits that field.
206/// This stays separate from typed-Rust `Default` behavior so write-time
207/// generation remains an explicit schema contract.
208///
209
210#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211pub enum FieldInsertGeneration {
212    /// Generate one fresh `Ulid` value at insert time.
213    Ulid,
214    /// Generate one current wall-clock `Timestamp` value at insert time.
215    Timestamp,
216}
217
218///
219/// FieldWriteManagement
220///
221/// FieldWriteManagement declares whether one runtime field is owned by the
222/// write boundary during insert or update synthesis.
223/// This keeps auto-managed system fields explicit in schema/runtime metadata
224/// instead of relying on literal field names in write paths.
225///
226
227#[derive(Clone, Copy, Debug, Eq, PartialEq)]
228pub enum FieldWriteManagement {
229    /// Fill only on insert when the row is first created.
230    CreatedAt,
231    /// Refresh on insert and every update.
232    UpdatedAt,
233}
234
235impl FieldModel {
236    /// Build one generated runtime field descriptor.
237    ///
238    /// This constructor exists for derive/codegen output and trusted test
239    /// fixtures. Runtime planning and execution treat `FieldModel` values as
240    /// build-time-validated metadata.
241    #[must_use]
242    #[doc(hidden)]
243    pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
244        Self::generated_with_storage_decode_and_nullability(
245            name,
246            kind,
247            FieldStorageDecode::ByKind,
248            false,
249        )
250    }
251
252    /// Build one runtime field descriptor with an explicit persisted decode contract.
253    #[must_use]
254    #[doc(hidden)]
255    pub const fn generated_with_storage_decode(
256        name: &'static str,
257        kind: FieldKind,
258        storage_decode: FieldStorageDecode,
259    ) -> Self {
260        Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
261    }
262
263    /// Build one runtime field descriptor with an explicit decode contract and nullability.
264    #[must_use]
265    #[doc(hidden)]
266    pub const fn generated_with_storage_decode_and_nullability(
267        name: &'static str,
268        kind: FieldKind,
269        storage_decode: FieldStorageDecode,
270        nullable: bool,
271    ) -> Self {
272        Self::generated_with_storage_decode_nullability_and_write_policies(
273            name,
274            kind,
275            storage_decode,
276            nullable,
277            None,
278            None,
279        )
280    }
281
282    /// Build one runtime field descriptor with an explicit decode contract, nullability,
283    /// and insert-time generation contract.
284    #[must_use]
285    #[doc(hidden)]
286    pub const fn generated_with_storage_decode_nullability_and_insert_generation(
287        name: &'static str,
288        kind: FieldKind,
289        storage_decode: FieldStorageDecode,
290        nullable: bool,
291        insert_generation: Option<FieldInsertGeneration>,
292    ) -> Self {
293        Self::generated_with_storage_decode_nullability_and_write_policies(
294            name,
295            kind,
296            storage_decode,
297            nullable,
298            insert_generation,
299            None,
300        )
301    }
302
303    /// Build one runtime field descriptor with explicit insert-generation and
304    /// write-management policies.
305    #[must_use]
306    #[doc(hidden)]
307    pub const fn generated_with_storage_decode_nullability_and_write_policies(
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    ) -> Self {
315        Self {
316            name,
317            kind,
318            nested_fields: &[],
319            nullable,
320            storage_decode,
321            leaf_codec: leaf_codec_for(kind, storage_decode),
322            insert_generation,
323            write_management,
324            database_default: FieldDatabaseDefault::None,
325        }
326    }
327
328    /// Build one runtime field descriptor with nested generated-record field metadata.
329    #[must_use]
330    #[doc(hidden)]
331    pub const fn generated_with_storage_decode_nullability_write_policies_and_nested_fields(
332        name: &'static str,
333        kind: FieldKind,
334        storage_decode: FieldStorageDecode,
335        nullable: bool,
336        insert_generation: Option<FieldInsertGeneration>,
337        write_management: Option<FieldWriteManagement>,
338        nested_fields: &'static [Self],
339    ) -> Self {
340        Self::generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
341            name,
342            kind,
343            storage_decode,
344            nullable,
345            insert_generation,
346            write_management,
347            FieldDatabaseDefault::None,
348            nested_fields,
349        )
350    }
351
352    /// Build one runtime field descriptor with explicit write policies,
353    /// database default metadata, and nested generated-record field metadata.
354    #[must_use]
355    #[doc(hidden)]
356    #[expect(
357        clippy::too_many_arguments,
358        reason = "generated schema metadata keeps every field contract explicit"
359    )]
360    pub const fn generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
361        name: &'static str,
362        kind: FieldKind,
363        storage_decode: FieldStorageDecode,
364        nullable: bool,
365        insert_generation: Option<FieldInsertGeneration>,
366        write_management: Option<FieldWriteManagement>,
367        database_default: FieldDatabaseDefault,
368        nested_fields: &'static [Self],
369    ) -> Self {
370        Self {
371            name,
372            kind,
373            nested_fields,
374            nullable,
375            storage_decode,
376            leaf_codec: leaf_codec_for(kind, storage_decode),
377            insert_generation,
378            write_management,
379            database_default,
380        }
381    }
382
383    /// Return the stable field name.
384    #[must_use]
385    pub const fn name(&self) -> &'static str {
386        self.name
387    }
388
389    /// Return the runtime type-kind descriptor.
390    #[must_use]
391    pub const fn kind(&self) -> FieldKind {
392        self.kind
393    }
394
395    /// Return known nested fields for generated structured records.
396    #[must_use]
397    pub const fn nested_fields(&self) -> &'static [Self] {
398        self.nested_fields
399    }
400
401    /// Return whether the persisted field contract permits explicit `NULL`.
402    #[must_use]
403    pub const fn nullable(&self) -> bool {
404        self.nullable
405    }
406
407    /// Return the persisted field decode contract.
408    #[must_use]
409    pub const fn storage_decode(&self) -> FieldStorageDecode {
410        self.storage_decode
411    }
412
413    /// Return the persisted leaf payload codec.
414    #[must_use]
415    pub const fn leaf_codec(&self) -> LeafCodec {
416        self.leaf_codec
417    }
418
419    /// Return the reduced-SQL insert-time generation contract for this field.
420    #[must_use]
421    pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
422        self.insert_generation
423    }
424
425    /// Return the write-boundary management contract for this field.
426    #[must_use]
427    pub const fn write_management(&self) -> Option<FieldWriteManagement> {
428        self.write_management
429    }
430
431    /// Return the database-level default contract for this field.
432    #[must_use]
433    pub const fn database_default(&self) -> FieldDatabaseDefault {
434        self.database_default
435    }
436
437    /// Validate one runtime value against this field's persisted storage contract.
438    ///
439    /// This is the model-owned compatibility gate used before row bytes are
440    /// emitted. It intentionally checks storage compatibility, not query
441    /// predicate compatibility, so `FieldStorageDecode::Value` can accept
442    /// open-ended structured payloads while still enforcing outer collection
443    /// shape, decimal scale, and deterministic set/map ordering.
444    #[cfg(test)]
445    pub(crate) fn validate_runtime_value_for_storage(
446        &self,
447        value: &Value,
448    ) -> Result<(), FieldStorageValidationError> {
449        if matches!(value, Value::Null) {
450            if self.nullable() {
451                return Ok(());
452            }
453
454            return Err(FieldStorageValidationError::RequiredFieldNull);
455        }
456
457        let accepts = match self.storage_decode() {
458            FieldStorageDecode::Value => {
459                value_storage_kind_accepts_runtime_value(self.kind(), value)
460            }
461            FieldStorageDecode::ByKind => {
462                by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
463            }
464        };
465        if !accepts {
466            return Err(FieldStorageValidationError::StorageContract);
467        }
468
469        ensure_decimal_scale_matches(self.kind(), value)?;
470        ensure_scalar_max_len_matches(self.kind(), value)?;
471        ensure_value_is_deterministic_for_storage(self.kind(), value)
472    }
473}
474
475// Resolve the canonical leaf codec from semantic field kind plus storage
476// contract. Fields that intentionally persist as `Value` or that still require
477// recursive payload decoding remain on the shared structural fallback.
478const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
479    if matches!(storage_decode, FieldStorageDecode::Value) {
480        return LeafCodec::StructuralFallback;
481    }
482
483    match kind {
484        FieldKind::Blob { .. } => LeafCodec::Scalar(ScalarCodec::Blob),
485        FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
486        FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
487        FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
488        FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
489        FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
490        FieldKind::Int8 | FieldKind::Int16 | FieldKind::Int32 | FieldKind::Int64 => {
491            LeafCodec::Scalar(ScalarCodec::Int64)
492        }
493        FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
494        FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
495        FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
496        FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
497        FieldKind::Nat8 | FieldKind::Nat16 | FieldKind::Nat32 | FieldKind::Nat64 => {
498            LeafCodec::Scalar(ScalarCodec::Nat64)
499        }
500        FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
501        FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
502        FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
503        FieldKind::Account
504        | FieldKind::Decimal { .. }
505        | FieldKind::Enum { .. }
506        | FieldKind::Int128
507        | FieldKind::IntBig { .. }
508        | FieldKind::List(_)
509        | FieldKind::Map { .. }
510        | FieldKind::Set(_)
511        | FieldKind::Structured { .. }
512        | FieldKind::Nat128
513        | FieldKind::NatBig { .. } => LeafCodec::StructuralFallback,
514    }
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    /// Enforced typed relation; `key_kind` reflects the referenced key type.
578    Relation {
579        /// Fully-qualified Rust type path for diagnostics.
580        target_path: &'static str,
581        /// Stable external name used in storage keys.
582        target_entity_name: &'static str,
583        /// Stable runtime identity used on hot execution paths.
584        target_entity_tag: EntityTag,
585        /// Data store path where the target entity is persisted.
586        target_store_path: &'static str,
587        key_kind: &'static Self,
588    },
589
590    // Collections
591    List(&'static Self),
592    Set(&'static Self),
593    /// Deterministic, unordered key/value collection.
594    ///
595    /// Map fields are persistable and patchable, but not queryable or indexable.
596    Map {
597        key: &'static Self,
598        value: &'static Self,
599    },
600
601    /// Structured (non-atomic) value.
602    /// Queryability here controls whether predicates may target this field,
603    /// not whether it may be stored or updated.
604    Structured {
605        queryable: bool,
606    },
607}
608
609impl FieldKind {
610    #[must_use]
611    pub const fn value_kind(&self) -> RuntimeValueKind {
612        match self {
613            Self::Account
614            | Self::Blob { .. }
615            | Self::Bool
616            | Self::Date
617            | Self::Duration
618            | Self::Enum { .. }
619            | Self::Float32
620            | Self::Float64
621            | Self::Int8
622            | Self::Int16
623            | Self::Int32
624            | Self::Int64
625            | Self::Int128
626            | Self::IntBig { .. }
627            | Self::Principal
628            | Self::Subaccount
629            | Self::Text { .. }
630            | Self::Timestamp
631            | Self::Nat8
632            | Self::Nat16
633            | Self::Nat32
634            | Self::Nat64
635            | Self::Nat128
636            | Self::NatBig { .. }
637            | Self::Ulid
638            | Self::Unit
639            | Self::Decimal { .. }
640            | Self::Relation { .. } => RuntimeValueKind::Atomic,
641            Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
642            Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
643            Self::Structured { queryable } => RuntimeValueKind::Structured {
644                queryable: *queryable,
645            },
646        }
647    }
648
649    /// Returns `true` if this field shape is permitted in
650    /// persisted or query-visible schemas under the current
651    /// determinism policy.
652    ///
653    /// This shape-level check is structural only; query-time policy
654    /// enforcement (for example, map predicate fencing) is applied at
655    /// query construction and validation boundaries.
656    #[must_use]
657    pub const fn is_deterministic_collection_shape(&self) -> bool {
658        match self {
659            Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
660
661            Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
662
663            Self::Map { key, value } => {
664                key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
665            }
666
667            _ => true,
668        }
669    }
670
671    /// Return true when this planner-frozen grouped field kind can stay on the
672    /// borrowed grouped-key probe path without owned canonical materialization.
673    #[cfg(test)]
674    #[must_use]
675    pub(crate) fn supports_group_probe(&self) -> bool {
676        match self {
677            Self::Enum { variants, .. } => variants.iter().all(|variant| {
678                variant
679                    .payload_kind()
680                    .is_none_or(Self::supports_group_probe)
681            }),
682            Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
683            Self::Decimal { .. } => true,
684            _ => super::field_kind_semantics::field_kind_has_identity_group_canonical_form(*self),
685        }
686    }
687
688    /// Match one runtime value against generated model metadata in tests.
689    #[cfg(test)]
690    #[must_use]
691    pub(crate) fn accepts_value(&self, value: &Value) -> bool {
692        match (self, value) {
693            (Self::Account, Value::Account(_))
694            | (Self::Blob { .. }, Value::Blob(_))
695            | (Self::Bool, Value::Bool(_))
696            | (Self::Date, Value::Date(_))
697            | (Self::Decimal { .. }, Value::Decimal(_))
698            | (Self::Duration, Value::Duration(_))
699            | (Self::Enum { .. }, Value::Enum(_))
700            | (Self::Float32, Value::Float32(_))
701            | (Self::Float64, Value::Float64(_))
702            | (Self::Int64, Value::Int64(_))
703            | (Self::Int128, Value::Int128(_))
704            | (Self::Nat64, Value::Nat64(_))
705            | (Self::Principal, Value::Principal(_))
706            | (Self::Subaccount, Value::Subaccount(_))
707            | (Self::Text { .. }, Value::Text(_))
708            | (Self::Timestamp, Value::Timestamp(_))
709            | (Self::Nat128, Value::Nat128(_))
710            | (Self::Ulid, Value::Ulid(_))
711            | (Self::Unit, Value::Unit)
712            | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
713            (Self::Int8, Value::Int64(value)) => i8::try_from(*value).is_ok(),
714            (Self::Int16, Value::Int64(value)) => i16::try_from(*value).is_ok(),
715            (Self::Int32, Value::Int64(value)) => i32::try_from(*value).is_ok(),
716            (Self::Nat8, Value::Nat64(value)) => u8::try_from(*value).is_ok(),
717            (Self::Nat16, Value::Nat64(value)) => u16::try_from(*value).is_ok(),
718            (Self::Nat32, Value::Nat64(value)) => u32::try_from(*value).is_ok(),
719            (Self::IntBig { max_bytes }, Value::IntBig(value)) => {
720                value.to_leb128().len() <= *max_bytes as usize
721            }
722            (Self::NatBig { max_bytes }, Value::NatBig(value)) => {
723                value.to_leb128().len() <= *max_bytes as usize
724            }
725            (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
726            (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
727                items.iter().all(|item| inner.accepts_value(item))
728            }
729            (Self::Map { key, value }, Value::Map(entries)) => {
730                Value::validate_map_entries(entries.as_slice()).is_ok()
731                    && entries.iter().all(|(entry_key, entry_value)| {
732                        key.accepts_value(entry_key) && value.accepts_value(entry_value)
733                    })
734            }
735            _ => false,
736        }
737    }
738}
739
740// `FieldStorageDecode::ByKind` follows the same literal compatibility rule as
741// the schema predicate layer without routing the storage model through
742// `db::schema`. Structured field kinds are intentionally not accepted here;
743// fields that persist open-ended structured payloads use
744// `FieldStorageDecode::Value` instead.
745#[cfg(test)]
746fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
747    match (kind, value) {
748        (FieldKind::Relation { key_kind, .. }, value) => {
749            by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
750        }
751        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
752            .iter()
753            .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
754        (
755            FieldKind::Map {
756                key,
757                value: value_kind,
758            },
759            Value::Map(entries),
760        ) => {
761            if Value::validate_map_entries(entries.as_slice()).is_err() {
762                return false;
763            }
764
765            entries.iter().all(|(entry_key, entry_value)| {
766                by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
767                    && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
768            })
769        }
770        (FieldKind::Structured { .. }, _) => false,
771        _ => kind.accepts_value(value),
772    }
773}
774
775// `FieldStorageDecode::Value` fields persist an opaque runtime `Value` envelope,
776// so `FieldKind::Structured` must stay open-ended while outer collection/map
777// shapes still enforce the recursive structure the model owns.
778#[cfg(test)]
779fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
780    match (kind, value) {
781        (FieldKind::Structured { .. }, _) => true,
782        (FieldKind::Relation { key_kind, .. }, value) => {
783            value_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| value_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                value_storage_kind_accepts_runtime_value(*key, entry_key)
801                    && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
802            })
803        }
804        _ => kind.accepts_value(value),
805    }
806}
807
808// Enforce fixed decimal scales through nested collection/map shapes before a
809// field-level runtime value is persisted.
810#[cfg(test)]
811fn ensure_decimal_scale_matches(
812    kind: FieldKind,
813    value: &Value,
814) -> Result<(), FieldStorageValidationError> {
815    if matches!(value, Value::Null) {
816        return Ok(());
817    }
818
819    match (kind, value) {
820        (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
821            if decimal.scale() != scale {
822                return Err(FieldStorageValidationError::DecimalScaleMismatch);
823            }
824
825            Ok(())
826        }
827        (FieldKind::Relation { key_kind, .. }, value) => {
828            ensure_decimal_scale_matches(*key_kind, value)
829        }
830        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
831            for item in items {
832                ensure_decimal_scale_matches(*inner, item)?;
833            }
834
835            Ok(())
836        }
837        (
838            FieldKind::Map {
839                key,
840                value: map_value,
841            },
842            Value::Map(entries),
843        ) => {
844            for (entry_key, entry_value) in entries {
845                ensure_decimal_scale_matches(*key, entry_key)?;
846                ensure_decimal_scale_matches(*map_value, entry_value)?;
847            }
848
849            Ok(())
850        }
851        _ => Ok(()),
852    }
853}
854
855// Enforce bounded text/blob length through nested collection/map shapes before
856// a field-level runtime value is persisted.
857#[cfg(test)]
858fn ensure_scalar_max_len_matches(
859    kind: FieldKind,
860    value: &Value,
861) -> Result<(), FieldStorageValidationError> {
862    if matches!(value, Value::Null) {
863        return Ok(());
864    }
865
866    match (kind, value) {
867        (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
868            let len = text.chars().count();
869            if len > max as usize {
870                return Err(FieldStorageValidationError::ScalarMaxLen);
871            }
872
873            Ok(())
874        }
875        (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
876            let len = bytes.len();
877            if len > max as usize {
878                return Err(FieldStorageValidationError::ScalarMaxLen);
879            }
880
881            Ok(())
882        }
883        (FieldKind::Relation { key_kind, .. }, value) => {
884            ensure_scalar_max_len_matches(*key_kind, value)
885        }
886        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
887            for item in items {
888                ensure_scalar_max_len_matches(*inner, item)?;
889            }
890
891            Ok(())
892        }
893        (
894            FieldKind::Map {
895                key,
896                value: map_value,
897            },
898            Value::Map(entries),
899        ) => {
900            for (entry_key, entry_value) in entries {
901                ensure_scalar_max_len_matches(*key, entry_key)?;
902                ensure_scalar_max_len_matches(*map_value, entry_value)?;
903            }
904
905            Ok(())
906        }
907        _ => Ok(()),
908    }
909}
910
911// Enforce the canonical persisted ordering rules for set/map shapes before one
912// field-level runtime value becomes row bytes.
913#[cfg(test)]
914fn ensure_value_is_deterministic_for_storage(
915    kind: FieldKind,
916    value: &Value,
917) -> Result<(), FieldStorageValidationError> {
918    match (kind, value) {
919        (FieldKind::Set(_), Value::List(items)) => {
920            for pair in items.windows(2) {
921                let [left, right] = pair else {
922                    continue;
923                };
924                if Value::canonical_cmp(left, right) != Ordering::Less {
925                    return Err(FieldStorageValidationError::SetCanonicalOrder);
926                }
927            }
928
929            Ok(())
930        }
931        (FieldKind::Map { .. }, Value::Map(entries)) => {
932            Value::validate_map_entries(entries.as_slice())
933                .map_err(|_| FieldStorageValidationError::MapEntryContract)?;
934
935            if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
936                return Err(FieldStorageValidationError::MapCanonicalOrder);
937            }
938
939            Ok(())
940        }
941        _ => Ok(()),
942    }
943}
944
945///
946/// TESTS
947///
948
949#[cfg(test)]
950mod tests {
951    use crate::{
952        model::field::{FieldKind, FieldModel},
953        value::Value,
954    };
955
956    static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
957    static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
958
959    #[test]
960    fn text_max_len_accepts_unbounded_text() {
961        let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
962
963        assert!(
964            field
965                .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
966                .is_ok()
967        );
968    }
969
970    #[test]
971    fn text_max_len_counts_unicode_scalars_not_bytes() {
972        let field = FieldModel::generated("name", BOUNDED_TEXT);
973
974        assert!(
975            field
976                .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
977                .is_ok()
978        );
979        assert!(
980            field
981                .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
982                .is_err()
983        );
984    }
985
986    #[test]
987    fn text_max_len_recurses_through_collections() {
988        static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
989        static TEXT_MAP: FieldKind = FieldKind::Map {
990            key: &BOUNDED_TEXT,
991            value: &BOUNDED_TEXT,
992        };
993
994        let list_field = FieldModel::generated("names", TEXT_LIST);
995        let map_field = FieldModel::generated("labels", TEXT_MAP);
996
997        assert!(
998            list_field
999                .validate_runtime_value_for_storage(&Value::List(vec![
1000                    Value::Text("Ada".into()),
1001                    Value::Text("Bob".into()),
1002                ]))
1003                .is_ok()
1004        );
1005        assert!(
1006            list_field
1007                .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1008                .is_err()
1009        );
1010        assert!(
1011            map_field
1012                .validate_runtime_value_for_storage(&Value::Map(vec![(
1013                    Value::Text("key".into()),
1014                    Value::Text("val".into()),
1015                )]))
1016                .is_ok()
1017        );
1018        assert!(
1019            map_field
1020                .validate_runtime_value_for_storage(&Value::Map(vec![(
1021                    Value::Text("long".into()),
1022                    Value::Text("val".into()),
1023                )]))
1024                .is_err()
1025        );
1026    }
1027
1028    #[test]
1029    fn blob_max_len_counts_bytes() {
1030        let field = FieldModel::generated("payload", BOUNDED_BLOB);
1031
1032        assert!(
1033            field
1034                .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1035                .is_ok()
1036        );
1037        assert!(
1038            field
1039                .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1040                .is_err()
1041        );
1042    }
1043
1044    #[test]
1045    fn blob_max_len_recurses_through_collections() {
1046        static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1047
1048        let field = FieldModel::generated("payloads", BLOB_LIST);
1049
1050        assert!(
1051            field
1052                .validate_runtime_value_for_storage(&Value::List(vec![
1053                    Value::Blob(vec![1, 2, 3]),
1054                    Value::Blob(vec![4, 5]),
1055                ]))
1056                .is_ok()
1057        );
1058        assert!(
1059            field
1060                .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1061                    1, 2, 3, 4
1062                ])]))
1063                .is_err()
1064        );
1065    }
1066}