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/// RelationStrength
519///
520/// Explicit relation intent for save-time referential integrity.
521///
522
523#[derive(Clone, Copy, Debug, Eq, PartialEq)]
524pub enum RelationStrength {
525    Strong,
526    Weak,
527}
528
529///
530/// FieldKind
531///
532/// Minimal runtime type surface needed by planning, validation, and execution.
533///
534/// This is aligned with `Value` variants and intentionally lossy: it encodes
535/// only the shape required for predicate compatibility and index planning.
536///
537
538#[derive(Clone, Copy, Debug)]
539pub enum FieldKind {
540    // Scalar primitives
541    Account,
542    Blob {
543        /// Optional schema-declared maximum byte length for blob fields.
544        max_len: Option<u32>,
545    },
546    Bool,
547    Date,
548    Decimal {
549        /// Required schema-declared fractional scale for decimal fields.
550        scale: u32,
551    },
552    Duration,
553    Enum {
554        /// Fully-qualified enum type path used for strict filter normalization.
555        path: &'static str,
556        /// Declared per-variant payload decode metadata.
557        variants: &'static [EnumVariantModel],
558    },
559    Float32,
560    Float64,
561    Int8,
562    Int16,
563    Int32,
564    Int64,
565    Int128,
566    IntBig {
567        /// Maximum accepted persisted payload byte length.
568        max_bytes: u32,
569    },
570    Principal,
571    Subaccount,
572    Text {
573        /// Optional schema-declared maximum Unicode scalar count for text fields.
574        max_len: Option<u32>,
575    },
576    Timestamp,
577    Nat8,
578    Nat16,
579    Nat32,
580    Nat64,
581    Nat128,
582    NatBig {
583        /// Maximum accepted persisted payload byte length.
584        max_bytes: u32,
585    },
586    Ulid,
587    Unit,
588
589    /// Typed relation; `key_kind` reflects the referenced key type.
590    /// `strength` encodes strong vs. weak relation intent.
591    Relation {
592        /// Fully-qualified Rust type path for diagnostics.
593        target_path: &'static str,
594        /// Stable external name used in storage keys.
595        target_entity_name: &'static str,
596        /// Stable runtime identity used on hot execution paths.
597        target_entity_tag: EntityTag,
598        /// Data store path where the target entity is persisted.
599        target_store_path: &'static str,
600        key_kind: &'static Self,
601        strength: RelationStrength,
602    },
603
604    // Collections
605    List(&'static Self),
606    Set(&'static Self),
607    /// Deterministic, unordered key/value collection.
608    ///
609    /// Map fields are persistable and patchable, but not queryable or indexable.
610    Map {
611        key: &'static Self,
612        value: &'static Self,
613    },
614
615    /// Structured (non-atomic) value.
616    /// Queryability here controls whether predicates may target this field,
617    /// not whether it may be stored or updated.
618    Structured {
619        queryable: bool,
620    },
621}
622
623impl FieldKind {
624    #[must_use]
625    pub const fn value_kind(&self) -> RuntimeValueKind {
626        match self {
627            Self::Account
628            | Self::Blob { .. }
629            | Self::Bool
630            | Self::Date
631            | Self::Duration
632            | Self::Enum { .. }
633            | Self::Float32
634            | Self::Float64
635            | Self::Int8
636            | Self::Int16
637            | Self::Int32
638            | Self::Int64
639            | Self::Int128
640            | Self::IntBig { .. }
641            | Self::Principal
642            | Self::Subaccount
643            | Self::Text { .. }
644            | Self::Timestamp
645            | Self::Nat8
646            | Self::Nat16
647            | Self::Nat32
648            | Self::Nat64
649            | Self::Nat128
650            | Self::NatBig { .. }
651            | Self::Ulid
652            | Self::Unit
653            | Self::Decimal { .. }
654            | Self::Relation { .. } => RuntimeValueKind::Atomic,
655            Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
656            Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
657            Self::Structured { queryable } => RuntimeValueKind::Structured {
658                queryable: *queryable,
659            },
660        }
661    }
662
663    /// Returns `true` if this field shape is permitted in
664    /// persisted or query-visible schemas under the current
665    /// determinism policy.
666    ///
667    /// This shape-level check is structural only; query-time policy
668    /// enforcement (for example, map predicate fencing) is applied at
669    /// query construction and validation boundaries.
670    #[must_use]
671    pub const fn is_deterministic_collection_shape(&self) -> bool {
672        match self {
673            Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
674
675            Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
676
677            Self::Map { key, value } => {
678                key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
679            }
680
681            _ => true,
682        }
683    }
684
685    /// Return true when this planner-frozen grouped field kind can stay on the
686    /// borrowed grouped-key probe path without owned canonical materialization.
687    #[cfg(test)]
688    #[must_use]
689    pub(crate) fn supports_group_probe(&self) -> bool {
690        match self {
691            Self::Enum { variants, .. } => variants.iter().all(|variant| {
692                variant
693                    .payload_kind()
694                    .is_none_or(Self::supports_group_probe)
695            }),
696            Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
697            Self::Decimal { .. } => true,
698            _ => super::field_kind_semantics::field_kind_has_identity_group_canonical_form(*self),
699        }
700    }
701
702    /// Match one runtime value against generated model metadata in tests.
703    #[cfg(test)]
704    #[must_use]
705    pub(crate) fn accepts_value(&self, value: &Value) -> bool {
706        match (self, value) {
707            (Self::Account, Value::Account(_))
708            | (Self::Blob { .. }, Value::Blob(_))
709            | (Self::Bool, Value::Bool(_))
710            | (Self::Date, Value::Date(_))
711            | (Self::Decimal { .. }, Value::Decimal(_))
712            | (Self::Duration, Value::Duration(_))
713            | (Self::Enum { .. }, Value::Enum(_))
714            | (Self::Float32, Value::Float32(_))
715            | (Self::Float64, Value::Float64(_))
716            | (Self::Int64, Value::Int64(_))
717            | (Self::Int128, Value::Int128(_))
718            | (Self::Nat64, Value::Nat64(_))
719            | (Self::Principal, Value::Principal(_))
720            | (Self::Subaccount, Value::Subaccount(_))
721            | (Self::Text { .. }, Value::Text(_))
722            | (Self::Timestamp, Value::Timestamp(_))
723            | (Self::Nat128, Value::Nat128(_))
724            | (Self::Ulid, Value::Ulid(_))
725            | (Self::Unit, Value::Unit)
726            | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
727            (Self::Int8, Value::Int64(value)) => i8::try_from(*value).is_ok(),
728            (Self::Int16, Value::Int64(value)) => i16::try_from(*value).is_ok(),
729            (Self::Int32, Value::Int64(value)) => i32::try_from(*value).is_ok(),
730            (Self::Nat8, Value::Nat64(value)) => u8::try_from(*value).is_ok(),
731            (Self::Nat16, Value::Nat64(value)) => u16::try_from(*value).is_ok(),
732            (Self::Nat32, Value::Nat64(value)) => u32::try_from(*value).is_ok(),
733            (Self::IntBig { max_bytes }, Value::IntBig(value)) => {
734                value.to_leb128().len() <= *max_bytes as usize
735            }
736            (Self::NatBig { max_bytes }, Value::NatBig(value)) => {
737                value.to_leb128().len() <= *max_bytes as usize
738            }
739            (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
740            (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
741                items.iter().all(|item| inner.accepts_value(item))
742            }
743            (Self::Map { key, value }, Value::Map(entries)) => {
744                Value::validate_map_entries(entries.as_slice()).is_ok()
745                    && entries.iter().all(|(entry_key, entry_value)| {
746                        key.accepts_value(entry_key) && value.accepts_value(entry_value)
747                    })
748            }
749            _ => false,
750        }
751    }
752}
753
754// `FieldStorageDecode::ByKind` follows the same literal compatibility rule as
755// the schema predicate layer without routing the storage model through
756// `db::schema`. Structured field kinds are intentionally not accepted here;
757// fields that persist open-ended structured payloads use
758// `FieldStorageDecode::Value` instead.
759#[cfg(test)]
760fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
761    match (kind, value) {
762        (FieldKind::Relation { key_kind, .. }, value) => {
763            by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
764        }
765        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
766            .iter()
767            .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
768        (
769            FieldKind::Map {
770                key,
771                value: value_kind,
772            },
773            Value::Map(entries),
774        ) => {
775            if Value::validate_map_entries(entries.as_slice()).is_err() {
776                return false;
777            }
778
779            entries.iter().all(|(entry_key, entry_value)| {
780                by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
781                    && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
782            })
783        }
784        (FieldKind::Structured { .. }, _) => false,
785        _ => kind.accepts_value(value),
786    }
787}
788
789// `FieldStorageDecode::Value` fields persist an opaque runtime `Value` envelope,
790// so `FieldKind::Structured` must stay open-ended while outer collection/map
791// shapes still enforce the recursive structure the model owns.
792#[cfg(test)]
793fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
794    match (kind, value) {
795        (FieldKind::Structured { .. }, _) => true,
796        (FieldKind::Relation { key_kind, .. }, value) => {
797            value_storage_kind_accepts_runtime_value(*key_kind, value)
798        }
799        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
800            .iter()
801            .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
802        (
803            FieldKind::Map {
804                key,
805                value: value_kind,
806            },
807            Value::Map(entries),
808        ) => {
809            if Value::validate_map_entries(entries.as_slice()).is_err() {
810                return false;
811            }
812
813            entries.iter().all(|(entry_key, entry_value)| {
814                value_storage_kind_accepts_runtime_value(*key, entry_key)
815                    && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
816            })
817        }
818        _ => kind.accepts_value(value),
819    }
820}
821
822// Enforce fixed decimal scales through nested collection/map shapes before a
823// field-level runtime value is persisted.
824#[cfg(test)]
825fn ensure_decimal_scale_matches(
826    kind: FieldKind,
827    value: &Value,
828) -> Result<(), FieldStorageValidationError> {
829    if matches!(value, Value::Null) {
830        return Ok(());
831    }
832
833    match (kind, value) {
834        (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
835            if decimal.scale() != scale {
836                return Err(FieldStorageValidationError::DecimalScaleMismatch);
837            }
838
839            Ok(())
840        }
841        (FieldKind::Relation { key_kind, .. }, value) => {
842            ensure_decimal_scale_matches(*key_kind, value)
843        }
844        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
845            for item in items {
846                ensure_decimal_scale_matches(*inner, item)?;
847            }
848
849            Ok(())
850        }
851        (
852            FieldKind::Map {
853                key,
854                value: map_value,
855            },
856            Value::Map(entries),
857        ) => {
858            for (entry_key, entry_value) in entries {
859                ensure_decimal_scale_matches(*key, entry_key)?;
860                ensure_decimal_scale_matches(*map_value, entry_value)?;
861            }
862
863            Ok(())
864        }
865        _ => Ok(()),
866    }
867}
868
869// Enforce bounded text/blob length through nested collection/map shapes before
870// a field-level runtime value is persisted.
871#[cfg(test)]
872fn ensure_scalar_max_len_matches(
873    kind: FieldKind,
874    value: &Value,
875) -> Result<(), FieldStorageValidationError> {
876    if matches!(value, Value::Null) {
877        return Ok(());
878    }
879
880    match (kind, value) {
881        (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
882            let len = text.chars().count();
883            if len > max as usize {
884                return Err(FieldStorageValidationError::ScalarMaxLen);
885            }
886
887            Ok(())
888        }
889        (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
890            let len = bytes.len();
891            if len > max as usize {
892                return Err(FieldStorageValidationError::ScalarMaxLen);
893            }
894
895            Ok(())
896        }
897        (FieldKind::Relation { key_kind, .. }, value) => {
898            ensure_scalar_max_len_matches(*key_kind, value)
899        }
900        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
901            for item in items {
902                ensure_scalar_max_len_matches(*inner, item)?;
903            }
904
905            Ok(())
906        }
907        (
908            FieldKind::Map {
909                key,
910                value: map_value,
911            },
912            Value::Map(entries),
913        ) => {
914            for (entry_key, entry_value) in entries {
915                ensure_scalar_max_len_matches(*key, entry_key)?;
916                ensure_scalar_max_len_matches(*map_value, entry_value)?;
917            }
918
919            Ok(())
920        }
921        _ => Ok(()),
922    }
923}
924
925// Enforce the canonical persisted ordering rules for set/map shapes before one
926// field-level runtime value becomes row bytes.
927#[cfg(test)]
928fn ensure_value_is_deterministic_for_storage(
929    kind: FieldKind,
930    value: &Value,
931) -> Result<(), FieldStorageValidationError> {
932    match (kind, value) {
933        (FieldKind::Set(_), Value::List(items)) => {
934            for pair in items.windows(2) {
935                let [left, right] = pair else {
936                    continue;
937                };
938                if Value::canonical_cmp(left, right) != Ordering::Less {
939                    return Err(FieldStorageValidationError::SetCanonicalOrder);
940                }
941            }
942
943            Ok(())
944        }
945        (FieldKind::Map { .. }, Value::Map(entries)) => {
946            Value::validate_map_entries(entries.as_slice())
947                .map_err(|_| FieldStorageValidationError::MapEntryContract)?;
948
949            if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
950                return Err(FieldStorageValidationError::MapCanonicalOrder);
951            }
952
953            Ok(())
954        }
955        _ => Ok(()),
956    }
957}
958
959///
960/// TESTS
961///
962
963#[cfg(test)]
964mod tests {
965    use crate::{
966        model::field::{FieldKind, FieldModel},
967        value::Value,
968    };
969
970    static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
971    static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
972
973    #[test]
974    fn text_max_len_accepts_unbounded_text() {
975        let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
976
977        assert!(
978            field
979                .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
980                .is_ok()
981        );
982    }
983
984    #[test]
985    fn text_max_len_counts_unicode_scalars_not_bytes() {
986        let field = FieldModel::generated("name", BOUNDED_TEXT);
987
988        assert!(
989            field
990                .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
991                .is_ok()
992        );
993        assert!(
994            field
995                .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
996                .is_err()
997        );
998    }
999
1000    #[test]
1001    fn text_max_len_recurses_through_collections() {
1002        static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1003        static TEXT_MAP: FieldKind = FieldKind::Map {
1004            key: &BOUNDED_TEXT,
1005            value: &BOUNDED_TEXT,
1006        };
1007
1008        let list_field = FieldModel::generated("names", TEXT_LIST);
1009        let map_field = FieldModel::generated("labels", TEXT_MAP);
1010
1011        assert!(
1012            list_field
1013                .validate_runtime_value_for_storage(&Value::List(vec![
1014                    Value::Text("Ada".into()),
1015                    Value::Text("Bob".into()),
1016                ]))
1017                .is_ok()
1018        );
1019        assert!(
1020            list_field
1021                .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1022                .is_err()
1023        );
1024        assert!(
1025            map_field
1026                .validate_runtime_value_for_storage(&Value::Map(vec![(
1027                    Value::Text("key".into()),
1028                    Value::Text("val".into()),
1029                )]))
1030                .is_ok()
1031        );
1032        assert!(
1033            map_field
1034                .validate_runtime_value_for_storage(&Value::Map(vec![(
1035                    Value::Text("long".into()),
1036                    Value::Text("val".into()),
1037                )]))
1038                .is_err()
1039        );
1040    }
1041
1042    #[test]
1043    fn blob_max_len_counts_bytes() {
1044        let field = FieldModel::generated("payload", BOUNDED_BLOB);
1045
1046        assert!(
1047            field
1048                .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1049                .is_ok()
1050        );
1051        assert!(
1052            field
1053                .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1054                .is_err()
1055        );
1056    }
1057
1058    #[test]
1059    fn blob_max_len_recurses_through_collections() {
1060        static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1061
1062        let field = FieldModel::generated("payloads", BLOB_LIST);
1063
1064        assert!(
1065            field
1066                .validate_runtime_value_for_storage(&Value::List(vec![
1067                    Value::Blob(vec![1, 2, 3]),
1068                    Value::Blob(vec![4, 5]),
1069                ]))
1070                .is_ok()
1071        );
1072        assert!(
1073            field
1074                .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1075                    1, 2, 3, 4
1076                ])]))
1077                .is_err()
1078        );
1079    }
1080}