Skip to main content

icydb_core/value/
mod.rs

1mod coercion;
2mod compare;
3mod rank;
4mod tag;
5mod wire;
6
7#[cfg(test)]
8mod tests;
9
10use crate::{
11    db::StorageKey,
12    prelude::*,
13    traits::{EnumValue, FieldValue, NumFromPrimitive},
14    types::*,
15};
16use candid::CandidType;
17use serde::{Deserialize, Serialize};
18use std::cmp::Ordering;
19
20// re-exports
21pub use coercion::{CoercionFamily, CoercionFamilyExt};
22pub use tag::ValueTag;
23
24///
25/// CONSTANTS
26///
27
28const F64_SAFE_I64: i64 = 1i64 << 53;
29const F64_SAFE_U64: u64 = 1u64 << 53;
30const F64_SAFE_I128: i128 = 1i128 << 53;
31const F64_SAFE_U128: u128 = 1u128 << 53;
32
33///
34/// NumericRepr
35///
36
37enum NumericRepr {
38    Decimal(Decimal),
39    F64(f64),
40    None,
41}
42
43///
44/// TextMode
45///
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub enum TextMode {
49    Cs, // case-sensitive
50    Ci, // case-insensitive
51}
52
53///
54/// MapValueError
55///
56/// Invariant violations for `Value::Map` construction/normalization.
57///
58
59#[derive(Clone, Debug, Eq, PartialEq)]
60pub enum MapValueError {
61    EmptyKey {
62        index: usize,
63    },
64    NonScalarKey {
65        index: usize,
66        key: Value,
67    },
68    NonScalarValue {
69        index: usize,
70        value: Value,
71    },
72    DuplicateKey {
73        left_index: usize,
74        right_index: usize,
75    },
76}
77
78impl std::fmt::Display for MapValueError {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            Self::EmptyKey { index } => write!(f, "map key at index {index} must be non-null"),
82            Self::NonScalarKey { index, key } => {
83                write!(f, "map key at index {index} is not scalar: {key:?}")
84            }
85            Self::NonScalarValue { index, value } => {
86                write!(
87                    f,
88                    "map value at index {index} is not scalar/ref-like: {value:?}"
89                )
90            }
91            Self::DuplicateKey {
92                left_index,
93                right_index,
94            } => write!(
95                f,
96                "map contains duplicate keys at normalized positions {left_index} and {right_index}"
97            ),
98        }
99    }
100}
101
102impl std::error::Error for MapValueError {}
103
104///
105/// SchemaInvariantError
106///
107/// Invariant violations encountered while materializing schema/runtime values.
108///
109
110#[derive(Clone, Debug, Eq, PartialEq)]
111pub enum SchemaInvariantError {
112    InvalidMapValue(MapValueError),
113}
114
115impl std::fmt::Display for SchemaInvariantError {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        match self {
118            Self::InvalidMapValue(err) => write!(f, "{err}"),
119        }
120    }
121}
122
123impl std::error::Error for SchemaInvariantError {}
124
125impl From<MapValueError> for SchemaInvariantError {
126    fn from(value: MapValueError) -> Self {
127        Self::InvalidMapValue(value)
128    }
129}
130
131///
132/// Value
133/// can be used in WHERE statements
134///
135/// Null        → the field’s value is Option::None (i.e., SQL NULL).
136/// Unit        → internal placeholder for RHS; not a real value.
137///
138
139#[derive(CandidType, Clone, Debug, Eq, PartialEq, Serialize)]
140pub enum Value {
141    Account(Account),
142    Blob(Vec<u8>),
143    Bool(bool),
144    Date(Date),
145    Decimal(Decimal),
146    Duration(Duration),
147    Enum(ValueEnum),
148    E8s(E8s),
149    E18s(E18s),
150    Float32(Float32),
151    Float64(Float64),
152    Int(i64),
153    Int128(Int128),
154    IntBig(Int),
155    /// Ordered list of values.
156    /// Used for many-cardinality transport.
157    /// List order is preserved for normalization and fingerprints.
158    List(Vec<Self>),
159    /// Canonical deterministic map representation.
160    ///
161    /// - Maps are unordered values; insertion order is discarded.
162    /// - Entries are always sorted by canonical key order and keys are unique.
163    /// - Map fields are patchable through update views, but remain non-queryable.
164    /// - Persistence treats map fields as atomic value replacements per row save.
165    Map(Vec<(Self, Self)>),
166    Null,
167    Principal(Principal),
168    Subaccount(Subaccount),
169    Text(String),
170    Timestamp(Timestamp),
171    Uint(u64),
172    Uint128(Nat128),
173    UintBig(Nat),
174    Ulid(Ulid),
175    Unit,
176}
177
178// Local helpers to expand the scalar registry into match arms.
179macro_rules! value_is_numeric_from_registry {
180    ( @args $value:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
181        match $value {
182            $( $value_pat => $is_numeric, )*
183            _ => false,
184        }
185    };
186}
187
188macro_rules! value_supports_numeric_coercion_from_registry {
189    ( @args $value:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
190        match $value {
191            $( $value_pat => $supports_numeric_coercion, )*
192            _ => false,
193        }
194    };
195}
196
197macro_rules! value_storage_key_case {
198    ( $value:expr, Unit, true ) => {
199        if let Value::Unit = $value {
200            Some(StorageKey::Unit)
201        } else {
202            None
203        }
204    };
205    ( $value:expr, $scalar:ident, true ) => {
206        if let Value::$scalar(v) = $value {
207            Some(StorageKey::$scalar(*v))
208        } else {
209            None
210        }
211    };
212    ( $value:expr, $scalar:ident, false ) => {
213        None
214    };
215}
216
217macro_rules! value_storage_key_from_registry {
218    ( @args $value:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:tt, is_storage_key_encodable = $is_storage_key_encodable:tt) ),* $(,)? ) => {
219        {
220            let mut key = None;
221            $(
222                match key {
223                    Some(_) => {}
224                    None => {
225                        key = value_storage_key_case!($value, $scalar, $is_storage_key_encodable);
226                    }
227                }
228            )*
229            key
230        }
231    };
232}
233
234macro_rules! value_coercion_family_from_registry {
235    ( @args $value:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
236        match $value {
237            $( $value_pat => $coercion_family, )*
238            Value::List(_) => CoercionFamily::Collection,
239            Value::Map(_) => CoercionFamily::Collection,
240            Value::Null => CoercionFamily::Null,
241        }
242    };
243}
244
245impl Value {
246    ///
247    /// CONSTRUCTION
248    ///
249
250    /// Build a `Value::List` from a list literal.
251    ///
252    /// Intended for tests and inline construction.
253    /// Requires `Clone` because items are borrowed.
254    pub fn from_slice<T>(items: &[T]) -> Self
255    where
256        T: Into<Self> + Clone,
257    {
258        Self::List(items.iter().cloned().map(Into::into).collect())
259    }
260
261    /// Build a `Value::List` from owned items.
262    ///
263    /// This is the canonical constructor for query / DTO boundaries.
264    pub fn from_list<T>(items: Vec<T>) -> Self
265    where
266        T: Into<Self>,
267    {
268        Self::List(items.into_iter().map(Into::into).collect())
269    }
270
271    /// Build a canonical `Value::Map` from owned key/value entries.
272    ///
273    /// Invariants are validated and entries are normalized:
274    /// - keys must be scalar and non-null
275    /// - values must be scalar/ref-like (no collections)
276    /// - entries are sorted by canonical key order
277    /// - duplicate keys are rejected
278    pub fn from_map(entries: Vec<(Self, Self)>) -> Result<Self, MapValueError> {
279        let normalized = Self::normalize_map_entries(entries)?;
280        Ok(Self::Map(normalized))
281    }
282
283    /// Validate map entry invariants without changing order.
284    pub fn validate_map_entries(entries: &[(Self, Self)]) -> Result<(), MapValueError> {
285        for (index, (key, value)) in entries.iter().enumerate() {
286            if matches!(key, Self::Null) {
287                return Err(MapValueError::EmptyKey { index });
288            }
289            if !key.is_scalar() {
290                return Err(MapValueError::NonScalarKey {
291                    index,
292                    key: key.clone(),
293                });
294            }
295
296            if !value.is_scalar() {
297                return Err(MapValueError::NonScalarValue {
298                    index,
299                    value: value.clone(),
300                });
301            }
302        }
303
304        Ok(())
305    }
306
307    /// Normalize map entries into canonical deterministic order.
308    pub fn normalize_map_entries(
309        mut entries: Vec<(Self, Self)>,
310    ) -> Result<Vec<(Self, Self)>, MapValueError> {
311        Self::validate_map_entries(&entries)?;
312        entries
313            .sort_by(|(left_key, _), (right_key, _)| Self::canonical_cmp_key(left_key, right_key));
314
315        for i in 1..entries.len() {
316            let (left_key, _) = &entries[i - 1];
317            let (right_key, _) = &entries[i];
318            if Self::canonical_cmp_key(left_key, right_key) == Ordering::Equal {
319                return Err(MapValueError::DuplicateKey {
320                    left_index: i - 1,
321                    right_index: i,
322                });
323            }
324        }
325
326        Ok(entries)
327    }
328
329    /// Build a `Value::Enum` from a domain enum using its explicit mapping.
330    pub fn from_enum<E: EnumValue>(value: E) -> Self {
331        Self::Enum(value.to_value_enum())
332    }
333
334    /// Build a strict enum value using the canonical path of `E`.
335    #[must_use]
336    pub fn enum_strict<E: Path>(variant: &str) -> Self {
337        Self::Enum(ValueEnum::strict::<E>(variant))
338    }
339
340    ///
341    /// TYPES
342    ///
343
344    /// Returns true if the value is one of the numeric-like variants
345    /// supported by numeric comparison/ordering.
346    #[must_use]
347    pub const fn is_numeric(&self) -> bool {
348        scalar_registry!(value_is_numeric_from_registry, self)
349    }
350
351    /// Returns true when numeric coercion/comparison is explicitly allowed.
352    #[must_use]
353    pub const fn supports_numeric_coercion(&self) -> bool {
354        scalar_registry!(value_supports_numeric_coercion_from_registry, self)
355    }
356
357    /// Returns true if the value is Text.
358    #[must_use]
359    pub const fn is_text(&self) -> bool {
360        matches!(self, Self::Text(_))
361    }
362
363    /// Returns true if the value is Unit (used for presence/null comparators).
364    #[must_use]
365    pub const fn is_unit(&self) -> bool {
366        matches!(self, Self::Unit)
367    }
368
369    #[must_use]
370    pub const fn is_scalar(&self) -> bool {
371        match self {
372            // definitely not scalar:
373            Self::List(_) | Self::Map(_) | Self::Unit => false,
374            _ => true,
375        }
376    }
377
378    /// Stable canonical variant tag used by hash/fingerprint encodings.
379    #[must_use]
380    pub(crate) const fn canonical_tag(&self) -> ValueTag {
381        tag::canonical_tag(self)
382    }
383
384    /// Stable canonical rank used by all cross-variant ordering surfaces.
385    #[must_use]
386    pub(crate) const fn canonical_rank(&self) -> u8 {
387        rank::canonical_rank(self)
388    }
389
390    /// Total canonical comparator used by planner/predicate/fingerprint surfaces.
391    #[must_use]
392    pub(crate) fn canonical_cmp(left: &Self, right: &Self) -> Ordering {
393        compare::canonical_cmp(left, right)
394    }
395
396    /// Total canonical comparator used for map-key normalization.
397    #[must_use]
398    pub fn canonical_cmp_key(left: &Self, right: &Self) -> Ordering {
399        compare::canonical_cmp_key(left, right)
400    }
401
402    /// Strict comparator for identical orderable variants.
403    ///
404    /// Returns `None` for mismatched or non-orderable variants.
405    #[must_use]
406    pub(crate) fn strict_order_cmp(left: &Self, right: &Self) -> Option<Ordering> {
407        compare::strict_order_cmp(left, right)
408    }
409
410    fn numeric_repr(&self) -> NumericRepr {
411        // Numeric comparison eligibility is registry-authoritative.
412        if !self.supports_numeric_coercion() {
413            return NumericRepr::None;
414        }
415
416        if let Some(d) = self.to_decimal() {
417            return NumericRepr::Decimal(d);
418        }
419        if let Some(f) = self.to_f64_lossless() {
420            return NumericRepr::F64(f);
421        }
422        NumericRepr::None
423    }
424
425    ///
426    /// CONVERSION
427    ///
428
429    /// NOTE:
430    /// `Unit` is intentionally treated as a valid storage key and indexable,
431    /// used for singleton tables and synthetic identity entities.
432    /// Only `Null` is non-indexable.
433    #[must_use]
434    pub const fn as_storage_key(&self) -> Option<StorageKey> {
435        scalar_registry!(value_storage_key_from_registry, self)
436    }
437
438    #[must_use]
439    pub const fn as_text(&self) -> Option<&str> {
440        if let Self::Text(s) = self {
441            Some(s.as_str())
442        } else {
443            None
444        }
445    }
446
447    #[must_use]
448    pub const fn as_list(&self) -> Option<&[Self]> {
449        if let Self::List(xs) = self {
450            Some(xs.as_slice())
451        } else {
452            None
453        }
454    }
455
456    #[must_use]
457    pub const fn as_map(&self) -> Option<&[(Self, Self)]> {
458        if let Self::Map(entries) = self {
459            Some(entries.as_slice())
460        } else {
461            None
462        }
463    }
464
465    fn to_decimal(&self) -> Option<Decimal> {
466        match self {
467            Self::Decimal(d) => Some(*d),
468            Self::Duration(d) => Decimal::from_u64(d.get()),
469            Self::E8s(v) => Some(v.to_decimal()),
470            Self::E18s(v) => v.to_decimal(),
471            Self::Float64(f) => Decimal::from_f64(f.get()),
472            Self::Float32(f) => Decimal::from_f32(f.get()),
473            Self::Int(i) => Decimal::from_i64(*i),
474            Self::Int128(i) => Decimal::from_i128(i.get()),
475            Self::IntBig(i) => i.to_i128().and_then(Decimal::from_i128),
476            Self::Timestamp(t) => Decimal::from_u64(t.get()),
477            Self::Uint(u) => Decimal::from_u64(*u),
478            Self::Uint128(u) => Decimal::from_u128(u.get()),
479            Self::UintBig(u) => u.to_u128().and_then(Decimal::from_u128),
480
481            _ => None,
482        }
483    }
484
485    // it's lossless, trust me bro
486    #[expect(clippy::cast_precision_loss)]
487    fn to_f64_lossless(&self) -> Option<f64> {
488        match self {
489            Self::Duration(d) if d.get() <= F64_SAFE_U64 => Some(d.get() as f64),
490            Self::Float64(f) => Some(f.get()),
491            Self::Float32(f) => Some(f64::from(f.get())),
492            Self::Int(i) if (-F64_SAFE_I64..=F64_SAFE_I64).contains(i) => Some(*i as f64),
493            Self::Int128(i) if (-F64_SAFE_I128..=F64_SAFE_I128).contains(&i.get()) => {
494                Some(i.get() as f64)
495            }
496            Self::IntBig(i) => i.to_i128().and_then(|v| {
497                (-F64_SAFE_I128..=F64_SAFE_I128)
498                    .contains(&v)
499                    .then_some(v as f64)
500            }),
501            Self::Timestamp(t) if t.get() <= F64_SAFE_U64 => Some(t.get() as f64),
502            Self::Uint(u) if *u <= F64_SAFE_U64 => Some(*u as f64),
503            Self::Uint128(u) if u.get() <= F64_SAFE_U128 => Some(u.get() as f64),
504            Self::UintBig(u) => u
505                .to_u128()
506                .and_then(|v| (v <= F64_SAFE_U128).then_some(v as f64)),
507
508            _ => None,
509        }
510    }
511
512    /// Cross-type numeric comparison; returns None if non-numeric.
513    #[must_use]
514    pub fn cmp_numeric(&self, other: &Self) -> Option<Ordering> {
515        if !self.supports_numeric_coercion() || !other.supports_numeric_coercion() {
516            return None;
517        }
518
519        match (self.numeric_repr(), other.numeric_repr()) {
520            (NumericRepr::Decimal(a), NumericRepr::Decimal(b)) => a.partial_cmp(&b),
521            (NumericRepr::F64(a), NumericRepr::F64(b)) => a.partial_cmp(&b),
522            _ => None,
523        }
524    }
525
526    ///
527    /// TEXT COMPARISON
528    ///
529
530    fn fold_ci(s: &str) -> std::borrow::Cow<'_, str> {
531        if s.is_ascii() {
532            return std::borrow::Cow::Owned(s.to_ascii_lowercase());
533        }
534        // NOTE: Unicode fallback — temporary to_lowercase for non‑ASCII.
535        // Future: replace with proper NFKC + full casefold when available.
536        std::borrow::Cow::Owned(s.to_lowercase())
537    }
538
539    fn text_with_mode(s: &'_ str, mode: TextMode) -> std::borrow::Cow<'_, str> {
540        match mode {
541            TextMode::Cs => std::borrow::Cow::Borrowed(s),
542            TextMode::Ci => Self::fold_ci(s),
543        }
544    }
545
546    fn text_op(
547        &self,
548        other: &Self,
549        mode: TextMode,
550        f: impl Fn(&str, &str) -> bool,
551    ) -> Option<bool> {
552        let (a, b) = (self.as_text()?, other.as_text()?);
553        let a = Self::text_with_mode(a, mode);
554        let b = Self::text_with_mode(b, mode);
555        Some(f(&a, &b))
556    }
557
558    fn ci_key(&self) -> Option<String> {
559        match self {
560            Self::Text(s) => Some(Self::fold_ci(s).into_owned()),
561            Self::Ulid(u) => Some(u.to_string().to_ascii_lowercase()),
562            Self::Principal(p) => Some(p.to_string().to_ascii_lowercase()),
563            Self::Account(a) => Some(a.to_string().to_ascii_lowercase()),
564            _ => None,
565        }
566    }
567
568    fn eq_ci(a: &Self, b: &Self) -> bool {
569        if let (Some(ak), Some(bk)) = (a.ci_key(), b.ci_key()) {
570            return ak == bk;
571        }
572
573        a == b
574    }
575
576    fn normalize_list_ref(v: &Self) -> Vec<&Self> {
577        match v {
578            Self::List(vs) => vs.iter().collect(),
579            v => vec![v],
580        }
581    }
582
583    fn contains_by<F>(&self, needle: &Self, eq: F) -> Option<bool>
584    where
585        F: Fn(&Self, &Self) -> bool,
586    {
587        self.as_list()
588            .map(|items| items.iter().any(|v| eq(v, needle)))
589    }
590
591    #[expect(clippy::unnecessary_wraps)]
592    fn contains_any_by<F>(&self, needles: &Self, eq: F) -> Option<bool>
593    where
594        F: Fn(&Self, &Self) -> bool,
595    {
596        let needles = Self::normalize_list_ref(needles);
597        match self {
598            Self::List(items) => Some(needles.iter().any(|n| items.iter().any(|v| eq(v, n)))),
599            scalar => Some(needles.iter().any(|n| eq(scalar, n))),
600        }
601    }
602
603    #[expect(clippy::unnecessary_wraps)]
604    fn contains_all_by<F>(&self, needles: &Self, eq: F) -> Option<bool>
605    where
606        F: Fn(&Self, &Self) -> bool,
607    {
608        let needles = Self::normalize_list_ref(needles);
609        match self {
610            Self::List(items) => Some(needles.iter().all(|n| items.iter().any(|v| eq(v, n)))),
611            scalar => Some(needles.len() == 1 && eq(scalar, needles[0])),
612        }
613    }
614
615    fn in_list_by<F>(&self, haystack: &Self, eq: F) -> Option<bool>
616    where
617        F: Fn(&Self, &Self) -> bool,
618    {
619        if let Self::List(items) = haystack {
620            Some(items.iter().any(|h| eq(h, self)))
621        } else {
622            None
623        }
624    }
625
626    #[must_use]
627    /// Case-sensitive/insensitive equality check for text-like values.
628    pub fn text_eq(&self, other: &Self, mode: TextMode) -> Option<bool> {
629        self.text_op(other, mode, |a, b| a == b)
630    }
631
632    #[must_use]
633    /// Check whether `other` is a substring of `self` under the given text mode.
634    pub fn text_contains(&self, needle: &Self, mode: TextMode) -> Option<bool> {
635        self.text_op(needle, mode, |a, b| a.contains(b))
636    }
637
638    #[must_use]
639    /// Check whether `self` starts with `other` under the given text mode.
640    pub fn text_starts_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
641        self.text_op(needle, mode, |a, b| a.starts_with(b))
642    }
643
644    #[must_use]
645    /// Check whether `self` ends with `other` under the given text mode.
646    pub fn text_ends_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
647        self.text_op(needle, mode, |a, b| a.ends_with(b))
648    }
649
650    ///
651    /// EMPTY
652    ///
653
654    #[must_use]
655    pub const fn is_empty(&self) -> Option<bool> {
656        match self {
657            Self::List(xs) => Some(xs.is_empty()),
658            Self::Map(entries) => Some(entries.is_empty()),
659            Self::Text(s) => Some(s.is_empty()),
660            Self::Blob(b) => Some(b.is_empty()),
661
662            //  fields represented as Value::Null:
663            Self::Null => Some(true),
664
665            _ => None,
666        }
667    }
668
669    #[must_use]
670    /// Logical negation of [`is_empty`](Self::is_empty).
671    pub fn is_not_empty(&self) -> Option<bool> {
672        self.is_empty().map(|b| !b)
673    }
674
675    ///
676    /// COLLECTIONS
677    ///
678
679    #[must_use]
680    /// Returns true if `self` contains `needle` (or equals it for scalars).
681    pub fn contains(&self, needle: &Self) -> Option<bool> {
682        self.contains_by(needle, |a, b| a == b)
683    }
684
685    #[must_use]
686    /// Returns true if any item in `needles` matches a member of `self`.
687    pub fn contains_any(&self, needles: &Self) -> Option<bool> {
688        self.contains_any_by(needles, |a, b| a == b)
689    }
690
691    #[must_use]
692    /// Returns true if every item in `needles` matches a member of `self`.
693    pub fn contains_all(&self, needles: &Self) -> Option<bool> {
694        self.contains_all_by(needles, |a, b| a == b)
695    }
696
697    #[must_use]
698    /// Returns true if `self` exists inside the provided list.
699    pub fn in_list(&self, haystack: &Self) -> Option<bool> {
700        self.in_list_by(haystack, |a, b| a == b)
701    }
702
703    #[must_use]
704    /// Case-insensitive `contains` supporting text and identifier variants.
705    pub fn contains_ci(&self, needle: &Self) -> Option<bool> {
706        match self {
707            Self::List(_) => self.contains_by(needle, Self::eq_ci),
708            _ => Some(Self::eq_ci(self, needle)),
709        }
710    }
711
712    #[must_use]
713    /// Case-insensitive variant of [`contains_any`](Self::contains_any).
714    pub fn contains_any_ci(&self, needles: &Self) -> Option<bool> {
715        self.contains_any_by(needles, Self::eq_ci)
716    }
717
718    #[must_use]
719    /// Case-insensitive variant of [`contains_all`](Self::contains_all).
720    pub fn contains_all_ci(&self, needles: &Self) -> Option<bool> {
721        self.contains_all_by(needles, Self::eq_ci)
722    }
723
724    #[must_use]
725    /// Case-insensitive variant of [`in_list`](Self::in_list).
726    pub fn in_list_ci(&self, haystack: &Self) -> Option<bool> {
727        self.in_list_by(haystack, Self::eq_ci)
728    }
729}
730
731impl FieldValue for Value {
732    fn kind() -> crate::traits::FieldValueKind {
733        crate::traits::FieldValueKind::Atomic
734    }
735
736    fn to_value(&self) -> Value {
737        self.clone()
738    }
739
740    fn from_value(value: &Value) -> Option<Self> {
741        Some(value.clone())
742    }
743}
744
745#[macro_export]
746macro_rules! impl_from_for {
747    ( $( $type:ty => $variant:ident ),* $(,)? ) => {
748        $(
749            impl From<$type> for Value {
750                fn from(v: $type) -> Self {
751                    Self::$variant(v.into())
752                }
753            }
754        )*
755    };
756}
757
758impl_from_for! {
759    Account    => Account,
760    Date       => Date,
761    Decimal    => Decimal,
762    Duration   => Duration,
763    E8s        => E8s,
764    E18s       => E18s,
765    bool       => Bool,
766    i8         => Int,
767    i16        => Int,
768    i32        => Int,
769    i64        => Int,
770    i128       => Int128,
771    Int        => IntBig,
772    Principal  => Principal,
773    Subaccount => Subaccount,
774    &str       => Text,
775    String     => Text,
776    Timestamp  => Timestamp,
777    u8         => Uint,
778    u16        => Uint,
779    u32        => Uint,
780    u64        => Uint,
781    u128       => Uint128,
782    Nat        => UintBig,
783    Ulid       => Ulid,
784}
785
786impl CoercionFamilyExt for Value {
787    /// Returns the coercion-routing family for this value.
788    ///
789    /// NOTE:
790    /// This does NOT imply numeric, arithmetic, ordering, or keyability support.
791    /// All scalar capabilities are registry-driven.
792    fn coercion_family(&self) -> CoercionFamily {
793        scalar_registry!(value_coercion_family_from_registry, self)
794    }
795}
796
797impl From<Vec<Self>> for Value {
798    fn from(vec: Vec<Self>) -> Self {
799        Self::List(vec)
800    }
801}
802
803impl TryFrom<Vec<(Self, Self)>> for Value {
804    type Error = SchemaInvariantError;
805
806    fn try_from(entries: Vec<(Self, Self)>) -> Result<Self, Self::Error> {
807        Self::from_map(entries).map_err(Self::Error::from)
808    }
809}
810
811impl From<()> for Value {
812    fn from((): ()) -> Self {
813        Self::Unit
814    }
815}
816
817// NOTE:
818// Value::partial_cmp is NOT the canonical ordering for database semantics.
819// Some orderable scalar types (e.g. Account, Unit) intentionally do not
820// participate here. Use canonical_cmp / strict ordering for ORDER BY,
821// planning, and key-range validation.
822impl PartialOrd for Value {
823    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
824        match (self, other) {
825            (Self::Bool(a), Self::Bool(b)) => a.partial_cmp(b),
826            (Self::Date(a), Self::Date(b)) => a.partial_cmp(b),
827            (Self::Decimal(a), Self::Decimal(b)) => a.partial_cmp(b),
828            (Self::Duration(a), Self::Duration(b)) => a.partial_cmp(b),
829            (Self::E8s(a), Self::E8s(b)) => a.partial_cmp(b),
830            (Self::E18s(a), Self::E18s(b)) => a.partial_cmp(b),
831            (Self::Enum(a), Self::Enum(b)) => a.partial_cmp(b),
832            (Self::Float32(a), Self::Float32(b)) => a.partial_cmp(b),
833            (Self::Float64(a), Self::Float64(b)) => a.partial_cmp(b),
834            (Self::Int(a), Self::Int(b)) => a.partial_cmp(b),
835            (Self::Int128(a), Self::Int128(b)) => a.partial_cmp(b),
836            (Self::IntBig(a), Self::IntBig(b)) => a.partial_cmp(b),
837            (Self::Principal(a), Self::Principal(b)) => a.partial_cmp(b),
838            (Self::Subaccount(a), Self::Subaccount(b)) => a.partial_cmp(b),
839            (Self::Text(a), Self::Text(b)) => a.partial_cmp(b),
840            (Self::Timestamp(a), Self::Timestamp(b)) => a.partial_cmp(b),
841            (Self::Uint(a), Self::Uint(b)) => a.partial_cmp(b),
842            (Self::Uint128(a), Self::Uint128(b)) => a.partial_cmp(b),
843            (Self::UintBig(a), Self::UintBig(b)) => a.partial_cmp(b),
844            (Self::Ulid(a), Self::Ulid(b)) => a.partial_cmp(b),
845            (Self::Map(a), Self::Map(b)) => {
846                for ((left_key, left_value), (right_key, right_value)) in a.iter().zip(b.iter()) {
847                    let key_cmp = Self::canonical_cmp_key(left_key, right_key);
848                    if key_cmp != Ordering::Equal {
849                        return Some(key_cmp);
850                    }
851
852                    match left_value.partial_cmp(right_value) {
853                        Some(Ordering::Equal) => {}
854                        non_eq => return non_eq,
855                    }
856                }
857                a.len().partial_cmp(&b.len())
858            }
859
860            // Cross-type comparisons: no ordering
861            _ => None,
862        }
863    }
864}
865
866///
867/// ValueEnum
868/// handles the Enum case; `path` is optional to allow strict (typed) or loose matching.
869///
870
871#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, PartialOrd, Serialize)]
872pub struct ValueEnum {
873    pub variant: String,
874    pub path: Option<String>,
875    pub payload: Option<Box<Value>>,
876}
877
878impl ValueEnum {
879    #[must_use]
880    /// Build a strict enum value matching the provided variant and path.
881    pub fn new(variant: &str, path: Option<&str>) -> Self {
882        Self {
883            variant: variant.to_string(),
884            path: path.map(ToString::to_string),
885            payload: None,
886        }
887    }
888
889    #[must_use]
890    /// Build a strict enum value using the canonical path of `E`.
891    pub fn strict<E: Path>(variant: &str) -> Self {
892        Self::new(variant, Some(E::PATH))
893    }
894
895    #[must_use]
896    /// Build a strict enum value from a domain enum using its explicit mapping.
897    pub fn from_enum<E: EnumValue>(value: E) -> Self {
898        value.to_value_enum()
899    }
900
901    #[must_use]
902    /// Build an enum value that ignores the path for loose matching.
903    pub fn loose(variant: &str) -> Self {
904        Self::new(variant, None)
905    }
906
907    #[must_use]
908    /// Attach an enum payload (used for data-carrying variants).
909    pub fn with_payload(mut self, payload: Value) -> Self {
910        self.payload = Some(Box::new(payload));
911        self
912    }
913}