Skip to main content

icydb_core/value/
mod.rs

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