icydb_core/value/
mod.rs

1mod bytes;
2mod family;
3mod tests;
4
5use crate::{
6    Key,
7    traits::{FieldValue, NumFromPrimitive},
8    types::*,
9};
10use candid::CandidType;
11use num_traits::ToPrimitive;
12use serde::{Deserialize, Serialize};
13use std::cmp::Ordering;
14
15pub use family::{ValueFamily, ValueFamilyExt};
16
17///
18/// CONSTANTS
19///
20
21const F64_SAFE_I64: i64 = 1i64 << 53;
22const F64_SAFE_U64: u64 = 1u64 << 53;
23const F64_SAFE_I128: i128 = 1i128 << 53;
24const F64_SAFE_U128: u128 = 1u128 << 53;
25
26///
27/// TextMode
28///
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub enum TextMode {
32    Cs, // case-sensitive
33    Ci, // case-insensitive
34}
35
36///
37/// ValueEnum
38/// handles the Enum case
39///
40
41#[derive(CandidType, Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
42pub struct ValueEnum {
43    pub path: String,
44    pub variant: String,
45}
46
47impl ValueEnum {
48    #[must_use]
49    pub fn new(path: &str, variant: &str) -> Self {
50        Self {
51            path: path.to_string(),
52            variant: variant.to_string(),
53        }
54    }
55}
56
57///
58/// Value
59/// can be used in WHERE statements
60///
61/// None        → the field’s value is Option::None (i.e., SQL NULL).
62/// Unit        → internal placeholder for RHS; not a real value.
63/// Unsupported → the field exists but isn’t filterable/indexable.
64///
65
66#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
67pub enum Value {
68    Account(Account),
69    Blob(Vec<u8>),
70    Bool(bool),
71    Date(Date),
72    Decimal(Decimal),
73    Duration(Duration),
74    Enum(ValueEnum),
75    E8s(E8s),
76    E18s(E18s),
77    Float32(Float32),
78    Float64(Float64),
79    Int(i64),
80    Int128(Int128),
81    IntBig(Int),
82    List(Vec<Value>),
83    None,
84    Principal(Principal),
85    Subaccount(Subaccount),
86    Text(String),
87    Timestamp(Timestamp),
88    Uint(u64),
89    Uint128(Nat128),
90    UintBig(Nat),
91    Ulid(Ulid),
92    Unit,
93    Unsupported,
94}
95
96impl Value {
97    ///
98    /// CONSTRUCTION
99    ///
100
101    pub fn from_list<T: Into<Self> + Clone>(items: &[T]) -> Self {
102        Self::List(items.iter().cloned().map(Into::into).collect())
103    }
104
105    ///
106    /// TYPES
107    ///
108
109    /// Returns true if the value is one of the numeric-like variants
110    /// supported by numeric comparison/ordering.
111    #[must_use]
112    pub const fn is_numeric(&self) -> bool {
113        matches!(
114            self,
115            Self::Decimal(_)
116                | Self::Duration(_)
117                | Self::E8s(_)
118                | Self::E18s(_)
119                | Self::Float32(_)
120                | Self::Float64(_)
121                | Self::Int(_)
122                | Self::Int128(_)
123                | Self::Timestamp(_)
124                | Self::Uint(_)
125                | Self::Uint128(_)
126        )
127    }
128
129    /// Returns true if the value is Text.
130    #[must_use]
131    pub const fn is_text(&self) -> bool {
132        matches!(self, Self::Text(_))
133    }
134
135    /// Returns true if the value is Unit (used for presence/null comparators).
136    #[must_use]
137    pub const fn is_unit(&self) -> bool {
138        matches!(self, Self::Unit)
139    }
140
141    #[must_use]
142    pub const fn is_scalar(&self) -> bool {
143        match self {
144            // definitely not scalar:
145            Self::List(_) | Self::Unit => false,
146            _ => true,
147        }
148    }
149
150    ///
151    /// HASHING
152    ///
153
154    #[must_use]
155    pub const fn tag(&self) -> u8 {
156        match self {
157            Self::Account(_) => ValueTag::Account,
158            Self::Blob(_) => ValueTag::Blob,
159            Self::Bool(_) => ValueTag::Bool,
160            Self::Date(_) => ValueTag::Date,
161            Self::Decimal(_) => ValueTag::Decimal,
162            Self::Duration(_) => ValueTag::Duration,
163            Self::Enum(_) => ValueTag::Enum,
164            Self::E8s(_) => ValueTag::E8s,
165            Self::E18s(_) => ValueTag::E18s,
166            Self::Float32(_) => ValueTag::Float32,
167            Self::Float64(_) => ValueTag::Float64,
168            Self::Int(_) => ValueTag::Int,
169            Self::Int128(_) => ValueTag::Int128,
170            Self::IntBig(_) => ValueTag::IntBig,
171            Self::List(_) => ValueTag::List,
172            Self::None => ValueTag::None,
173            Self::Principal(_) => ValueTag::Principal,
174            Self::Subaccount(_) => ValueTag::Subaccount,
175            Self::Text(_) => ValueTag::Text,
176            Self::Timestamp(_) => ValueTag::Timestamp,
177            Self::Uint(_) => ValueTag::Uint,
178            Self::Uint128(_) => ValueTag::Uint128,
179            Self::UintBig(_) => ValueTag::UintBig,
180            Self::Ulid(_) => ValueTag::Ulid,
181            Self::Unit => ValueTag::Unit,
182            Self::Unsupported => ValueTag::Unsupported,
183        }
184        .to_u8()
185    }
186
187    ///
188    /// CONVERSION
189    ///
190
191    #[must_use]
192    pub const fn as_key(&self) -> Option<Key> {
193        match self {
194            Self::Account(v) => Some(Key::Account(*v)),
195            Self::Int(v) => Some(Key::Int(*v)),
196            Self::Uint(v) => Some(Key::Uint(*v)),
197            Self::Principal(v) => Some(Key::Principal(*v)),
198            Self::Subaccount(v) => Some(Key::Subaccount(*v)),
199            Self::Ulid(v) => Some(Key::Ulid(*v)),
200            Self::Unit => Some(Key::Unit),
201            _ => None,
202        }
203    }
204
205    #[must_use]
206    pub const fn as_text(&self) -> Option<&str> {
207        if let Self::Text(s) = self {
208            Some(s.as_str())
209        } else {
210            None
211        }
212    }
213
214    #[must_use]
215    pub const fn as_list(&self) -> Option<&[Self]> {
216        if let Self::List(xs) = self {
217            Some(xs.as_slice())
218        } else {
219            None
220        }
221    }
222
223    fn to_decimal(&self) -> Option<Decimal> {
224        match self {
225            Self::Decimal(d) => Some(*d),
226            Self::Duration(d) => Decimal::from_u64(d.get()),
227            Self::E8s(v) => Some(v.to_decimal()),
228            Self::E18s(v) => Some(v.to_decimal()),
229            Self::Float64(f) => Decimal::from_f64(f.get()),
230            Self::Float32(f) => Decimal::from_f32(f.get()),
231            Self::Int(i) => Decimal::from_i64(*i),
232            Self::Int128(i) => Decimal::from_i128(i.get()),
233            Self::IntBig(i) => i.0.to_i128().and_then(Decimal::from_i128),
234            Self::Timestamp(t) => Decimal::from_u64(t.get()),
235            Self::Uint(u) => Decimal::from_u64(*u),
236            Self::Uint128(u) => Decimal::from_u128(u.get()),
237            Self::UintBig(u) => u.0.to_u128().and_then(Decimal::from_u128),
238
239            _ => None,
240        }
241    }
242
243    // it's lossless, trust me bro
244    #[allow(clippy::cast_precision_loss)]
245    fn to_f64_lossless(&self) -> Option<f64> {
246        match self {
247            Self::Duration(d) if d.get() <= F64_SAFE_U64 => Some(d.get() as f64),
248            Self::Float64(f) => Some(f.get()),
249            Self::Float32(f) => Some(f64::from(f.get())),
250            Self::Int(i) if (-F64_SAFE_I64..=F64_SAFE_I64).contains(i) => Some(*i as f64),
251            Self::Int128(i) if (-F64_SAFE_I128..=F64_SAFE_I128).contains(&i.get()) => {
252                Some(i.get() as f64)
253            }
254            Self::IntBig(i) => i.0.to_i128().and_then(|v| {
255                (-F64_SAFE_I128..=F64_SAFE_I128)
256                    .contains(&v)
257                    .then_some(v as f64)
258            }),
259            Self::Timestamp(t) if t.get() <= F64_SAFE_U64 => Some(t.get() as f64),
260            Self::Uint(u) if *u <= F64_SAFE_U64 => Some(*u as f64),
261            Self::Uint128(u) if u.get() <= F64_SAFE_U128 => Some(u.get() as f64),
262            Self::UintBig(u) => {
263                u.0.to_u128()
264                    .and_then(|v| (v <= F64_SAFE_U128).then_some(v as f64))
265            }
266
267            _ => None,
268        }
269    }
270
271    #[must_use]
272    pub fn to_index_fingerprint(&self) -> Option<[u8; 16]> {
273        match self {
274            Self::None | Self::Unsupported => None,
275            _ => Some(self.hash_value()),
276        }
277    }
278
279    /// Cross-type numeric comparison; returns None if non-numeric.
280    #[must_use]
281    pub fn cmp_numeric(&self, other: &Self) -> Option<Ordering> {
282        if let (Some(a), Some(b)) = (self.to_decimal(), other.to_decimal()) {
283            return a.partial_cmp(&b);
284        }
285        if let (Some(a), Some(b)) = (self.to_f64_lossless(), other.to_f64_lossless()) {
286            return a.partial_cmp(&b);
287        }
288
289        None
290    }
291
292    ///
293    /// TEXT COMPARISON
294    ///
295
296    #[inline]
297    fn fold_ci(s: &str) -> std::borrow::Cow<'_, str> {
298        if s.is_ascii() {
299            return std::borrow::Cow::Owned(s.to_ascii_lowercase());
300        }
301        // NOTE: Unicode fallback — temporary to_lowercase for non‑ASCII.
302        // Future: replace with proper NFKC + full casefold when available.
303        std::borrow::Cow::Owned(s.to_lowercase())
304    }
305
306    #[inline]
307    fn text_with_mode(s: &'_ str, mode: TextMode) -> std::borrow::Cow<'_, str> {
308        match mode {
309            TextMode::Cs => std::borrow::Cow::Borrowed(s),
310            TextMode::Ci => Self::fold_ci(s),
311        }
312    }
313
314    #[inline]
315    fn text_op(
316        &self,
317        other: &Self,
318        mode: TextMode,
319        f: impl Fn(&str, &str) -> bool,
320    ) -> Option<bool> {
321        let (a, b) = (self.as_text()?, other.as_text()?);
322        let a = Self::text_with_mode(a, mode);
323        let b = Self::text_with_mode(b, mode);
324        Some(f(&a, &b))
325    }
326
327    #[inline]
328    fn eq_ci(a: &Self, b: &Self) -> bool {
329        match (a, b) {
330            (Self::Text(x), Self::Text(y)) => Self::fold_ci(x) == Self::fold_ci(y),
331            _ => a == b,
332        }
333    }
334
335    #[inline]
336    fn normalize_list_ref(v: &Self) -> Vec<&Self> {
337        match v {
338            Self::List(vs) => vs.iter().collect(),
339            v => vec![v],
340        }
341    }
342
343    #[inline]
344    fn contains_by<F>(&self, needle: &Self, eq: F) -> Option<bool>
345    where
346        F: Fn(&Self, &Self) -> bool,
347    {
348        self.as_list()
349            .map(|items| items.iter().any(|v| eq(v, needle)))
350    }
351
352    #[inline]
353    #[allow(clippy::unnecessary_wraps)]
354    fn contains_any_by<F>(&self, needles: &Self, eq: F) -> Option<bool>
355    where
356        F: Fn(&Self, &Self) -> bool,
357    {
358        let needles = Self::normalize_list_ref(needles);
359        match self {
360            Self::List(items) => Some(needles.iter().any(|n| items.iter().any(|v| eq(v, n)))),
361            scalar => Some(needles.iter().any(|n| eq(scalar, n))),
362        }
363    }
364
365    #[inline]
366    #[allow(clippy::unnecessary_wraps)]
367    fn contains_all_by<F>(&self, needles: &Self, eq: F) -> Option<bool>
368    where
369        F: Fn(&Self, &Self) -> bool,
370    {
371        let needles = Self::normalize_list_ref(needles);
372        match self {
373            Self::List(items) => Some(needles.iter().all(|n| items.iter().any(|v| eq(v, n)))),
374            scalar => Some(needles.len() == 1 && eq(scalar, needles[0])),
375        }
376    }
377
378    #[inline]
379    fn in_list_by<F>(&self, haystack: &Self, eq: F) -> Option<bool>
380    where
381        F: Fn(&Self, &Self) -> bool,
382    {
383        if let Self::List(items) = haystack {
384            Some(items.iter().any(|h| eq(h, self)))
385        } else {
386            None
387        }
388    }
389
390    #[must_use]
391    pub fn text_eq(&self, other: &Self, mode: TextMode) -> Option<bool> {
392        self.text_op(other, mode, |a, b| a == b)
393    }
394
395    #[must_use]
396    pub fn text_contains(&self, needle: &Self, mode: TextMode) -> Option<bool> {
397        self.text_op(needle, mode, |a, b| a.contains(b))
398    }
399
400    #[must_use]
401    pub fn text_starts_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
402        self.text_op(needle, mode, |a, b| a.starts_with(b))
403    }
404
405    #[must_use]
406    pub fn text_ends_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
407        self.text_op(needle, mode, |a, b| a.ends_with(b))
408    }
409
410    ///
411    /// EMPTY
412    ///
413
414    #[must_use]
415    pub const fn is_empty(&self) -> Option<bool> {
416        match self {
417            Self::List(xs) => Some(xs.is_empty()),
418            Self::Text(s) => Some(s.is_empty()),
419            Self::Blob(b) => Some(b.is_empty()),
420
421            // For Option<T> fields represented as Value::None:
422            Self::None => Some(true),
423
424            _ => None,
425        }
426    }
427
428    #[must_use]
429    pub fn is_not_empty(&self) -> Option<bool> {
430        self.is_empty().map(|b| !b)
431    }
432
433    ///
434    /// COLLECTIONS
435    ///
436
437    #[must_use]
438    pub fn contains(&self, needle: &Self) -> Option<bool> {
439        self.contains_by(needle, |a, b| a == b)
440    }
441
442    #[must_use]
443    pub fn contains_any(&self, needles: &Self) -> Option<bool> {
444        self.contains_any_by(needles, |a, b| a == b)
445    }
446
447    #[must_use]
448    pub fn contains_all(&self, needles: &Self) -> Option<bool> {
449        self.contains_all_by(needles, |a, b| a == b)
450    }
451
452    #[must_use]
453    pub fn in_list(&self, haystack: &Self) -> Option<bool> {
454        self.in_list_by(haystack, |a, b| a == b)
455    }
456
457    #[must_use]
458    pub fn contains_ci(&self, needle: &Self) -> Option<bool> {
459        // Precompute folded needle and capture in comparator
460        let folded_needle = match needle {
461            Self::Text(b) => Some(Self::fold_ci(b)),
462            _ => None,
463        };
464        self.contains_by(needle, |a, b| match (a, b, &folded_needle) {
465            (Self::Text(x), Self::Text(_), Some(bf)) => Self::fold_ci(x) == *bf,
466            _ => a == b,
467        })
468    }
469
470    #[must_use]
471    pub fn contains_any_ci(&self, needles: &Self) -> Option<bool> {
472        self.contains_any_by(needles, Self::eq_ci)
473    }
474
475    #[must_use]
476    pub fn contains_all_ci(&self, needles: &Self) -> Option<bool> {
477        self.contains_all_by(needles, Self::eq_ci)
478    }
479
480    #[must_use]
481    pub fn in_list_ci(&self, haystack: &Self) -> Option<bool> {
482        // Precompute folded self and capture in comparator
483        let folded_self = match self {
484            Self::Text(b) => Some(Self::fold_ci(b)),
485            _ => None,
486        };
487        self.in_list_by(haystack, |a, b| match (a, b, &folded_self) {
488            (Self::Text(x), Self::Text(_), Some(sf)) => Self::fold_ci(x) == *sf,
489            _ => a == b,
490        })
491    }
492}
493
494#[macro_export]
495macro_rules! impl_from_for {
496    ( $( $type:ty => $variant:ident ),* $(,)? ) => {
497        $(
498            impl From<$type> for Value {
499                fn from(v: $type) -> Self {
500                    Self::$variant(v.into())
501                }
502            }
503        )*
504    };
505}
506
507impl_from_for! {
508    Account    => Account,
509    Date       => Date,
510    Decimal    => Decimal,
511    Duration   => Duration,
512    E8s        => E8s,
513    E18s       => E18s,
514    bool       => Bool,
515    i8         => Int,
516    i16        => Int,
517    i32        => Int,
518    i64        => Int,
519    i128       => Int128,
520    Int        => IntBig,
521    Principal  => Principal,
522    Subaccount => Subaccount,
523    &str       => Text,
524    String     => Text,
525    Timestamp  => Timestamp,
526    u8         => Uint,
527    u16        => Uint,
528    u32        => Uint,
529    u64        => Uint,
530    u128       => Uint128,
531    Nat        => UintBig,
532    Ulid       => Ulid,
533}
534
535impl ValueFamilyExt for Value {
536    fn family(&self) -> ValueFamily {
537        match self {
538            // Numeric
539            Self::Date(_)
540            | Self::Decimal(_)
541            | Self::Duration(_)
542            | Self::E8s(_)
543            | Self::E18s(_)
544            | Self::Float32(_)
545            | Self::Float64(_)
546            | Self::Int(_)
547            | Self::Int128(_)
548            | Self::Timestamp(_)
549            | Self::Uint(_)
550            | Self::Uint128(_)
551            | Self::IntBig(_)
552            | Self::UintBig(_) => ValueFamily::Numeric,
553
554            // Text
555            Self::Text(_) => ValueFamily::Textual,
556
557            // Identifiers
558            Self::Ulid(_) | Self::Principal(_) | Self::Account(_) => ValueFamily::Identifier,
559
560            // Enum
561            Self::Enum(_) => ValueFamily::Enum,
562
563            // Collections
564            Self::List(_) => ValueFamily::Collection,
565
566            // Blobs
567            Self::Blob(_) | Self::Subaccount(_) => ValueFamily::Blob,
568
569            // Bool
570            Self::Bool(_) => ValueFamily::Bool,
571
572            // Null / Unit
573            Self::None => ValueFamily::Null,
574            Self::Unit => ValueFamily::Unit,
575
576            // Everything else
577            Self::Unsupported => ValueFamily::Unsupported,
578        }
579    }
580}
581
582impl FieldValue for Value {
583    fn to_value(&self) -> Value {
584        self.clone()
585    }
586}
587
588impl From<Vec<Self>> for Value {
589    fn from(vec: Vec<Self>) -> Self {
590        Self::from_list(&vec)
591    }
592}
593
594impl From<()> for Value {
595    fn from((): ()) -> Self {
596        Self::Unit
597    }
598}
599
600impl PartialOrd for Value {
601    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
602        match (self, other) {
603            (Self::Bool(a), Self::Bool(b)) => a.partial_cmp(b),
604            (Self::Date(a), Self::Date(b)) => a.partial_cmp(b),
605            (Self::Decimal(a), Self::Decimal(b)) => a.partial_cmp(b),
606            (Self::Duration(a), Self::Duration(b)) => a.partial_cmp(b),
607            (Self::E8s(a), Self::E8s(b)) => a.partial_cmp(b),
608            (Self::E18s(a), Self::E18s(b)) => a.partial_cmp(b),
609            (Self::Float32(a), Self::Float32(b)) => a.partial_cmp(b),
610            (Self::Float64(a), Self::Float64(b)) => a.partial_cmp(b),
611            (Self::Int(a), Self::Int(b)) => a.partial_cmp(b),
612            (Self::Int128(a), Self::Int128(b)) => a.partial_cmp(b),
613            (Self::IntBig(a), Self::IntBig(b)) => a.partial_cmp(b),
614            (Self::Principal(a), Self::Principal(b)) => a.partial_cmp(b),
615            (Self::Subaccount(a), Self::Subaccount(b)) => a.partial_cmp(b),
616            (Self::Text(a), Self::Text(b)) => a.partial_cmp(b),
617            (Self::Timestamp(a), Self::Timestamp(b)) => a.partial_cmp(b),
618            (Self::Uint(a), Self::Uint(b)) => a.partial_cmp(b),
619            (Self::Uint128(a), Self::Uint128(b)) => a.partial_cmp(b),
620            (Self::UintBig(a), Self::UintBig(b)) => a.partial_cmp(b),
621            (Self::Ulid(a), Self::Ulid(b)) => a.partial_cmp(b),
622
623            // Cross-type comparisons: no ordering
624            _ => None,
625        }
626    }
627}
628
629///
630/// ValueTag
631///
632
633#[repr(u8)]
634#[derive(Clone, Copy, Debug, Eq, PartialEq)]
635pub enum ValueTag {
636    Account = 1,
637    Blob = 2,
638    Bool = 3,
639    Date = 4,
640    Decimal = 5,
641    Duration = 6,
642    Enum = 7,
643    E8s = 8,
644    E18s = 9,
645    Float32 = 10,
646    Float64 = 11,
647    Int = 12,
648    Int128 = 13,
649    IntBig = 14,
650    List = 15,
651    None = 16,
652    Principal = 17,
653    Subaccount = 18,
654    Text = 19,
655    Timestamp = 20,
656    Uint = 21,
657    Uint128 = 22,
658    UintBig = 23,
659    Ulid = 24,
660    Unit = 25,
661    Unsupported = 26,
662}
663
664impl ValueTag {
665    #[must_use]
666    pub const fn to_u8(self) -> u8 {
667        self as u8
668    }
669}