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, value::Value};
7use std::cmp::Ordering;
8
9///
10/// FieldStorageDecode
11///
12/// FieldStorageDecode captures how one persisted field payload must be
13/// interpreted at structural decode boundaries.
14/// Semantic `FieldKind` alone is not always authoritative for persisted decode:
15/// some fields intentionally store raw `Value` payloads even when their planner
16/// shape is narrower.
17///
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum FieldStorageDecode {
21    /// Decode the persisted field payload according to semantic `FieldKind`.
22    ByKind,
23    /// Decode the persisted field payload directly into `Value`.
24    Value,
25}
26
27///
28/// ScalarCodec
29///
30/// ScalarCodec identifies the canonical binary leaf encoding used for one
31/// scalar persisted field payload.
32/// These codecs are fixed-width or span-bounded by the surrounding row slot
33/// container; they do not perform map/array/value dispatch.
34///
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum ScalarCodec {
38    Blob,
39    Bool,
40    Date,
41    Duration,
42    Float32,
43    Float64,
44    Int64,
45    Principal,
46    Subaccount,
47    Text,
48    Timestamp,
49    Uint64,
50    Ulid,
51    Unit,
52}
53
54///
55/// LeafCodec
56///
57/// LeafCodec declares whether one persisted field payload uses a dedicated
58/// scalar codec or falls back to structural leaf decoding.
59/// The row container consults this metadata before deciding whether a slot can
60/// stay on the scalar fast path.
61///
62
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub enum LeafCodec {
65    Scalar(ScalarCodec),
66    StructuralFallback,
67}
68
69///
70/// EnumVariantModel
71///
72/// EnumVariantModel carries structural decode metadata for one generated enum
73/// variant payload.
74/// Runtime structural decode uses this to stay on the field-kind contract for
75/// enum payloads instead of falling back to generic untyped structural decode.
76///
77
78#[derive(Clone, Copy, Debug)]
79pub struct EnumVariantModel {
80    /// Stable schema variant tag.
81    pub(crate) ident: &'static str,
82    /// Declared payload kind when this variant carries data.
83    pub(crate) payload_kind: Option<&'static FieldKind>,
84    /// Persisted payload decode contract for the carried data.
85    pub(crate) payload_storage_decode: FieldStorageDecode,
86}
87
88impl EnumVariantModel {
89    /// Build one enum variant structural decode descriptor.
90    #[must_use]
91    pub const fn new(
92        ident: &'static str,
93        payload_kind: Option<&'static FieldKind>,
94        payload_storage_decode: FieldStorageDecode,
95    ) -> Self {
96        Self {
97            ident,
98            payload_kind,
99            payload_storage_decode,
100        }
101    }
102
103    /// Return the stable schema variant tag.
104    #[must_use]
105    pub const fn ident(&self) -> &'static str {
106        self.ident
107    }
108
109    /// Return the declared payload kind when this variant carries data.
110    #[must_use]
111    pub const fn payload_kind(&self) -> Option<&'static FieldKind> {
112        self.payload_kind
113    }
114
115    /// Return the persisted payload decode contract for this variant.
116    #[must_use]
117    pub const fn payload_storage_decode(&self) -> FieldStorageDecode {
118        self.payload_storage_decode
119    }
120}
121
122///
123/// FieldModel
124///
125/// Runtime field metadata surfaced by macro-generated `EntityModel` values.
126///
127/// This is the smallest unit consumed by predicate validation, planning,
128/// and executor-side plan checks.
129///
130
131#[derive(Debug)]
132pub struct FieldModel {
133    /// Field name as used in predicates and indexing.
134    pub(crate) name: &'static str,
135    /// Runtime type shape (no schema-layer graph nodes).
136    pub(crate) kind: FieldKind,
137    /// Whether the field may persist an explicit `NULL` payload.
138    pub(crate) nullable: bool,
139    /// Persisted field decode contract used by structural runtime decoders.
140    pub(crate) storage_decode: FieldStorageDecode,
141    /// Leaf payload codec used by slot readers and writers.
142    pub(crate) leaf_codec: LeafCodec,
143    /// Insert-time generation contract admitted on reduced SQL write lanes.
144    pub(crate) insert_generation: Option<FieldInsertGeneration>,
145    /// Auto-managed write contract emitted for derive-owned system fields.
146    pub(crate) write_management: Option<FieldWriteManagement>,
147}
148
149///
150/// FieldInsertGeneration
151///
152/// FieldInsertGeneration declares whether one runtime field may be synthesized
153/// by the reduced SQL insert boundary when the user omits that field.
154/// This stays separate from typed-Rust `Default` behavior so write-time
155/// generation remains an explicit schema contract.
156///
157
158#[derive(Clone, Copy, Debug, Eq, PartialEq)]
159pub enum FieldInsertGeneration {
160    /// Generate one fresh `Ulid` value at insert time.
161    Ulid,
162    /// Generate one current wall-clock `Timestamp` value at insert time.
163    Timestamp,
164}
165
166///
167/// FieldWriteManagement
168///
169/// FieldWriteManagement declares whether one runtime field is owned by the
170/// write boundary during insert or update synthesis.
171/// This keeps auto-managed system fields explicit in schema/runtime metadata
172/// instead of relying on literal field names in write paths.
173///
174
175#[derive(Clone, Copy, Debug, Eq, PartialEq)]
176pub enum FieldWriteManagement {
177    /// Fill only on insert when the row is first created.
178    CreatedAt,
179    /// Refresh on insert and every update.
180    UpdatedAt,
181}
182
183impl FieldModel {
184    /// Build one generated runtime field descriptor.
185    ///
186    /// This constructor exists for derive/codegen output and trusted test
187    /// fixtures. Runtime planning and execution treat `FieldModel` values as
188    /// build-time-validated metadata.
189    #[must_use]
190    #[doc(hidden)]
191    pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
192        Self::generated_with_storage_decode_and_nullability(
193            name,
194            kind,
195            FieldStorageDecode::ByKind,
196            false,
197        )
198    }
199
200    /// Build one runtime field descriptor with an explicit persisted decode contract.
201    #[must_use]
202    #[doc(hidden)]
203    pub const fn generated_with_storage_decode(
204        name: &'static str,
205        kind: FieldKind,
206        storage_decode: FieldStorageDecode,
207    ) -> Self {
208        Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
209    }
210
211    /// Build one runtime field descriptor with an explicit decode contract and nullability.
212    #[must_use]
213    #[doc(hidden)]
214    pub const fn generated_with_storage_decode_and_nullability(
215        name: &'static str,
216        kind: FieldKind,
217        storage_decode: FieldStorageDecode,
218        nullable: bool,
219    ) -> Self {
220        Self::generated_with_storage_decode_nullability_and_write_policies(
221            name,
222            kind,
223            storage_decode,
224            nullable,
225            None,
226            None,
227        )
228    }
229
230    /// Build one runtime field descriptor with an explicit decode contract, nullability,
231    /// and insert-time generation contract.
232    #[must_use]
233    #[doc(hidden)]
234    pub const fn generated_with_storage_decode_nullability_and_insert_generation(
235        name: &'static str,
236        kind: FieldKind,
237        storage_decode: FieldStorageDecode,
238        nullable: bool,
239        insert_generation: Option<FieldInsertGeneration>,
240    ) -> Self {
241        Self::generated_with_storage_decode_nullability_and_write_policies(
242            name,
243            kind,
244            storage_decode,
245            nullable,
246            insert_generation,
247            None,
248        )
249    }
250
251    /// Build one runtime field descriptor with explicit insert-generation and
252    /// write-management policies.
253    #[must_use]
254    #[doc(hidden)]
255    pub const fn generated_with_storage_decode_nullability_and_write_policies(
256        name: &'static str,
257        kind: FieldKind,
258        storage_decode: FieldStorageDecode,
259        nullable: bool,
260        insert_generation: Option<FieldInsertGeneration>,
261        write_management: Option<FieldWriteManagement>,
262    ) -> Self {
263        Self {
264            name,
265            kind,
266            nullable,
267            storage_decode,
268            leaf_codec: leaf_codec_for(kind, storage_decode),
269            insert_generation,
270            write_management,
271        }
272    }
273
274    /// Return the stable field name.
275    #[must_use]
276    pub const fn name(&self) -> &'static str {
277        self.name
278    }
279
280    /// Return the runtime type-kind descriptor.
281    #[must_use]
282    pub const fn kind(&self) -> FieldKind {
283        self.kind
284    }
285
286    /// Return whether the persisted field contract permits explicit `NULL`.
287    #[must_use]
288    pub const fn nullable(&self) -> bool {
289        self.nullable
290    }
291
292    /// Return the persisted field decode contract.
293    #[must_use]
294    pub const fn storage_decode(&self) -> FieldStorageDecode {
295        self.storage_decode
296    }
297
298    /// Return the persisted leaf payload codec.
299    #[must_use]
300    pub const fn leaf_codec(&self) -> LeafCodec {
301        self.leaf_codec
302    }
303
304    /// Return the reduced-SQL insert-time generation contract for this field.
305    #[must_use]
306    pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
307        self.insert_generation
308    }
309
310    /// Return the write-boundary management contract for this field.
311    #[must_use]
312    pub const fn write_management(&self) -> Option<FieldWriteManagement> {
313        self.write_management
314    }
315
316    /// Validate one runtime value against this field's persisted storage contract.
317    ///
318    /// This is the model-owned compatibility gate used before row bytes are
319    /// emitted. It intentionally checks storage compatibility, not query
320    /// predicate compatibility, so `FieldStorageDecode::Value` can accept
321    /// open-ended structured payloads while still enforcing outer collection
322    /// shape, decimal scale, and deterministic set/map ordering.
323    pub(crate) fn validate_runtime_value_for_storage(&self, value: &Value) -> Result<(), String> {
324        if matches!(value, Value::Null) {
325            if self.nullable() {
326                return Ok(());
327            }
328
329            return Err("required field cannot store null".into());
330        }
331
332        let accepts = match self.storage_decode() {
333            FieldStorageDecode::Value => {
334                value_storage_kind_accepts_runtime_value(self.kind(), value)
335            }
336            FieldStorageDecode::ByKind => {
337                by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
338            }
339        };
340        if !accepts {
341            return Err(format!(
342                "field kind {:?} does not accept runtime value {value:?}",
343                self.kind()
344            ));
345        }
346
347        ensure_decimal_scale_matches(self.kind(), value)?;
348        ensure_value_is_deterministic_for_storage(self.kind(), value)
349    }
350}
351
352// Resolve the canonical leaf codec from semantic field kind plus storage
353// contract. Fields that intentionally persist as `Value` or that still require
354// recursive payload decoding remain on the shared structural fallback.
355const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
356    if matches!(storage_decode, FieldStorageDecode::Value) {
357        return LeafCodec::StructuralFallback;
358    }
359
360    match kind {
361        FieldKind::Blob => LeafCodec::Scalar(ScalarCodec::Blob),
362        FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
363        FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
364        FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
365        FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
366        FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
367        FieldKind::Int => LeafCodec::Scalar(ScalarCodec::Int64),
368        FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
369        FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
370        FieldKind::Text => LeafCodec::Scalar(ScalarCodec::Text),
371        FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
372        FieldKind::Uint => LeafCodec::Scalar(ScalarCodec::Uint64),
373        FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
374        FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
375        FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
376        FieldKind::Account
377        | FieldKind::Decimal { .. }
378        | FieldKind::Enum { .. }
379        | FieldKind::Int128
380        | FieldKind::IntBig
381        | FieldKind::List(_)
382        | FieldKind::Map { .. }
383        | FieldKind::Set(_)
384        | FieldKind::Structured { .. }
385        | FieldKind::Uint128
386        | FieldKind::UintBig => LeafCodec::StructuralFallback,
387    }
388}
389
390///
391/// RelationStrength
392///
393/// Explicit relation intent for save-time referential integrity.
394///
395
396#[derive(Clone, Copy, Debug, Eq, PartialEq)]
397pub enum RelationStrength {
398    Strong,
399    Weak,
400}
401
402///
403/// FieldKind
404///
405/// Minimal runtime type surface needed by planning, validation, and execution.
406///
407/// This is aligned with `Value` variants and intentionally lossy: it encodes
408/// only the shape required for predicate compatibility and index planning.
409///
410
411#[derive(Clone, Copy, Debug)]
412pub enum FieldKind {
413    // Scalar primitives
414    Account,
415    Blob,
416    Bool,
417    Date,
418    Decimal {
419        /// Required schema-declared fractional scale for decimal fields.
420        scale: u32,
421    },
422    Duration,
423    Enum {
424        /// Fully-qualified enum type path used for strict filter normalization.
425        path: &'static str,
426        /// Declared per-variant payload decode metadata.
427        variants: &'static [EnumVariantModel],
428    },
429    Float32,
430    Float64,
431    Int,
432    Int128,
433    IntBig,
434    Principal,
435    Subaccount,
436    Text,
437    Timestamp,
438    Uint,
439    Uint128,
440    UintBig,
441    Ulid,
442    Unit,
443
444    /// Typed relation; `key_kind` reflects the referenced key type.
445    /// `strength` encodes strong vs. weak relation intent.
446    Relation {
447        /// Fully-qualified Rust type path for diagnostics.
448        target_path: &'static str,
449        /// Stable external name used in storage keys.
450        target_entity_name: &'static str,
451        /// Stable runtime identity used on hot execution paths.
452        target_entity_tag: EntityTag,
453        /// Data store path where the target entity is persisted.
454        target_store_path: &'static str,
455        key_kind: &'static Self,
456        strength: RelationStrength,
457    },
458
459    // Collections
460    List(&'static Self),
461    Set(&'static Self),
462    /// Deterministic, unordered key/value collection.
463    ///
464    /// Map fields are persistable and patchable, but not queryable or indexable.
465    Map {
466        key: &'static Self,
467        value: &'static Self,
468    },
469
470    /// Structured (non-atomic) value.
471    /// Queryability here controls whether predicates may target this field,
472    /// not whether it may be stored or updated.
473    Structured {
474        queryable: bool,
475    },
476}
477
478impl FieldKind {
479    #[must_use]
480    pub const fn value_kind(&self) -> RuntimeValueKind {
481        match self {
482            Self::Account
483            | Self::Blob
484            | Self::Bool
485            | Self::Date
486            | Self::Duration
487            | Self::Enum { .. }
488            | Self::Float32
489            | Self::Float64
490            | Self::Int
491            | Self::Int128
492            | Self::IntBig
493            | Self::Principal
494            | Self::Subaccount
495            | Self::Text
496            | Self::Timestamp
497            | Self::Uint
498            | Self::Uint128
499            | Self::UintBig
500            | Self::Ulid
501            | Self::Unit
502            | Self::Decimal { .. }
503            | Self::Relation { .. } => RuntimeValueKind::Atomic,
504            Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
505            Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
506            Self::Structured { queryable } => RuntimeValueKind::Structured {
507                queryable: *queryable,
508            },
509        }
510    }
511
512    /// Returns `true` if this field shape is permitted in
513    /// persisted or query-visible schemas under the current
514    /// determinism policy.
515    ///
516    /// This shape-level check is structural only; query-time policy
517    /// enforcement (for example, map predicate fencing) is applied at
518    /// query construction and validation boundaries.
519    #[must_use]
520    pub const fn is_deterministic_collection_shape(&self) -> bool {
521        match self {
522            Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
523
524            Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
525
526            Self::Map { key, value } => {
527                key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
528            }
529
530            _ => true,
531        }
532    }
533
534    /// Return true when this planner-frozen grouped field kind can stay on the
535    /// borrowed grouped-key probe path without owned canonical materialization.
536    #[must_use]
537    pub(crate) fn supports_group_probe(&self) -> bool {
538        match self {
539            Self::Enum { variants, .. } => variants.iter().all(|variant| {
540                variant
541                    .payload_kind()
542                    .is_none_or(Self::supports_group_probe)
543            }),
544            Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
545            Self::List(_)
546            | Self::Set(_)
547            | Self::Map { .. }
548            | Self::Structured { .. }
549            | Self::Unit => false,
550            Self::Account
551            | Self::Blob
552            | Self::Bool
553            | Self::Date
554            | Self::Decimal { .. }
555            | Self::Duration
556            | Self::Float32
557            | Self::Float64
558            | Self::Int
559            | Self::Int128
560            | Self::IntBig
561            | Self::Principal
562            | Self::Subaccount
563            | Self::Text
564            | Self::Timestamp
565            | Self::Uint
566            | Self::Uint128
567            | Self::UintBig
568            | Self::Ulid => true,
569        }
570    }
571
572    /// Match one runtime value against this field kind contract.
573    ///
574    /// This is the shared recursive field-kind acceptance boundary used by
575    /// persisted-row encoding, mutation-save validation, and aggregate field
576    /// extraction.
577    #[must_use]
578    pub(crate) fn accepts_value(&self, value: &Value) -> bool {
579        match (self, value) {
580            (Self::Account, Value::Account(_))
581            | (Self::Blob, Value::Blob(_))
582            | (Self::Bool, Value::Bool(_))
583            | (Self::Date, Value::Date(_))
584            | (Self::Decimal { .. }, Value::Decimal(_))
585            | (Self::Duration, Value::Duration(_))
586            | (Self::Enum { .. }, Value::Enum(_))
587            | (Self::Float32, Value::Float32(_))
588            | (Self::Float64, Value::Float64(_))
589            | (Self::Int, Value::Int(_))
590            | (Self::Int128, Value::Int128(_))
591            | (Self::IntBig, Value::IntBig(_))
592            | (Self::Principal, Value::Principal(_))
593            | (Self::Subaccount, Value::Subaccount(_))
594            | (Self::Text, Value::Text(_))
595            | (Self::Timestamp, Value::Timestamp(_))
596            | (Self::Uint, Value::Uint(_))
597            | (Self::Uint128, Value::Uint128(_))
598            | (Self::UintBig, Value::UintBig(_))
599            | (Self::Ulid, Value::Ulid(_))
600            | (Self::Unit, Value::Unit)
601            | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
602            (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
603            (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
604                items.iter().all(|item| inner.accepts_value(item))
605            }
606            (Self::Map { key, value }, Value::Map(entries)) => {
607                if Value::validate_map_entries(entries.as_slice()).is_err() {
608                    return false;
609                }
610
611                entries.iter().all(|(entry_key, entry_value)| {
612                    key.accepts_value(entry_key) && value.accepts_value(entry_value)
613                })
614            }
615            _ => false,
616        }
617    }
618}
619
620// `FieldStorageDecode::ByKind` follows the same literal compatibility rule as
621// the schema predicate layer without routing the storage model through
622// `db::schema`. Structured field kinds are intentionally not accepted here;
623// fields that persist open-ended structured payloads use
624// `FieldStorageDecode::Value` instead.
625fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
626    match (kind, value) {
627        (FieldKind::Relation { key_kind, .. }, value) => {
628            by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
629        }
630        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
631            .iter()
632            .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
633        (
634            FieldKind::Map {
635                key,
636                value: value_kind,
637            },
638            Value::Map(entries),
639        ) => {
640            if Value::validate_map_entries(entries.as_slice()).is_err() {
641                return false;
642            }
643
644            entries.iter().all(|(entry_key, entry_value)| {
645                by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
646                    && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
647            })
648        }
649        (FieldKind::Structured { .. }, _) => false,
650        _ => kind.accepts_value(value),
651    }
652}
653
654// `FieldStorageDecode::Value` fields persist an opaque runtime `Value` envelope,
655// so `FieldKind::Structured` must stay open-ended while outer collection/map
656// shapes still enforce the recursive structure the model owns.
657fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
658    match (kind, value) {
659        (FieldKind::Structured { .. }, _) => true,
660        (FieldKind::Relation { key_kind, .. }, value) => {
661            value_storage_kind_accepts_runtime_value(*key_kind, value)
662        }
663        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
664            .iter()
665            .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
666        (
667            FieldKind::Map {
668                key,
669                value: value_kind,
670            },
671            Value::Map(entries),
672        ) => {
673            if Value::validate_map_entries(entries.as_slice()).is_err() {
674                return false;
675            }
676
677            entries.iter().all(|(entry_key, entry_value)| {
678                value_storage_kind_accepts_runtime_value(*key, entry_key)
679                    && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
680            })
681        }
682        _ => kind.accepts_value(value),
683    }
684}
685
686// Enforce fixed decimal scales through nested collection/map shapes before a
687// field-level runtime value is persisted.
688fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
689    if matches!(value, Value::Null) {
690        return Ok(());
691    }
692
693    match (kind, value) {
694        (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
695            if decimal.scale() != scale {
696                return Err(format!(
697                    "decimal scale mismatch: expected {scale}, found {}",
698                    decimal.scale()
699                ));
700            }
701
702            Ok(())
703        }
704        (FieldKind::Relation { key_kind, .. }, value) => {
705            ensure_decimal_scale_matches(*key_kind, value)
706        }
707        (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
708            for item in items {
709                ensure_decimal_scale_matches(*inner, item)?;
710            }
711
712            Ok(())
713        }
714        (
715            FieldKind::Map {
716                key,
717                value: map_value,
718            },
719            Value::Map(entries),
720        ) => {
721            for (entry_key, entry_value) in entries {
722                ensure_decimal_scale_matches(*key, entry_key)?;
723                ensure_decimal_scale_matches(*map_value, entry_value)?;
724            }
725
726            Ok(())
727        }
728        _ => Ok(()),
729    }
730}
731
732// Enforce the canonical persisted ordering rules for set/map shapes before one
733// field-level runtime value becomes row bytes.
734fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
735    match (kind, value) {
736        (FieldKind::Set(_), Value::List(items)) => {
737            for pair in items.windows(2) {
738                let [left, right] = pair else {
739                    continue;
740                };
741                if Value::canonical_cmp(left, right) != Ordering::Less {
742                    return Err("set payload must already be canonical and deduplicated".into());
743                }
744            }
745
746            Ok(())
747        }
748        (FieldKind::Map { .. }, Value::Map(entries)) => {
749            Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
750
751            if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
752                return Err("map payload must already be canonical and deduplicated".into());
753            }
754
755            Ok(())
756        }
757        _ => Ok(()),
758    }
759}