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::{traits::RuntimeValueKind, types::EntityTag};
7#[cfg(test)]
8use crate::{types::Decimal, value::Value};
9#[cfg(test)]
10use std::{borrow::Cow, 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    // Normalize decimal payloads to this field's fixed scale before storage
475    // encoding. Validation still runs after this step, so malformed shapes and
476    // deterministic collection rules remain owned by the normal field contract.
477    #[cfg(test)]
478    pub(crate) fn normalize_runtime_value_for_storage<'a>(
479        &self,
480        value: &'a Value,
481    ) -> Result<Cow<'a, Value>, FieldStorageValidationError> {
482        normalize_decimal_scale_for_storage(self.kind(), value)
483    }
484}
485
486// Resolve the canonical leaf codec from semantic field kind plus storage
487// contract. Fields that intentionally persist as `Value` or that still require
488// recursive payload decoding remain on the shared structural fallback.
489const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
490    if matches!(storage_decode, FieldStorageDecode::Value) {
491        return LeafCodec::StructuralFallback;
492    }
493
494    match kind {
495        FieldKind::Blob { .. } => LeafCodec::Scalar(ScalarCodec::Blob),
496        FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
497        FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
498        FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
499        FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
500        FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
501        FieldKind::Int8 | FieldKind::Int16 | FieldKind::Int32 | FieldKind::Int64 => {
502            LeafCodec::Scalar(ScalarCodec::Int64)
503        }
504        FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
505        FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
506        FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
507        FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
508        FieldKind::Nat8 | FieldKind::Nat16 | FieldKind::Nat32 | FieldKind::Nat64 => {
509            LeafCodec::Scalar(ScalarCodec::Nat64)
510        }
511        FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
512        FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
513        FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
514        FieldKind::Account
515        | FieldKind::Decimal { .. }
516        | FieldKind::Enum { .. }
517        | FieldKind::Int128
518        | FieldKind::IntBig { .. }
519        | FieldKind::List(_)
520        | FieldKind::Map { .. }
521        | FieldKind::Set(_)
522        | FieldKind::Structured { .. }
523        | FieldKind::Nat128
524        | FieldKind::NatBig { .. } => LeafCodec::StructuralFallback,
525    }
526}
527
528///
529/// RelationStrength
530///
531/// Explicit relation intent for save-time referential integrity.
532///
533
534#[derive(Clone, Copy, Debug, Eq, PartialEq)]
535pub enum RelationStrength {
536    Strong,
537    Weak,
538}
539
540///
541/// FieldKind
542///
543/// Minimal runtime type surface needed by planning, validation, and execution.
544///
545/// This is aligned with `Value` variants and intentionally lossy: it encodes
546/// only the shape required for predicate compatibility and index planning.
547///
548
549#[derive(Clone, Copy, Debug)]
550pub enum FieldKind {
551    // Scalar primitives
552    Account,
553    Blob {
554        /// Optional schema-declared maximum byte length for blob fields.
555        max_len: Option<u32>,
556    },
557    Bool,
558    Date,
559    Decimal {
560        /// Required schema-declared fractional scale for decimal fields.
561        scale: u32,
562    },
563    Duration,
564    Enum {
565        /// Fully-qualified enum type path used for strict filter normalization.
566        path: &'static str,
567        /// Declared per-variant payload decode metadata.
568        variants: &'static [EnumVariantModel],
569    },
570    Float32,
571    Float64,
572    Int8,
573    Int16,
574    Int32,
575    Int64,
576    Int128,
577    IntBig {
578        /// Maximum accepted persisted payload byte length.
579        max_bytes: u32,
580    },
581    Principal,
582    Subaccount,
583    Text {
584        /// Optional schema-declared maximum Unicode scalar count for text fields.
585        max_len: Option<u32>,
586    },
587    Timestamp,
588    Nat8,
589    Nat16,
590    Nat32,
591    Nat64,
592    Nat128,
593    NatBig {
594        /// Maximum accepted persisted payload byte length.
595        max_bytes: u32,
596    },
597    Ulid,
598    Unit,
599
600    /// Typed relation; `key_kind` reflects the referenced key type.
601    /// `strength` encodes strong vs. weak relation intent.
602    Relation {
603        /// Fully-qualified Rust type path for diagnostics.
604        target_path: &'static str,
605        /// Stable external name used in storage keys.
606        target_entity_name: &'static str,
607        /// Stable runtime identity used on hot execution paths.
608        target_entity_tag: EntityTag,
609        /// Data store path where the target entity is persisted.
610        target_store_path: &'static str,
611        key_kind: &'static Self,
612        strength: RelationStrength,
613    },
614
615    // Collections
616    List(&'static Self),
617    Set(&'static Self),
618    /// Deterministic, unordered key/value collection.
619    ///
620    /// Map fields are persistable and patchable, but not queryable or indexable.
621    Map {
622        key: &'static Self,
623        value: &'static Self,
624    },
625
626    /// Structured (non-atomic) value.
627    /// Queryability here controls whether predicates may target this field,
628    /// not whether it may be stored or updated.
629    Structured {
630        queryable: bool,
631    },
632}
633
634impl FieldKind {
635    #[must_use]
636    pub const fn value_kind(&self) -> RuntimeValueKind {
637        match self {
638            Self::Account
639            | Self::Blob { .. }
640            | Self::Bool
641            | Self::Date
642            | Self::Duration
643            | Self::Enum { .. }
644            | Self::Float32
645            | Self::Float64
646            | Self::Int8
647            | Self::Int16
648            | Self::Int32
649            | Self::Int64
650            | Self::Int128
651            | Self::IntBig { .. }
652            | Self::Principal
653            | Self::Subaccount
654            | Self::Text { .. }
655            | Self::Timestamp
656            | Self::Nat8
657            | Self::Nat16
658            | Self::Nat32
659            | Self::Nat64
660            | Self::Nat128
661            | Self::NatBig { .. }
662            | Self::Ulid
663            | Self::Unit
664            | Self::Decimal { .. }
665            | Self::Relation { .. } => RuntimeValueKind::Atomic,
666            Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
667            Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
668            Self::Structured { queryable } => RuntimeValueKind::Structured {
669                queryable: *queryable,
670            },
671        }
672    }
673
674    /// Returns `true` if this field shape is permitted in
675    /// persisted or query-visible schemas under the current
676    /// determinism policy.
677    ///
678    /// This shape-level check is structural only; query-time policy
679    /// enforcement (for example, map predicate fencing) is applied at
680    /// query construction and validation boundaries.
681    #[must_use]
682    pub const fn is_deterministic_collection_shape(&self) -> bool {
683        match self {
684            Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
685
686            Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
687
688            Self::Map { key, value } => {
689                key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
690            }
691
692            _ => true,
693        }
694    }
695
696    /// Return true when this planner-frozen grouped field kind can stay on the
697    /// borrowed grouped-key probe path without owned canonical materialization.
698    #[cfg(test)]
699    #[must_use]
700    pub(crate) fn supports_group_probe(&self) -> bool {
701        match self {
702            Self::Enum { variants, .. } => variants.iter().all(|variant| {
703                variant
704                    .payload_kind()
705                    .is_none_or(Self::supports_group_probe)
706            }),
707            Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
708            Self::Decimal { .. } => true,
709            _ => super::field_kind_semantics::field_kind_has_identity_group_canonical_form(*self),
710        }
711    }
712
713    /// Match one runtime value against generated model metadata in tests.
714    #[cfg(test)]
715    #[must_use]
716    pub(crate) fn accepts_value(&self, value: &Value) -> bool {
717        match (self, value) {
718            (Self::Account, Value::Account(_))
719            | (Self::Blob { .. }, Value::Blob(_))
720            | (Self::Bool, Value::Bool(_))
721            | (Self::Date, Value::Date(_))
722            | (Self::Decimal { .. }, Value::Decimal(_))
723            | (Self::Duration, Value::Duration(_))
724            | (Self::Enum { .. }, Value::Enum(_))
725            | (Self::Float32, Value::Float32(_))
726            | (Self::Float64, Value::Float64(_))
727            | (Self::Int64, Value::Int64(_))
728            | (Self::Int128, Value::Int128(_))
729            | (Self::Nat64, Value::Nat64(_))
730            | (Self::Principal, Value::Principal(_))
731            | (Self::Subaccount, Value::Subaccount(_))
732            | (Self::Text { .. }, Value::Text(_))
733            | (Self::Timestamp, Value::Timestamp(_))
734            | (Self::Nat128, Value::Nat128(_))
735            | (Self::Ulid, Value::Ulid(_))
736            | (Self::Unit, Value::Unit)
737            | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
738            (Self::Int8, Value::Int64(value)) => i8::try_from(*value).is_ok(),
739            (Self::Int16, Value::Int64(value)) => i16::try_from(*value).is_ok(),
740            (Self::Int32, Value::Int64(value)) => i32::try_from(*value).is_ok(),
741            (Self::Nat8, Value::Nat64(value)) => u8::try_from(*value).is_ok(),
742            (Self::Nat16, Value::Nat64(value)) => u16::try_from(*value).is_ok(),
743            (Self::Nat32, Value::Nat64(value)) => u32::try_from(*value).is_ok(),
744            (Self::IntBig { max_bytes }, Value::IntBig(value)) => {
745                value.to_leb128().len() <= *max_bytes as usize
746            }
747            (Self::NatBig { max_bytes }, Value::NatBig(value)) => {
748                value.to_leb128().len() <= *max_bytes as usize
749            }
750            (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
751            (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
752                items.iter().all(|item| inner.accepts_value(item))
753            }
754            (Self::Map { key, value }, Value::Map(entries)) => {
755                Value::validate_map_entries(entries.as_slice()).is_ok()
756                    && entries.iter().all(|(entry_key, entry_value)| {
757                        key.accepts_value(entry_key) && value.accepts_value(entry_value)
758                    })
759            }
760            _ => false,
761        }
762    }
763}
764
765// `FieldStorageDecode::ByKind` follows the same literal compatibility rule as
766// the schema predicate layer without routing the storage model through
767// `db::schema`. Structured field kinds are intentionally not accepted here;
768// fields that persist open-ended structured payloads use
769// `FieldStorageDecode::Value` instead.
770#[cfg(test)]
771fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
772    match (kind, value) {
773        (FieldKind::Relation { key_kind, .. }, value) => {
774            by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
775        }
776        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
777            .iter()
778            .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
779        (
780            FieldKind::Map {
781                key,
782                value: value_kind,
783            },
784            Value::Map(entries),
785        ) => {
786            if Value::validate_map_entries(entries.as_slice()).is_err() {
787                return false;
788            }
789
790            entries.iter().all(|(entry_key, entry_value)| {
791                by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
792                    && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
793            })
794        }
795        (FieldKind::Structured { .. }, _) => false,
796        _ => kind.accepts_value(value),
797    }
798}
799
800// `FieldStorageDecode::Value` fields persist an opaque runtime `Value` envelope,
801// so `FieldKind::Structured` must stay open-ended while outer collection/map
802// shapes still enforce the recursive structure the model owns.
803#[cfg(test)]
804fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
805    match (kind, value) {
806        (FieldKind::Structured { .. }, _) => true,
807        (FieldKind::Relation { key_kind, .. }, value) => {
808            value_storage_kind_accepts_runtime_value(*key_kind, value)
809        }
810        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
811            .iter()
812            .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
813        (
814            FieldKind::Map {
815                key,
816                value: value_kind,
817            },
818            Value::Map(entries),
819        ) => {
820            if Value::validate_map_entries(entries.as_slice()).is_err() {
821                return false;
822            }
823
824            entries.iter().all(|(entry_key, entry_value)| {
825                value_storage_kind_accepts_runtime_value(*key, entry_key)
826                    && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
827            })
828        }
829        _ => kind.accepts_value(value),
830    }
831}
832
833// Enforce fixed decimal scales through nested collection/map shapes before a
834// field-level runtime value is persisted.
835#[cfg(test)]
836fn ensure_decimal_scale_matches(
837    kind: FieldKind,
838    value: &Value,
839) -> Result<(), FieldStorageValidationError> {
840    if matches!(value, Value::Null) {
841        return Ok(());
842    }
843
844    match (kind, value) {
845        (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
846            if decimal.scale() != scale {
847                return Err(FieldStorageValidationError::DecimalScaleMismatch);
848            }
849
850            Ok(())
851        }
852        (FieldKind::Relation { key_kind, .. }, value) => {
853            ensure_decimal_scale_matches(*key_kind, value)
854        }
855        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
856            for item in items {
857                ensure_decimal_scale_matches(*inner, item)?;
858            }
859
860            Ok(())
861        }
862        (
863            FieldKind::Map {
864                key,
865                value: map_value,
866            },
867            Value::Map(entries),
868        ) => {
869            for (entry_key, entry_value) in entries {
870                ensure_decimal_scale_matches(*key, entry_key)?;
871                ensure_decimal_scale_matches(*map_value, entry_value)?;
872            }
873
874            Ok(())
875        }
876        _ => Ok(()),
877    }
878}
879
880/// Normalize fixed-scale decimal values through nested collection/map shapes
881/// before the field-level payload is encoded. This is write-side
882/// canonicalization; callers that validate already persisted bytes still use
883/// the exact scale check.
884#[cfg(test)]
885pub(crate) fn normalize_decimal_scale_for_storage(
886    kind: FieldKind,
887    value: &Value,
888) -> Result<Cow<'_, Value>, FieldStorageValidationError> {
889    if matches!(value, Value::Null) {
890        return Ok(Cow::Borrowed(value));
891    }
892
893    match (kind, value) {
894        (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
895            let normalized = decimal_with_storage_scale(*decimal, scale)
896                .ok_or(FieldStorageValidationError::DecimalScaleMismatch)?;
897
898            if normalized.scale() == decimal.scale() {
899                Ok(Cow::Borrowed(value))
900            } else {
901                Ok(Cow::Owned(Value::Decimal(normalized)))
902            }
903        }
904        (FieldKind::Relation { key_kind, .. }, value) => {
905            normalize_decimal_scale_for_storage(*key_kind, value)
906        }
907        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
908            normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
909                items.map_or_else(
910                    || Cow::Borrowed(value),
911                    |items| Cow::Owned(Value::List(items)),
912                )
913            })
914        }
915        (
916            FieldKind::Map {
917                key,
918                value: map_value,
919            },
920            Value::Map(entries),
921        ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
922            entries.map_or_else(
923                || Cow::Borrowed(value),
924                |entries| Cow::Owned(Value::Map(entries)),
925            )
926        }),
927        _ => Ok(Cow::Borrowed(value)),
928    }
929}
930
931// Convert one decimal into the exact storage scale. Lower-scale values are
932// padded without changing their numeric value; higher-scale values use the same
933// round-half-away-from-zero policy as SQL/fluent decimal rounding.
934#[cfg(test)]
935fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
936    match decimal.scale().cmp(&scale) {
937        Ordering::Equal => Some(decimal),
938        Ordering::Less => decimal
939            .scale_to_integer(scale)
940            .and_then(|mantissa| Decimal::try_from_i128_with_scale(mantissa, scale)),
941        Ordering::Greater => Some(decimal.round_dp(scale)),
942    }
943}
944
945// Normalize decimal items while preserving the original list allocation when
946// every item is already canonical for its nested field kind.
947#[cfg(test)]
948fn normalize_decimal_list_items(
949    kind: FieldKind,
950    items: &[Value],
951) -> Result<Option<Vec<Value>>, FieldStorageValidationError> {
952    let mut normalized_items = None;
953
954    for (index, item) in items.iter().enumerate() {
955        let normalized = normalize_decimal_scale_for_storage(kind, item)?;
956        if let Cow::Owned(value) = normalized {
957            let items = normalized_items.get_or_insert_with(|| items.to_vec());
958            items[index] = value;
959        }
960    }
961
962    Ok(normalized_items)
963}
964
965// Normalize decimal keys and values while preserving the original map
966// allocation when every entry is already canonical for its nested field kind.
967#[cfg(test)]
968fn normalize_decimal_map_entries(
969    key_kind: FieldKind,
970    value_kind: FieldKind,
971    entries: &[(Value, Value)],
972) -> Result<Option<Vec<(Value, Value)>>, FieldStorageValidationError> {
973    let mut normalized_entries = None;
974
975    for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
976        let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
977        let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
978
979        if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
980            let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
981            if let Cow::Owned(value) = normalized_key {
982                entries[index].0 = value;
983            }
984            if let Cow::Owned(value) = normalized_value {
985                entries[index].1 = value;
986            }
987        }
988    }
989
990    Ok(normalized_entries)
991}
992
993// Enforce bounded text/blob length through nested collection/map shapes before
994// a field-level runtime value is persisted.
995#[cfg(test)]
996fn ensure_scalar_max_len_matches(
997    kind: FieldKind,
998    value: &Value,
999) -> Result<(), FieldStorageValidationError> {
1000    if matches!(value, Value::Null) {
1001        return Ok(());
1002    }
1003
1004    match (kind, value) {
1005        (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
1006            let len = text.chars().count();
1007            if len > max as usize {
1008                return Err(FieldStorageValidationError::ScalarMaxLen);
1009            }
1010
1011            Ok(())
1012        }
1013        (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
1014            let len = bytes.len();
1015            if len > max as usize {
1016                return Err(FieldStorageValidationError::ScalarMaxLen);
1017            }
1018
1019            Ok(())
1020        }
1021        (FieldKind::Relation { key_kind, .. }, value) => {
1022            ensure_scalar_max_len_matches(*key_kind, value)
1023        }
1024        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
1025            for item in items {
1026                ensure_scalar_max_len_matches(*inner, item)?;
1027            }
1028
1029            Ok(())
1030        }
1031        (
1032            FieldKind::Map {
1033                key,
1034                value: map_value,
1035            },
1036            Value::Map(entries),
1037        ) => {
1038            for (entry_key, entry_value) in entries {
1039                ensure_scalar_max_len_matches(*key, entry_key)?;
1040                ensure_scalar_max_len_matches(*map_value, entry_value)?;
1041            }
1042
1043            Ok(())
1044        }
1045        _ => Ok(()),
1046    }
1047}
1048
1049// Enforce the canonical persisted ordering rules for set/map shapes before one
1050// field-level runtime value becomes row bytes.
1051#[cfg(test)]
1052fn ensure_value_is_deterministic_for_storage(
1053    kind: FieldKind,
1054    value: &Value,
1055) -> Result<(), FieldStorageValidationError> {
1056    match (kind, value) {
1057        (FieldKind::Set(_), Value::List(items)) => {
1058            for pair in items.windows(2) {
1059                let [left, right] = pair else {
1060                    continue;
1061                };
1062                if Value::canonical_cmp(left, right) != Ordering::Less {
1063                    return Err(FieldStorageValidationError::SetCanonicalOrder);
1064                }
1065            }
1066
1067            Ok(())
1068        }
1069        (FieldKind::Map { .. }, Value::Map(entries)) => {
1070            Value::validate_map_entries(entries.as_slice())
1071                .map_err(|_| FieldStorageValidationError::MapEntryContract)?;
1072
1073            if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1074                return Err(FieldStorageValidationError::MapCanonicalOrder);
1075            }
1076
1077            Ok(())
1078        }
1079        _ => Ok(()),
1080    }
1081}
1082
1083///
1084/// TESTS
1085///
1086
1087#[cfg(test)]
1088mod tests {
1089    use crate::{
1090        model::field::{FieldKind, FieldModel},
1091        value::Value,
1092    };
1093
1094    static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1095    static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
1096
1097    #[test]
1098    fn text_max_len_accepts_unbounded_text() {
1099        let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1100
1101        assert!(
1102            field
1103                .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1104                .is_ok()
1105        );
1106    }
1107
1108    #[test]
1109    fn text_max_len_counts_unicode_scalars_not_bytes() {
1110        let field = FieldModel::generated("name", BOUNDED_TEXT);
1111
1112        assert!(
1113            field
1114                .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1115                .is_ok()
1116        );
1117        assert!(
1118            field
1119                .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1120                .is_err()
1121        );
1122    }
1123
1124    #[test]
1125    fn text_max_len_recurses_through_collections() {
1126        static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1127        static TEXT_MAP: FieldKind = FieldKind::Map {
1128            key: &BOUNDED_TEXT,
1129            value: &BOUNDED_TEXT,
1130        };
1131
1132        let list_field = FieldModel::generated("names", TEXT_LIST);
1133        let map_field = FieldModel::generated("labels", TEXT_MAP);
1134
1135        assert!(
1136            list_field
1137                .validate_runtime_value_for_storage(&Value::List(vec![
1138                    Value::Text("Ada".into()),
1139                    Value::Text("Bob".into()),
1140                ]))
1141                .is_ok()
1142        );
1143        assert!(
1144            list_field
1145                .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1146                .is_err()
1147        );
1148        assert!(
1149            map_field
1150                .validate_runtime_value_for_storage(&Value::Map(vec![(
1151                    Value::Text("key".into()),
1152                    Value::Text("val".into()),
1153                )]))
1154                .is_ok()
1155        );
1156        assert!(
1157            map_field
1158                .validate_runtime_value_for_storage(&Value::Map(vec![(
1159                    Value::Text("long".into()),
1160                    Value::Text("val".into()),
1161                )]))
1162                .is_err()
1163        );
1164    }
1165
1166    #[test]
1167    fn blob_max_len_counts_bytes() {
1168        let field = FieldModel::generated("payload", BOUNDED_BLOB);
1169
1170        assert!(
1171            field
1172                .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1173                .is_ok()
1174        );
1175        assert!(
1176            field
1177                .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1178                .is_err()
1179        );
1180    }
1181
1182    #[test]
1183    fn blob_max_len_recurses_through_collections() {
1184        static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1185
1186        let field = FieldModel::generated("payloads", BLOB_LIST);
1187
1188        assert!(
1189            field
1190                .validate_runtime_value_for_storage(&Value::List(vec![
1191                    Value::Blob(vec![1, 2, 3]),
1192                    Value::Blob(vec![4, 5]),
1193                ]))
1194                .is_ok()
1195        );
1196        assert!(
1197            field
1198                .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1199                    1, 2, 3, 4
1200                ])]))
1201                .is_err()
1202        );
1203    }
1204}