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::{ValueFamily, ValueFamilyExt};
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/// Value
50/// can be used in WHERE statements
51///
52/// None        → the field’s value is Option::None (i.e., SQL NULL).
53/// Unit        → internal placeholder for RHS; not a real value.
54/// Unsupported → the field exists but isn’t filterable/indexable.
55///
56
57#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
58pub enum Value {
59    Account(Account),
60    Blob(Vec<u8>),
61    Bool(bool),
62    Date(Date),
63    Decimal(Decimal),
64    Duration(Duration),
65    Enum(ValueEnum),
66    E8s(E8s),
67    E18s(E18s),
68    Float32(Float32),
69    Float64(Float64),
70    Int(i64),
71    Int128(Int128),
72    IntBig(Int),
73    /// Ordered list of values.
74    /// Used for many-cardinality transport and map-like encodings (`[key, value]` pairs).
75    /// List order is preserved for normalization and fingerprints.
76    List(Vec<Self>),
77    None,
78    Principal(Principal),
79    Subaccount(Subaccount),
80    Text(String),
81    Timestamp(Timestamp),
82    Uint(u64),
83    Uint128(Nat128),
84    UintBig(Nat),
85    Ulid(Ulid),
86    Unit,
87    Unsupported,
88}
89
90// Local helpers to expand the scalar registry into match arms.
91macro_rules! value_is_numeric_from_registry {
92    ( @args $value:expr; @entries $( ($scalar:ident, $family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr) ),* $(,)? ) => {
93        match $value {
94            $( $value_pat => $is_numeric, )*
95            _ => false,
96        }
97    };
98}
99
100macro_rules! value_storage_key_case {
101    ( $value:expr, Unit, true ) => {
102        if let Value::Unit = $value {
103            Some(StorageKey::Unit)
104        } else {
105            None
106        }
107    };
108    ( $value:expr, $scalar:ident, true ) => {
109        if let Value::$scalar(v) = $value {
110            Some(StorageKey::$scalar(*v))
111        } else {
112            None
113        }
114    };
115    ( $value:expr, $scalar:ident, false ) => {
116        None
117    };
118}
119
120macro_rules! value_storage_key_from_registry {
121    ( @args $value:expr; @entries $( ($scalar:ident, $family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:tt) ),* $(,)? ) => {
122        {
123            let mut key = None;
124            $(
125                match key {
126                    Some(_) => {}
127                    None => {
128                        key = value_storage_key_case!($value, $scalar, $is_keyable);
129                    }
130                }
131            )*
132            key
133        }
134    };
135}
136
137impl Value {
138    ///
139    /// CONSTRUCTION
140    ///
141
142    /// Build a `Value::List` from a list literal.
143    ///
144    /// Intended for tests and inline construction.
145    /// Requires `Clone` because items are borrowed.
146    pub fn from_slice<T>(items: &[T]) -> Self
147    where
148        T: Into<Self> + Clone,
149    {
150        Self::List(items.iter().cloned().map(Into::into).collect())
151    }
152
153    /// Build a `Value::List` from owned items.
154    ///
155    /// This is the canonical constructor for query / DTO boundaries.
156    pub fn from_list<T>(items: Vec<T>) -> Self
157    where
158        T: Into<Self>,
159    {
160        Self::List(items.into_iter().map(Into::into).collect())
161    }
162
163    /// Build a `Value::Enum` from a domain enum using its explicit mapping.
164    pub fn from_enum<E: EnumValue>(value: E) -> Self {
165        Self::Enum(value.to_value_enum())
166    }
167
168    /// Build a strict enum value using the canonical path of `E`.
169    #[must_use]
170    pub fn enum_strict<E: Path>(variant: &str) -> Self {
171        Self::Enum(ValueEnum::strict::<E>(variant))
172    }
173
174    ///
175    /// TYPES
176    ///
177
178    /// Returns true if the value is one of the numeric-like variants
179    /// supported by numeric comparison/ordering.
180    #[must_use]
181    pub const fn is_numeric(&self) -> bool {
182        scalar_registry!(value_is_numeric_from_registry, self)
183    }
184
185    /// Returns true if the value is Text.
186    #[must_use]
187    pub const fn is_text(&self) -> bool {
188        matches!(self, Self::Text(_))
189    }
190
191    /// Returns true if the value is Unit (used for presence/null comparators).
192    #[must_use]
193    pub const fn is_unit(&self) -> bool {
194        matches!(self, Self::Unit)
195    }
196
197    #[must_use]
198    pub const fn is_scalar(&self) -> bool {
199        match self {
200            // definitely not scalar:
201            Self::List(_) | Self::Unit => false,
202            _ => true,
203        }
204    }
205
206    fn numeric_repr(&self) -> NumericRepr {
207        if let Some(d) = self.to_decimal() {
208            return NumericRepr::Decimal(d);
209        }
210        if let Some(f) = self.to_f64_lossless() {
211            return NumericRepr::F64(f);
212        }
213        NumericRepr::None
214    }
215
216    ///
217    /// CONVERSION
218    ///
219
220    /// NOTE:
221    /// `Unit` is intentionally treated as a valid storage key and indexable,
222    /// used for singleton tables and synthetic identity entities.
223    /// Only `None` and `Unsupported` are non-indexable.
224    #[must_use]
225    pub const fn as_storage_key(&self) -> Option<StorageKey> {
226        scalar_registry!(value_storage_key_from_registry, self)
227    }
228
229    #[must_use]
230    pub const fn as_text(&self) -> Option<&str> {
231        if let Self::Text(s) = self {
232            Some(s.as_str())
233        } else {
234            None
235        }
236    }
237
238    #[must_use]
239    pub const fn as_list(&self) -> Option<&[Self]> {
240        if let Self::List(xs) = self {
241            Some(xs.as_slice())
242        } else {
243            None
244        }
245    }
246
247    fn to_decimal(&self) -> Option<Decimal> {
248        match self {
249            Self::Decimal(d) => Some(*d),
250            Self::Duration(d) => Decimal::from_u64(d.get()),
251            Self::E8s(v) => Some(v.to_decimal()),
252            Self::E18s(v) => v.to_decimal(),
253            Self::Float64(f) => Decimal::from_f64(f.get()),
254            Self::Float32(f) => Decimal::from_f32(f.get()),
255            Self::Int(i) => Decimal::from_i64(*i),
256            Self::Int128(i) => Decimal::from_i128(i.get()),
257            Self::IntBig(i) => i.to_i128().and_then(Decimal::from_i128),
258            Self::Timestamp(t) => Decimal::from_u64(t.get()),
259            Self::Uint(u) => Decimal::from_u64(*u),
260            Self::Uint128(u) => Decimal::from_u128(u.get()),
261            Self::UintBig(u) => u.to_u128().and_then(Decimal::from_u128),
262
263            _ => None,
264        }
265    }
266
267    // it's lossless, trust me bro
268    #[allow(clippy::cast_precision_loss)]
269    fn to_f64_lossless(&self) -> Option<f64> {
270        match self {
271            Self::Duration(d) if d.get() <= F64_SAFE_U64 => Some(d.get() as f64),
272            Self::Float64(f) => Some(f.get()),
273            Self::Float32(f) => Some(f64::from(f.get())),
274            Self::Int(i) if (-F64_SAFE_I64..=F64_SAFE_I64).contains(i) => Some(*i as f64),
275            Self::Int128(i) if (-F64_SAFE_I128..=F64_SAFE_I128).contains(&i.get()) => {
276                Some(i.get() as f64)
277            }
278            Self::IntBig(i) => i.to_i128().and_then(|v| {
279                (-F64_SAFE_I128..=F64_SAFE_I128)
280                    .contains(&v)
281                    .then_some(v as f64)
282            }),
283            Self::Timestamp(t) if t.get() <= F64_SAFE_U64 => Some(t.get() as f64),
284            Self::Uint(u) if *u <= F64_SAFE_U64 => Some(*u as f64),
285            Self::Uint128(u) if u.get() <= F64_SAFE_U128 => Some(u.get() as f64),
286            Self::UintBig(u) => u
287                .to_u128()
288                .and_then(|v| (v <= F64_SAFE_U128).then_some(v as f64)),
289
290            _ => None,
291        }
292    }
293
294    /// Cross-type numeric comparison; returns None if non-numeric.
295    #[must_use]
296    pub fn cmp_numeric(&self, other: &Self) -> Option<Ordering> {
297        match (self.numeric_repr(), other.numeric_repr()) {
298            (NumericRepr::Decimal(a), NumericRepr::Decimal(b)) => a.partial_cmp(&b),
299            (NumericRepr::F64(a), NumericRepr::F64(b)) => a.partial_cmp(&b),
300            _ => None,
301        }
302    }
303
304    ///
305    /// TEXT COMPARISON
306    ///
307
308    fn fold_ci(s: &str) -> std::borrow::Cow<'_, str> {
309        if s.is_ascii() {
310            return std::borrow::Cow::Owned(s.to_ascii_lowercase());
311        }
312        // NOTE: Unicode fallback — temporary to_lowercase for non‑ASCII.
313        // Future: replace with proper NFKC + full casefold when available.
314        std::borrow::Cow::Owned(s.to_lowercase())
315    }
316
317    fn text_with_mode(s: &'_ str, mode: TextMode) -> std::borrow::Cow<'_, str> {
318        match mode {
319            TextMode::Cs => std::borrow::Cow::Borrowed(s),
320            TextMode::Ci => Self::fold_ci(s),
321        }
322    }
323
324    fn text_op(
325        &self,
326        other: &Self,
327        mode: TextMode,
328        f: impl Fn(&str, &str) -> bool,
329    ) -> Option<bool> {
330        let (a, b) = (self.as_text()?, other.as_text()?);
331        let a = Self::text_with_mode(a, mode);
332        let b = Self::text_with_mode(b, mode);
333        Some(f(&a, &b))
334    }
335
336    fn ci_key(&self) -> Option<String> {
337        match self {
338            Self::Text(s) => Some(Self::fold_ci(s).into_owned()),
339            Self::Ulid(u) => Some(u.to_string().to_ascii_lowercase()),
340            Self::Principal(p) => Some(p.to_string().to_ascii_lowercase()),
341            Self::Account(a) => Some(a.to_string().to_ascii_lowercase()),
342            _ => None,
343        }
344    }
345
346    fn eq_ci(a: &Self, b: &Self) -> bool {
347        if let (Some(ak), Some(bk)) = (a.ci_key(), b.ci_key()) {
348            return ak == bk;
349        }
350
351        a == b
352    }
353
354    fn normalize_list_ref(v: &Self) -> Vec<&Self> {
355        match v {
356            Self::List(vs) => vs.iter().collect(),
357            v => vec![v],
358        }
359    }
360
361    fn contains_by<F>(&self, needle: &Self, eq: F) -> Option<bool>
362    where
363        F: Fn(&Self, &Self) -> bool,
364    {
365        self.as_list()
366            .map(|items| items.iter().any(|v| eq(v, needle)))
367    }
368
369    #[allow(clippy::unnecessary_wraps)]
370    fn contains_any_by<F>(&self, needles: &Self, eq: F) -> Option<bool>
371    where
372        F: Fn(&Self, &Self) -> bool,
373    {
374        let needles = Self::normalize_list_ref(needles);
375        match self {
376            Self::List(items) => Some(needles.iter().any(|n| items.iter().any(|v| eq(v, n)))),
377            scalar => Some(needles.iter().any(|n| eq(scalar, n))),
378        }
379    }
380
381    #[allow(clippy::unnecessary_wraps)]
382    fn contains_all_by<F>(&self, needles: &Self, eq: F) -> Option<bool>
383    where
384        F: Fn(&Self, &Self) -> bool,
385    {
386        let needles = Self::normalize_list_ref(needles);
387        match self {
388            Self::List(items) => Some(needles.iter().all(|n| items.iter().any(|v| eq(v, n)))),
389            scalar => Some(needles.len() == 1 && eq(scalar, needles[0])),
390        }
391    }
392
393    fn in_list_by<F>(&self, haystack: &Self, eq: F) -> Option<bool>
394    where
395        F: Fn(&Self, &Self) -> bool,
396    {
397        if let Self::List(items) = haystack {
398            Some(items.iter().any(|h| eq(h, self)))
399        } else {
400            None
401        }
402    }
403
404    #[must_use]
405    /// Case-sensitive/insensitive equality check for text-like values.
406    pub fn text_eq(&self, other: &Self, mode: TextMode) -> Option<bool> {
407        self.text_op(other, mode, |a, b| a == b)
408    }
409
410    #[must_use]
411    /// Check whether `other` is a substring of `self` under the given text mode.
412    pub fn text_contains(&self, needle: &Self, mode: TextMode) -> Option<bool> {
413        self.text_op(needle, mode, |a, b| a.contains(b))
414    }
415
416    #[must_use]
417    /// Check whether `self` starts with `other` under the given text mode.
418    pub fn text_starts_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
419        self.text_op(needle, mode, |a, b| a.starts_with(b))
420    }
421
422    #[must_use]
423    /// Check whether `self` ends with `other` under the given text mode.
424    pub fn text_ends_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
425        self.text_op(needle, mode, |a, b| a.ends_with(b))
426    }
427
428    ///
429    /// EMPTY
430    ///
431
432    #[must_use]
433    pub const fn is_empty(&self) -> Option<bool> {
434        match self {
435            Self::List(xs) => Some(xs.is_empty()),
436            Self::Text(s) => Some(s.is_empty()),
437            Self::Blob(b) => Some(b.is_empty()),
438
439            //  fields represented as Value::None:
440            Self::None => Some(true),
441
442            _ => None,
443        }
444    }
445
446    #[must_use]
447    /// Logical negation of [`is_empty`](Self::is_empty).
448    pub fn is_not_empty(&self) -> Option<bool> {
449        self.is_empty().map(|b| !b)
450    }
451
452    ///
453    /// COLLECTIONS
454    ///
455
456    #[must_use]
457    /// Returns true if `self` contains `needle` (or equals it for scalars).
458    pub fn contains(&self, needle: &Self) -> Option<bool> {
459        self.contains_by(needle, |a, b| a == b)
460    }
461
462    #[must_use]
463    /// Returns true if any item in `needles` matches a member of `self`.
464    pub fn contains_any(&self, needles: &Self) -> Option<bool> {
465        self.contains_any_by(needles, |a, b| a == b)
466    }
467
468    #[must_use]
469    /// Returns true if every item in `needles` matches a member of `self`.
470    pub fn contains_all(&self, needles: &Self) -> Option<bool> {
471        self.contains_all_by(needles, |a, b| a == b)
472    }
473
474    #[must_use]
475    /// Returns true if `self` exists inside the provided list.
476    pub fn in_list(&self, haystack: &Self) -> Option<bool> {
477        self.in_list_by(haystack, |a, b| a == b)
478    }
479
480    #[must_use]
481    /// Case-insensitive `contains` supporting text and identifier variants.
482    pub fn contains_ci(&self, needle: &Self) -> Option<bool> {
483        match self {
484            Self::List(_) => self.contains_by(needle, Self::eq_ci),
485            _ => Some(Self::eq_ci(self, needle)),
486        }
487    }
488
489    #[must_use]
490    /// Case-insensitive variant of [`contains_any`](Self::contains_any).
491    pub fn contains_any_ci(&self, needles: &Self) -> Option<bool> {
492        self.contains_any_by(needles, Self::eq_ci)
493    }
494
495    #[must_use]
496    /// Case-insensitive variant of [`contains_all`](Self::contains_all).
497    pub fn contains_all_ci(&self, needles: &Self) -> Option<bool> {
498        self.contains_all_by(needles, Self::eq_ci)
499    }
500
501    #[must_use]
502    /// Case-insensitive variant of [`in_list`](Self::in_list).
503    pub fn in_list_ci(&self, haystack: &Self) -> Option<bool> {
504        self.in_list_by(haystack, Self::eq_ci)
505    }
506}
507
508impl FieldValue for Value {
509    fn to_value(&self) -> Value {
510        self.clone()
511    }
512}
513
514#[macro_export]
515macro_rules! impl_from_for {
516    ( $( $type:ty => $variant:ident ),* $(,)? ) => {
517        $(
518            impl From<$type> for Value {
519                fn from(v: $type) -> Self {
520                    Self::$variant(v.into())
521                }
522            }
523        )*
524    };
525}
526
527impl_from_for! {
528    Account    => Account,
529    Date       => Date,
530    Decimal    => Decimal,
531    Duration   => Duration,
532    E8s        => E8s,
533    E18s       => E18s,
534    bool       => Bool,
535    i8         => Int,
536    i16        => Int,
537    i32        => Int,
538    i64        => Int,
539    i128       => Int128,
540    Int        => IntBig,
541    Principal  => Principal,
542    Subaccount => Subaccount,
543    &str       => Text,
544    String     => Text,
545    Timestamp  => Timestamp,
546    u8         => Uint,
547    u16        => Uint,
548    u32        => Uint,
549    u64        => Uint,
550    u128       => Uint128,
551    Nat        => UintBig,
552    Ulid       => Ulid,
553}
554
555impl ValueFamilyExt for Value {
556    fn family(&self) -> ValueFamily {
557        match self {
558            // Numeric
559            Self::Date(_)
560            | Self::Decimal(_)
561            | Self::Duration(_)
562            | Self::E8s(_)
563            | Self::E18s(_)
564            | Self::Float32(_)
565            | Self::Float64(_)
566            | Self::Int(_)
567            | Self::Int128(_)
568            | Self::Timestamp(_)
569            | Self::Uint(_)
570            | Self::Uint128(_)
571            | Self::IntBig(_)
572            | Self::UintBig(_) => ValueFamily::Numeric,
573
574            // Text
575            Self::Text(_) => ValueFamily::Textual,
576
577            // Identifiers
578            Self::Ulid(_) | Self::Principal(_) | Self::Account(_) => ValueFamily::Identifier,
579
580            // Enum
581            Self::Enum(_) => ValueFamily::Enum,
582
583            // Collections
584            Self::List(_) => ValueFamily::Collection,
585
586            // Blobs
587            Self::Blob(_) | Self::Subaccount(_) => ValueFamily::Blob,
588
589            // Bool
590            Self::Bool(_) => ValueFamily::Bool,
591
592            // Null / Unit
593            Self::None => ValueFamily::Null,
594            Self::Unit => ValueFamily::Unit,
595
596            // Everything else
597            Self::Unsupported => ValueFamily::Unsupported,
598        }
599    }
600}
601
602impl<E: EntityIdentity> From<Ref<E>> for Value {
603    fn from(r: Ref<E>) -> Self {
604        r.to_value() // via FieldValue
605    }
606}
607
608impl From<Vec<Self>> for Value {
609    fn from(vec: Vec<Self>) -> Self {
610        Self::List(vec)
611    }
612}
613
614impl From<()> for Value {
615    fn from((): ()) -> Self {
616        Self::Unit
617    }
618}
619
620// NOTE:
621// Value::partial_cmp is NOT the canonical ordering for database semantics.
622// Some orderable scalar types (e.g. Account, Unit) intentionally do not
623// participate here. Use canonical_cmp / strict ordering for ORDER BY,
624// planning, and key-range validation.
625impl PartialOrd for Value {
626    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
627        match (self, other) {
628            (Self::Bool(a), Self::Bool(b)) => a.partial_cmp(b),
629            (Self::Date(a), Self::Date(b)) => a.partial_cmp(b),
630            (Self::Decimal(a), Self::Decimal(b)) => a.partial_cmp(b),
631            (Self::Duration(a), Self::Duration(b)) => a.partial_cmp(b),
632            (Self::E8s(a), Self::E8s(b)) => a.partial_cmp(b),
633            (Self::E18s(a), Self::E18s(b)) => a.partial_cmp(b),
634            (Self::Enum(a), Self::Enum(b)) => a.partial_cmp(b),
635            (Self::Float32(a), Self::Float32(b)) => a.partial_cmp(b),
636            (Self::Float64(a), Self::Float64(b)) => a.partial_cmp(b),
637            (Self::Int(a), Self::Int(b)) => a.partial_cmp(b),
638            (Self::Int128(a), Self::Int128(b)) => a.partial_cmp(b),
639            (Self::IntBig(a), Self::IntBig(b)) => a.partial_cmp(b),
640            (Self::Principal(a), Self::Principal(b)) => a.partial_cmp(b),
641            (Self::Subaccount(a), Self::Subaccount(b)) => a.partial_cmp(b),
642            (Self::Text(a), Self::Text(b)) => a.partial_cmp(b),
643            (Self::Timestamp(a), Self::Timestamp(b)) => a.partial_cmp(b),
644            (Self::Uint(a), Self::Uint(b)) => a.partial_cmp(b),
645            (Self::Uint128(a), Self::Uint128(b)) => a.partial_cmp(b),
646            (Self::UintBig(a), Self::UintBig(b)) => a.partial_cmp(b),
647            (Self::Ulid(a), Self::Ulid(b)) => a.partial_cmp(b),
648
649            // Cross-type comparisons: no ordering
650            _ => None,
651        }
652    }
653}
654
655///
656/// ValueEnum
657/// handles the Enum case; `path` is optional to allow strict (typed) or loose matching.
658///
659
660#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, PartialOrd, Serialize)]
661pub struct ValueEnum {
662    pub variant: String,
663    pub path: Option<String>,
664    pub payload: Option<Box<Value>>,
665}
666
667impl ValueEnum {
668    #[must_use]
669    /// Build a strict enum value matching the provided variant and path.
670    pub fn new(variant: &str, path: Option<&str>) -> Self {
671        Self {
672            variant: variant.to_string(),
673            path: path.map(ToString::to_string),
674            payload: None,
675        }
676    }
677
678    #[must_use]
679    /// Build a strict enum value using the canonical path of `E`.
680    pub fn strict<E: Path>(variant: &str) -> Self {
681        Self::new(variant, Some(E::PATH))
682    }
683
684    #[must_use]
685    /// Build a strict enum value from a domain enum using its explicit mapping.
686    pub fn from_enum<E: EnumValue>(value: E) -> Self {
687        value.to_value_enum()
688    }
689
690    #[must_use]
691    /// Build an enum value that ignores the path for loose matching.
692    pub fn loose(variant: &str) -> Self {
693        Self::new(variant, None)
694    }
695
696    #[must_use]
697    /// Attach an enum payload (used for data-carrying variants).
698    pub fn with_payload(mut self, payload: Value) -> Self {
699        self.payload = Some(Box::new(payload));
700        self
701    }
702}