Skip to main content

icydb_core/value/
mod.rs

1//! Module: value
2//!
3//! Responsibility: canonical dynamic values and public boundary conversion.
4//! Does not own: planner semantics, primary-key encoding, or persisted decode policy.
5//! Boundary: shared value/domain surface used by query, executor, and storage layers.
6//!
7//! `Value` is the runtime canonical value model. Public canister/query boundaries
8//! should prefer `InputValue` for caller-supplied literals and `OutputValue` for
9//! result payloads, so API surfaces do not depend on runtime execution internals.
10
11mod canonical;
12mod canonical_enum;
13mod coercion;
14mod compare;
15#[cfg(any(test, feature = "query"))]
16mod hash;
17mod input;
18mod map;
19pub(crate) mod ops;
20mod output;
21mod rank;
22mod semantics;
23mod tag;
24mod wire;
25
26#[cfg(test)]
27mod tests;
28
29use crate::types::*;
30use serde::{Deserialize, Deserializer, de};
31use std::{cmp::Ordering, fmt};
32
33// re-exports
34pub(crate) use canonical::canonicalize_value_set;
35pub(crate) use canonical_enum::{CanonicalEnumBody, CanonicalEnumValue, EnumTypeId, EnumVariantId};
36pub use coercion::CoercionFamily;
37#[cfg(test)]
38pub(crate) use hash::with_test_hash_override;
39#[cfg(any(test, feature = "query"))]
40pub(crate) use hash::{ValueHashWriter, hash_single_list_identity_canonical_value, hash_value};
41pub use input::{InputValue, InputValueEnum};
42pub use map::{MapValueError, SchemaInvariantError};
43pub use output::{OutputValue, OutputValueEnum, render_output_value_text};
44pub use tag::ValueTag;
45
46//
47// CONSTANTS
48//
49
50const VALUE_WIRE_TYPE_NAME: &str = "Value";
51const VALUE_WIRE_VARIANT_LABELS: &[&str] = &[
52    "Account",
53    "Blob",
54    "Bool",
55    "Date",
56    "Decimal",
57    "Duration",
58    "Enum",
59    "Float32",
60    "Float64",
61    "Int",
62    "Int128",
63    "IntBig",
64    "List",
65    "Map",
66    "Null",
67    "Principal",
68    "Subaccount",
69    "Text",
70    "Timestamp",
71    "Nat",
72    "Nat128",
73    "NatBig",
74    "Ulid",
75    "Unit",
76];
77
78// Name and discriminant owner for the stable `Value` serde wire shape.
79#[derive(Clone, Copy)]
80enum ValueWireVariant {
81    Account,
82    Blob,
83    Bool,
84    Date,
85    Decimal,
86    Duration,
87    Enum,
88    Float32,
89    Float64,
90    Int64,
91    Int128,
92    IntBig,
93    List,
94    Map,
95    Null,
96    Principal,
97    Subaccount,
98    Text,
99    Timestamp,
100    Nat64,
101    Nat128,
102    NatBig,
103    Ulid,
104    Unit,
105}
106
107impl ValueWireVariant {
108    // Resolve one stable serde variant label back to its runtime discriminant.
109    fn from_label(label: &str) -> Option<Self> {
110        match label {
111            "Account" => Some(Self::Account),
112            "Blob" => Some(Self::Blob),
113            "Bool" => Some(Self::Bool),
114            "Date" => Some(Self::Date),
115            "Decimal" => Some(Self::Decimal),
116            "Duration" => Some(Self::Duration),
117            "Enum" => Some(Self::Enum),
118            "Float32" => Some(Self::Float32),
119            "Float64" => Some(Self::Float64),
120            "Int" => Some(Self::Int64),
121            "Int128" => Some(Self::Int128),
122            "IntBig" => Some(Self::IntBig),
123            "List" => Some(Self::List),
124            "Map" => Some(Self::Map),
125            "Null" => Some(Self::Null),
126            "Principal" => Some(Self::Principal),
127            "Subaccount" => Some(Self::Subaccount),
128            "Text" => Some(Self::Text),
129            "Timestamp" => Some(Self::Timestamp),
130            "Nat" => Some(Self::Nat64),
131            "Nat128" => Some(Self::Nat128),
132            "NatBig" => Some(Self::NatBig),
133            "Ulid" => Some(Self::Ulid),
134            "Unit" => Some(Self::Unit),
135            _ => None,
136        }
137    }
138}
139
140//
141// TextMode
142//
143
144#[derive(Clone, Copy, Debug, Eq, PartialEq)]
145pub enum TextMode {
146    Cs, // case-sensitive
147    Ci, // case-insensitive
148}
149
150//
151// Value
152//
153// Runtime-only dynamic value used by query evaluation, SQL expressions,
154// projection materialization, predicates, cursor payloads, and intermediate
155// execution state.
156//
157// Value is intentionally not a persisted field type. Schema persistence must
158// admit it through an accepted field contract before selecting a storage codec.
159//
160// Null        → the field’s value is Option::None (i.e., SQL NULL).
161// Unit        → internal placeholder for RHS; not a real value.
162//
163#[derive(Clone, Eq, PartialEq)]
164pub enum Value {
165    Account(Account),
166    Blob(Vec<u8>),
167    Bool(bool),
168    Date(Date),
169    Decimal(Decimal),
170    Duration(Duration),
171    Enum(ValueEnum),
172    Float32(Float32),
173    Float64(Float64),
174    Int64(i64),
175    Int128(i128),
176    IntBig(IntBig),
177    /// Ordered list of values.
178    /// Used for many-cardinality transport.
179    /// List order is preserved for normalization and fingerprints.
180    List(Vec<Self>),
181    /// Canonical deterministic map representation.
182    ///
183    /// - Maps are unordered values; insertion order is discarded.
184    /// - Entries are always sorted by canonical key order and keys are unique.
185    /// - Map fields remain non-queryable and persist as atomic value replacements.
186    /// - Persistence treats map fields as atomic value replacements per row save.
187    Map(Vec<(Self, Self)>),
188    Null,
189    Principal(Principal),
190    Subaccount(Subaccount),
191    Text(String),
192    Timestamp(Timestamp),
193    Nat64(u64),
194    Nat128(u128),
195    NatBig(NatBig),
196    Ulid(Ulid),
197    Unit,
198}
199
200impl fmt::Debug for Value {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        match self {
203            Self::Account(value) => f.debug_tuple("Account").field(value).finish(),
204            Self::Blob(value) => write!(f, "Blob({} bytes)", value.len()),
205            Self::Bool(value) => f.debug_tuple("Bool").field(value).finish(),
206            Self::Date(value) => f.debug_tuple("Date").field(value).finish(),
207            Self::Decimal(value) => f.debug_tuple("Decimal").field(value).finish(),
208            Self::Duration(value) => f.debug_tuple("Duration").field(value).finish(),
209            Self::Enum(value) => f.debug_tuple("Enum").field(value).finish(),
210            Self::Float32(value) => f.debug_tuple("Float32").field(value).finish(),
211            Self::Float64(value) => f.debug_tuple("Float64").field(value).finish(),
212            Self::Int64(value) => f.debug_tuple("Int64").field(value).finish(),
213            Self::Int128(value) => f.debug_tuple("Int128").field(value).finish(),
214            Self::IntBig(value) => f.debug_tuple("IntBig").field(value).finish(),
215            Self::List(value) => f.debug_tuple("List").field(value).finish(),
216            Self::Map(value) => f.debug_tuple("Map").field(value).finish(),
217            Self::Null => f.write_str("Null"),
218            Self::Principal(value) => f.debug_tuple("Principal").field(value).finish(),
219            Self::Subaccount(value) => f.debug_tuple("Subaccount").field(value).finish(),
220            Self::Text(value) => f.debug_tuple("Text").field(value).finish(),
221            Self::Timestamp(value) => f.debug_tuple("Timestamp").field(value).finish(),
222            Self::Nat64(value) => f.debug_tuple("Nat64").field(value).finish(),
223            Self::Nat128(value) => f.debug_tuple("Nat128").field(value).finish(),
224            Self::NatBig(value) => f.debug_tuple("NatBig").field(value).finish(),
225            Self::Ulid(value) => f.debug_tuple("Ulid").field(value).finish(),
226            Self::Unit => f.write_str("Unit"),
227        }
228    }
229}
230
231impl Value {
232    ///
233    /// CONSTRUCTION
234    ///
235
236    /// Build a `Value::List` from a list literal.
237    ///
238    /// Intended for tests and inline construction.
239    /// Requires `Clone` because items are borrowed.
240    pub fn from_slice<T>(items: &[T]) -> Self
241    where
242        T: Into<Self> + Clone,
243    {
244        Self::List(items.iter().cloned().map(Into::into).collect())
245    }
246
247    /// Build a `Value::List` from owned items.
248    ///
249    /// This is the canonical constructor for query / DTO boundaries.
250    pub fn from_list<T>(items: Vec<T>) -> Self
251    where
252        T: Into<Self>,
253    {
254        Self::List(items.into_iter().map(Into::into).collect())
255    }
256
257    /// Build a canonical `Value::Map` from owned key/value entries.
258    ///
259    /// Invariants are validated and entries are normalized:
260    /// - keys must be scalar and non-null
261    /// - values may be scalar or structured
262    /// - entries are sorted by canonical key order
263    /// - duplicate keys are rejected
264    pub fn from_map(entries: Vec<(Self, Self)>) -> Result<Self, MapValueError> {
265        let normalized = map::normalize_map_entries(entries)?;
266        Ok(Self::Map(normalized))
267    }
268
269    ///
270    /// TYPES
271    ///
272
273    /// Returns true if the value is Text.
274    #[must_use]
275    pub const fn is_text(&self) -> bool {
276        matches!(self, Self::Text(_))
277    }
278
279    #[must_use]
280    pub const fn is_scalar(&self) -> bool {
281        match self {
282            // definitely not scalar:
283            Self::List(_) | Self::Map(_) | Self::Unit => false,
284            _ => true,
285        }
286    }
287
288    /// Return whether this runtime value contains canonical enum identity.
289    #[must_use]
290    #[cfg(any(test, feature = "query"))]
291    pub(crate) fn contains_enum(&self) -> bool {
292        match self {
293            Self::Enum(_) => true,
294            Self::List(values) => values.iter().any(Self::contains_enum),
295            Self::Map(entries) => entries
296                .iter()
297                .any(|(key, value)| key.contains_enum() || value.contains_enum()),
298            _ => false,
299        }
300    }
301
302    /// Stable canonical variant tag used by hash/fingerprint encodings.
303    #[must_use]
304    pub(crate) const fn canonical_tag(&self) -> ValueTag {
305        tag::canonical_tag(self)
306    }
307
308    /// Stable canonical rank used by all cross-variant ordering surfaces.
309    #[must_use]
310    pub(crate) const fn canonical_rank(&self) -> u8 {
311        rank::canonical_rank(self)
312    }
313
314    /// Total canonical comparator used by planner/predicate/fingerprint surfaces.
315    #[must_use]
316    pub(crate) fn canonical_cmp(left: &Self, right: &Self) -> Ordering {
317        compare::canonical_cmp(left, right)
318    }
319
320    /// Total canonical comparator used for map-key normalization.
321    #[must_use]
322    pub(crate) fn canonical_cmp_key(left: &Self, right: &Self) -> Ordering {
323        compare::canonical_cmp_key(left, right)
324    }
325
326    ///
327    /// CONVERSION
328    ///
329
330    #[must_use]
331    pub const fn as_text(&self) -> Option<&str> {
332        if let Self::Text(s) = self {
333            Some(s.as_str())
334        } else {
335            None
336        }
337    }
338
339    #[must_use]
340    pub const fn as_list(&self) -> Option<&[Self]> {
341        if let Self::List(xs) = self {
342            Some(xs.as_slice())
343        } else {
344            None
345        }
346    }
347
348    #[must_use]
349    pub const fn as_map(&self) -> Option<&[(Self, Self)]> {
350        if let Self::Map(entries) = self {
351            Some(entries.as_slice())
352        } else {
353            None
354        }
355    }
356}
357
358macro_rules! impl_from_for {
359    ( $( $type:ty => $variant:ident ),* $(,)? ) => {
360        $(
361            impl From<$type> for Value {
362                fn from(v: $type) -> Self {
363                    Self::$variant(v.into())
364                }
365            }
366        )*
367    };
368}
369
370impl_from_for! {
371    Account    => Account,
372    Date       => Date,
373    Decimal    => Decimal,
374    Duration   => Duration,
375    bool       => Bool,
376    i8         => Int64,
377    i16        => Int64,
378    i32        => Int64,
379    i64        => Int64,
380    i128       => Int128,
381    IntBig     => IntBig,
382    Principal  => Principal,
383    Subaccount => Subaccount,
384    &str       => Text,
385    String     => Text,
386    Timestamp  => Timestamp,
387    u8         => Nat64,
388    u16        => Nat64,
389    u32        => Nat64,
390    u64        => Nat64,
391    u128       => Nat128,
392    NatBig     => NatBig,
393    Ulid       => Ulid,
394}
395
396impl From<Vec<Self>> for Value {
397    fn from(vec: Vec<Self>) -> Self {
398        Self::List(vec)
399    }
400}
401
402impl TryFrom<Vec<(Self, Self)>> for Value {
403    type Error = SchemaInvariantError;
404
405    fn try_from(entries: Vec<(Self, Self)>) -> Result<Self, Self::Error> {
406        Self::from_map(entries).map_err(Self::Error::from)
407    }
408}
409
410impl From<()> for Value {
411    fn from((): ()) -> Self {
412        Self::Unit
413    }
414}
415
416//
417// ValueEnum
418// Canonical store-local enum identity. Names exist only at input/output boundaries.
419//
420
421#[derive(Clone, Debug, Eq, PartialEq, PartialOrd)]
422pub struct ValueEnum(CanonicalEnumValue<Value>);
423
424impl ValueEnum {
425    #[cfg(test)]
426    pub(crate) const fn test_unit(type_id: u32, variant_id: u32) -> Self {
427        Self::new(
428            EnumTypeId::new(type_id).expect("test enum type ID must be non-zero"),
429            EnumVariantId::new(variant_id).expect("test enum variant ID must be non-zero"),
430            CanonicalEnumBody::Unit,
431        )
432    }
433
434    #[cfg(test)]
435    pub(crate) fn test_payload(type_id: u32, variant_id: u32, payload: Value) -> Self {
436        Self::new(
437            EnumTypeId::new(type_id).expect("test enum type ID must be non-zero"),
438            EnumVariantId::new(variant_id).expect("test enum variant ID must be non-zero"),
439            CanonicalEnumBody::Payload(Box::new(payload)),
440        )
441    }
442
443    #[cfg(test)]
444    pub(crate) fn test_with_payload(self, payload: Value) -> Self {
445        Self::new(
446            self.type_id(),
447            self.variant_id(),
448            CanonicalEnumBody::Payload(Box::new(payload)),
449        )
450    }
451
452    #[must_use]
453    pub(crate) const fn from_canonical(value: CanonicalEnumValue<Value>) -> Self {
454        Self(value)
455    }
456
457    #[must_use]
458    pub(crate) const fn new(
459        type_id: EnumTypeId,
460        variant_id: EnumVariantId,
461        body: CanonicalEnumBody<Value>,
462    ) -> Self {
463        Self(CanonicalEnumValue::new(type_id, variant_id, body))
464    }
465
466    #[must_use]
467    pub(crate) const fn canonical(&self) -> &CanonicalEnumValue<Value> {
468        &self.0
469    }
470
471    #[must_use]
472    pub(crate) const fn type_id(&self) -> EnumTypeId {
473        self.0.type_id()
474    }
475
476    #[must_use]
477    pub(crate) const fn variant_id(&self) -> EnumVariantId {
478        self.0.variant_id()
479    }
480
481    #[must_use]
482    pub(crate) const fn body(&self) -> &CanonicalEnumBody<Value> {
483        self.0.body()
484    }
485
486    #[must_use]
487    pub(crate) fn payload(&self) -> Option<&Value> {
488        match self.body() {
489            CanonicalEnumBody::Unit => None,
490            CanonicalEnumBody::Payload(payload) => Some(payload.as_ref()),
491        }
492    }
493}
494
495impl<'de> Deserialize<'de> for ValueEnum {
496    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
497    where
498        D: Deserializer<'de>,
499    {
500        let (type_id, variant_id, payload): (u32, u32, Option<Box<Value>>) =
501            Deserialize::deserialize(deserializer)?;
502        let type_id = EnumTypeId::new(type_id)
503            .ok_or_else(|| de::Error::custom("enum type ID must be non-zero"))?;
504        let variant_id = EnumVariantId::new(variant_id)
505            .ok_or_else(|| de::Error::custom("enum variant ID must be non-zero"))?;
506        let body = payload.map_or(CanonicalEnumBody::Unit, CanonicalEnumBody::Payload);
507        Ok(Self::new(type_id, variant_id, body))
508    }
509}