Skip to main content

sift_query_core/model/
value.rs

1#[cfg(feature = "decimal")]
2use std::str::FromStr;
3
4use base64::Engine;
5
6use crate::{ParamError, ValueType};
7
8/// A typed, non-null scalar used in predicates.
9#[derive(Debug, Clone, PartialEq)]
10#[non_exhaustive]
11pub enum ScalarValue {
12    Bool(bool),
13    I32(i32),
14    I64(i64),
15    F32(f32),
16    F64(f64),
17    #[cfg(feature = "decimal")]
18    Decimal(rust_decimal::Decimal),
19    Text(String),
20    Bytes(Vec<u8>),
21    #[cfg(feature = "chrono")]
22    Date(chrono::NaiveDate),
23    #[cfg(feature = "chrono")]
24    Time(chrono::NaiveTime),
25    #[cfg(feature = "chrono")]
26    DateTime(chrono::NaiveDateTime),
27    #[cfg(feature = "chrono")]
28    Timestamp(chrono::DateTime<chrono::Utc>),
29    #[cfg(feature = "uuid")]
30    Uuid(uuid::Uuid),
31    #[cfg(feature = "json")]
32    Json(Box<serde_json::Value>),
33    #[cfg(feature = "postgres-types")]
34    ArrayI64(Vec<Option<i64>>),
35    #[cfg(feature = "postgres-types")]
36    ArrayText(Vec<Option<String>>),
37    #[cfg(feature = "postgres-types")]
38    Int8Range(Box<crate::Int8Range>),
39    #[cfg(feature = "postgres-types")]
40    Network(Box<ipnet::IpNet>),
41}
42
43impl ScalarValue {
44    /// Structured inputs charge their elements to the same logical budget.
45    pub fn logical_weight(&self) -> usize {
46        match self {
47            #[cfg(feature = "postgres-types")]
48            Self::ArrayI64(values) => values.len().max(1),
49            #[cfg(feature = "postgres-types")]
50            Self::ArrayText(values) => values.len().max(1),
51            #[cfg(feature = "json")]
52            Self::Json(value) => json_weight(value),
53            _ => 1,
54        }
55    }
56
57    pub(crate) fn validate(&self) -> Result<(), ParamError> {
58        #[cfg(feature = "postgres-types")]
59        if let Self::Int8Range(value) = self {
60            value
61                .validate()
62                .map_err(|reason| ParamError::InvalidValue {
63                    field: "<range>".to_owned(),
64                    reason,
65                })?;
66        }
67        if matches!(self, Self::F32(value) if !value.is_finite())
68            || matches!(self, Self::F64(value) if !value.is_finite())
69        {
70            return Err(ParamError::InvalidValue {
71                field: "<value>".to_owned(),
72                reason: "expected a finite float".to_owned(),
73            });
74        }
75        Ok(())
76    }
77    #[must_use]
78    pub fn to_external_text(&self) -> String {
79        RowValue::from(self.clone()).to_external_text()
80    }
81    #[must_use]
82    pub fn value_type(&self) -> ValueType {
83        match self {
84            Self::Bool(_) => ValueType::Bool,
85            Self::I32(_) => ValueType::I32,
86            Self::I64(_) => ValueType::I64,
87            Self::F32(_) => ValueType::F32,
88            Self::F64(_) => ValueType::F64,
89            #[cfg(feature = "decimal")]
90            Self::Decimal(_) => ValueType::Decimal,
91            Self::Text(_) => ValueType::Text,
92            Self::Bytes(_) => ValueType::Bytes,
93            #[cfg(feature = "chrono")]
94            Self::Date(_) => ValueType::Date,
95            #[cfg(feature = "chrono")]
96            Self::Time(_) => ValueType::Time,
97            #[cfg(feature = "chrono")]
98            Self::DateTime(_) => ValueType::DateTime,
99            #[cfg(feature = "chrono")]
100            Self::Timestamp(_) => ValueType::Timestamp,
101            #[cfg(feature = "uuid")]
102            Self::Uuid(_) => ValueType::Uuid,
103            #[cfg(feature = "json")]
104            Self::Json(_) => ValueType::Json,
105            #[cfg(feature = "postgres-types")]
106            Self::ArrayI64(_) => ValueType::ArrayI64,
107            #[cfg(feature = "postgres-types")]
108            Self::ArrayText(_) => ValueType::ArrayText,
109            #[cfg(feature = "postgres-types")]
110            Self::Int8Range(_) => ValueType::Int8Range,
111            #[cfg(feature = "postgres-types")]
112            Self::Network(_) => ValueType::Network,
113        }
114    }
115
116    pub(crate) fn parse(
117        value_type: ValueType,
118        input: &str,
119        field: &str,
120    ) -> Result<Self, ParamError> {
121        let invalid = |reason: &str| ParamError::InvalidValue {
122            field: field.to_owned(),
123            reason: reason.to_owned(),
124        };
125        match value_type {
126            ValueType::Bool => match input {
127                "true" => Ok(Self::Bool(true)),
128                "false" => Ok(Self::Bool(false)),
129                _ => Err(invalid("expected true or false")),
130            },
131            ValueType::I32 => input
132                .parse()
133                .map(Self::I32)
134                .map_err(|_| invalid("expected a 32-bit decimal integer")),
135            ValueType::I64 => input
136                .parse()
137                .map(Self::I64)
138                .map_err(|_| invalid("expected a 64-bit decimal integer")),
139            ValueType::F32 => {
140                let value: f32 = input
141                    .parse()
142                    .map_err(|_| invalid("expected a finite 32-bit float"))?;
143                value
144                    .is_finite()
145                    .then_some(Self::F32(value))
146                    .ok_or_else(|| invalid("expected a finite 32-bit float"))
147            }
148            ValueType::F64 => {
149                let value: f64 = input
150                    .parse()
151                    .map_err(|_| invalid("expected a finite 64-bit float"))?;
152                value
153                    .is_finite()
154                    .then_some(Self::F64(value))
155                    .ok_or_else(|| invalid("expected a finite 64-bit float"))
156            }
157            #[cfg(feature = "decimal")]
158            ValueType::Decimal => rust_decimal::Decimal::from_str(input)
159                .map(Self::Decimal)
160                .map_err(|_| invalid("expected a decimal")),
161            ValueType::Text => Ok(Self::Text(input.to_owned())),
162            ValueType::Bytes => base64::engine::general_purpose::URL_SAFE_NO_PAD
163                .decode(input)
164                .map(Self::Bytes)
165                .map_err(|_| invalid("expected unpadded base64url")),
166            #[cfg(feature = "chrono")]
167            ValueType::Date => chrono::NaiveDate::parse_from_str(input, "%Y-%m-%d")
168                .map(Self::Date)
169                .map_err(|_| invalid("expected YYYY-MM-DD")),
170            #[cfg(feature = "chrono")]
171            ValueType::Time => chrono::NaiveTime::parse_from_str(input, "%H:%M:%S%.f")
172                .map(Self::Time)
173                .map_err(|_| invalid("expected HH:MM:SS[.fraction]")),
174            #[cfg(feature = "chrono")]
175            ValueType::DateTime => {
176                chrono::NaiveDateTime::parse_from_str(input, "%Y-%m-%dT%H:%M:%S%.f")
177                    .map(Self::DateTime)
178                    .map_err(|_| invalid("expected YYYY-MM-DDTHH:MM:SS[.fraction]"))
179            }
180            #[cfg(feature = "chrono")]
181            ValueType::Timestamp => chrono::DateTime::parse_from_rfc3339(input)
182                .map(|value| Self::Timestamp(value.to_utc()))
183                .map_err(|_| invalid("expected an RFC 3339 timestamp")),
184            #[cfg(feature = "uuid")]
185            ValueType::Uuid => uuid::Uuid::parse_str(input)
186                .map(Self::Uuid)
187                .map_err(|_| invalid("expected a UUID")),
188            #[cfg(feature = "json")]
189            ValueType::Json => serde_json::from_str(input)
190                .map(|value| Self::Json(Box::new(value)))
191                .map_err(|_| invalid("expected structured JSON")),
192            #[cfg(feature = "postgres-types")]
193            ValueType::ArrayI64 => serde_json::from_str(input)
194                .map(Self::ArrayI64)
195                .map_err(|_| invalid("expected JSON array of int64 values or null")),
196            #[cfg(feature = "postgres-types")]
197            ValueType::ArrayText => serde_json::from_str(input)
198                .map(Self::ArrayText)
199                .map_err(|_| invalid("expected JSON array of strings or null")),
200            #[cfg(feature = "postgres-types")]
201            ValueType::Int8Range => input
202                .parse()
203                .map(|value| Self::Int8Range(Box::new(value)))
204                .map_err(|reason: String| invalid(&reason)),
205            #[cfg(feature = "postgres-types")]
206            ValueType::Network => input
207                .parse()
208                .or_else(|_| input.parse::<std::net::IpAddr>().map(ipnet::IpNet::from))
209                .map(|value| Self::Network(Box::new(value)))
210                .map_err(|_| invalid("expected IPv4/IPv6 address or network")),
211        }
212    }
213}
214
215#[cfg(feature = "json")]
216fn json_weight(value: &serde_json::Value) -> usize {
217    match value {
218        serde_json::Value::Array(values) => values
219            .iter()
220            .map(json_weight)
221            .sum::<usize>()
222            .saturating_add(1),
223        serde_json::Value::Object(values) => values
224            .values()
225            .map(json_weight)
226            .sum::<usize>()
227            .saturating_add(1),
228        _ => 1,
229    }
230}
231
232/// A homogeneous array value used by PostgreSQL `ANY`.
233#[derive(Debug, Clone, PartialEq)]
234pub struct TypedList {
235    pub element_type: ValueType,
236    pub values: Vec<ScalarValue>,
237}
238
239impl TypedList {
240    pub fn new(element_type: ValueType, values: Vec<ScalarValue>) -> Result<Self, ParamError> {
241        if values.is_empty() {
242            return Err(ParamError::InvalidValue {
243                field: "<list>".to_owned(),
244                reason: "list must not be empty".to_owned(),
245            });
246        }
247        if values
248            .iter()
249            .any(|value| value.value_type() != element_type)
250        {
251            return Err(ParamError::InvalidValue {
252                field: "<list>".to_owned(),
253                reason: "list elements must have one type".to_owned(),
254            });
255        }
256        Ok(Self {
257            element_type,
258            values,
259        })
260    }
261}
262
263/// Dynamic result value. Unlike [`ScalarValue`], this can represent SQL NULL.
264#[derive(Debug, Clone, PartialEq)]
265#[non_exhaustive]
266pub enum RowValue {
267    #[cfg(feature = "postgres-types")]
268    ArrayI64(Vec<Option<i64>>),
269    #[cfg(feature = "postgres-types")]
270    ArrayText(Vec<Option<String>>),
271    #[cfg(feature = "postgres-types")]
272    Int8Range(crate::Int8Range),
273    #[cfg(feature = "postgres-types")]
274    Network(ipnet::IpNet),
275    /// Presentation-only list; never a PostgreSQL array bind or cursor key.
276    List(Vec<RowValue>),
277    Null,
278    Bool(bool),
279    I32(i32),
280    I64(i64),
281    F32(f32),
282    F64(f64),
283    #[cfg(feature = "decimal")]
284    Decimal(rust_decimal::Decimal),
285    Text(String),
286    Bytes(Vec<u8>),
287    #[cfg(feature = "chrono")]
288    Date(chrono::NaiveDate),
289    #[cfg(feature = "chrono")]
290    Time(chrono::NaiveTime),
291    #[cfg(feature = "chrono")]
292    DateTime(chrono::NaiveDateTime),
293    #[cfg(feature = "chrono")]
294    Timestamp(chrono::DateTime<chrono::Utc>),
295    #[cfg(feature = "uuid")]
296    Uuid(uuid::Uuid),
297    #[cfg(feature = "json")]
298    Json(serde_json::Value),
299}
300
301impl RowValue {
302    #[must_use]
303    pub fn value_type(&self) -> Option<ValueType> {
304        match self {
305            Self::Null | Self::List(_) => None,
306            Self::Bool(_) => Some(ValueType::Bool),
307            Self::I32(_) => Some(ValueType::I32),
308            Self::I64(_) => Some(ValueType::I64),
309            Self::F32(_) => Some(ValueType::F32),
310            Self::F64(_) => Some(ValueType::F64),
311            #[cfg(feature = "decimal")]
312            Self::Decimal(_) => Some(ValueType::Decimal),
313            Self::Text(_) => Some(ValueType::Text),
314            Self::Bytes(_) => Some(ValueType::Bytes),
315            #[cfg(feature = "chrono")]
316            Self::Date(_) => Some(ValueType::Date),
317            #[cfg(feature = "chrono")]
318            Self::Time(_) => Some(ValueType::Time),
319            #[cfg(feature = "chrono")]
320            Self::DateTime(_) => Some(ValueType::DateTime),
321            #[cfg(feature = "chrono")]
322            Self::Timestamp(_) => Some(ValueType::Timestamp),
323            #[cfg(feature = "uuid")]
324            Self::Uuid(_) => Some(ValueType::Uuid),
325            #[cfg(feature = "json")]
326            Self::Json(_) => Some(ValueType::Json),
327            #[cfg(feature = "postgres-types")]
328            Self::ArrayI64(_) => Some(ValueType::ArrayI64),
329            #[cfg(feature = "postgres-types")]
330            Self::ArrayText(_) => Some(ValueType::ArrayText),
331            #[cfg(feature = "postgres-types")]
332            Self::Int8Range(_) => Some(ValueType::Int8Range),
333            #[cfg(feature = "postgres-types")]
334            Self::Network(_) => Some(ValueType::Network),
335        }
336    }
337    /// Canonical external text used by CSV's default formatter. JSON uses
338    /// compact JSON, bytes use unpadded base64url, and timestamps use RFC 3339.
339    #[must_use]
340    pub fn to_external_text(&self) -> String {
341        match self {
342            #[cfg(feature = "postgres-types")]
343            Self::ArrayI64(value) => serde_json::to_string(value).expect("integer array"),
344            #[cfg(feature = "postgres-types")]
345            Self::ArrayText(value) => serde_json::to_string(value).expect("text array"),
346            #[cfg(feature = "postgres-types")]
347            Self::Int8Range(value) => value.to_string(),
348            #[cfg(feature = "postgres-types")]
349            Self::Network(value) => value.to_string(),
350            Self::List(values) => format!(
351                "[{}]",
352                values.iter().map(json_text).collect::<Vec<_>>().join(",")
353            ),
354            Self::Null => String::new(),
355            Self::Bool(value) => value.to_string(),
356            Self::I32(value) => value.to_string(),
357            Self::I64(value) => value.to_string(),
358            Self::F32(value) => value.to_string(),
359            Self::F64(value) => value.to_string(),
360            Self::Text(value) => value.clone(),
361            Self::Bytes(value) => base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value),
362            #[cfg(feature = "decimal")]
363            Self::Decimal(value) => value.to_string(),
364            #[cfg(feature = "chrono")]
365            Self::Date(value) => value.format("%Y-%m-%d").to_string(),
366            #[cfg(feature = "chrono")]
367            Self::Time(value) => value.format("%H:%M:%S%.f").to_string(),
368            #[cfg(feature = "chrono")]
369            Self::DateTime(value) => value.format("%Y-%m-%dT%H:%M:%S%.f").to_string(),
370            #[cfg(feature = "chrono")]
371            Self::Timestamp(value) => value.to_rfc3339(),
372            #[cfg(feature = "uuid")]
373            Self::Uuid(value) => value.to_string(),
374            #[cfg(feature = "json")]
375            Self::Json(value) => value.to_string(),
376        }
377    }
378}
379
380impl From<ScalarValue> for RowValue {
381    fn from(value: ScalarValue) -> Self {
382        match value {
383            #[cfg(feature = "postgres-types")]
384            ScalarValue::ArrayI64(value) => Self::ArrayI64(value),
385            #[cfg(feature = "postgres-types")]
386            ScalarValue::ArrayText(value) => Self::ArrayText(value),
387            #[cfg(feature = "postgres-types")]
388            ScalarValue::Int8Range(value) => Self::Int8Range(*value),
389            #[cfg(feature = "postgres-types")]
390            ScalarValue::Network(value) => Self::Network(*value),
391            ScalarValue::Bool(value) => Self::Bool(value),
392            ScalarValue::I32(value) => Self::I32(value),
393            ScalarValue::I64(value) => Self::I64(value),
394            ScalarValue::F32(value) => Self::F32(value),
395            ScalarValue::F64(value) => Self::F64(value),
396            ScalarValue::Text(value) => Self::Text(value),
397            ScalarValue::Bytes(value) => Self::Bytes(value),
398            #[cfg(feature = "decimal")]
399            ScalarValue::Decimal(value) => Self::Decimal(value),
400            #[cfg(feature = "chrono")]
401            ScalarValue::Date(value) => Self::Date(value),
402            #[cfg(feature = "chrono")]
403            ScalarValue::Time(value) => Self::Time(value),
404            #[cfg(feature = "chrono")]
405            ScalarValue::DateTime(value) => Self::DateTime(value),
406            #[cfg(feature = "chrono")]
407            ScalarValue::Timestamp(value) => Self::Timestamp(value),
408            #[cfg(feature = "uuid")]
409            ScalarValue::Uuid(value) => Self::Uuid(value),
410            #[cfg(feature = "json")]
411            ScalarValue::Json(value) => Self::Json(*value),
412        }
413    }
414}
415
416fn json_text(value: &RowValue) -> String {
417    match value {
418        RowValue::Null => "null".to_owned(),
419        RowValue::Bool(_) | RowValue::I32(_) => value.to_external_text(),
420        RowValue::F32(number) if number.is_finite() => number.to_string(),
421        RowValue::F64(number) if number.is_finite() => number.to_string(),
422        RowValue::F32(_) | RowValue::F64(_) => "null".to_owned(),
423        RowValue::List(_) => value.to_external_text(),
424        #[cfg(feature = "json")]
425        RowValue::Json(json) => json.to_string(),
426        _ => {
427            use std::fmt::Write;
428            let mut output = String::from("\"");
429            for character in value.to_external_text().chars() {
430                match character {
431                    '"' => output.push_str("\\\""),
432                    '\\' => output.push_str("\\\\"),
433                    character if character <= '\u{1f}' => {
434                        write!(&mut output, "\\u{:04x}", u32::from(character))
435                            .expect("writing to String cannot fail");
436                    }
437                    character => output.push(character),
438                }
439            }
440            output.push('"');
441            output
442        }
443    }
444}
445
446#[cfg(feature = "serde")]
447impl serde::Serialize for RowValue {
448    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
449    where
450        S: serde::Serializer,
451    {
452        use serde::ser::Error as _;
453        match self {
454            Self::List(values) => serde::Serialize::serialize(values, serializer),
455            #[cfg(feature = "postgres-types")]
456            Self::ArrayI64(values) => serde::Serialize::serialize(
457                &values
458                    .iter()
459                    .map(|value| value.map(|value| value.to_string()))
460                    .collect::<Vec<_>>(),
461                serializer,
462            ),
463            #[cfg(feature = "postgres-types")]
464            Self::ArrayText(values) => serde::Serialize::serialize(values, serializer),
465            #[cfg(feature = "postgres-types")]
466            Self::Int8Range(value) => serializer.serialize_str(&value.to_string()),
467            #[cfg(feature = "postgres-types")]
468            Self::Network(value) => serializer.serialize_str(&value.to_string()),
469            Self::Null => serializer.serialize_none(),
470            Self::Bool(value) => serializer.serialize_bool(*value),
471            Self::I32(value) => serializer.serialize_i32(*value),
472            // Preserve exact values for JavaScript and other IEEE-754 consumers.
473            Self::I64(value) => serializer.serialize_str(&value.to_string()),
474            Self::F32(value) if value.is_finite() => serializer.serialize_f32(*value),
475            Self::F64(value) if value.is_finite() => serializer.serialize_f64(*value),
476            Self::F32(_) | Self::F64(_) => {
477                Err(S::Error::custom("non-finite floats cannot be serialized"))
478            }
479            #[cfg(feature = "decimal")]
480            Self::Decimal(value) => serializer.serialize_str(&value.to_string()),
481            Self::Text(value) => serializer.serialize_str(value),
482            Self::Bytes(value) => serializer
483                .serialize_str(&base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value)),
484            #[cfg(feature = "chrono")]
485            Self::Date(value) => serializer.serialize_str(&value.format("%Y-%m-%d").to_string()),
486            #[cfg(feature = "chrono")]
487            Self::Time(value) => serializer.serialize_str(&value.format("%H:%M:%S%.f").to_string()),
488            #[cfg(feature = "chrono")]
489            Self::DateTime(value) => {
490                serializer.serialize_str(&value.format("%Y-%m-%dT%H:%M:%S%.f").to_string())
491            }
492            #[cfg(feature = "chrono")]
493            Self::Timestamp(value) => serializer.serialize_str(&value.to_rfc3339()),
494            #[cfg(feature = "uuid")]
495            Self::Uuid(value) => serializer.serialize_str(&value.to_string()),
496            #[cfg(feature = "json")]
497            Self::Json(value) => value.serialize(serializer),
498        }
499    }
500}
501
502pub trait IntoScalarValue {
503    fn into_scalar_value(self) -> ScalarValue;
504}
505
506macro_rules! impl_scalar {
507    ($type:ty, $variant:ident) => {
508        impl IntoScalarValue for $type {
509            fn into_scalar_value(self) -> ScalarValue {
510                ScalarValue::$variant(self)
511            }
512        }
513    };
514}
515
516impl_scalar!(bool, Bool);
517impl_scalar!(i32, I32);
518impl_scalar!(i64, I64);
519impl_scalar!(f32, F32);
520impl_scalar!(f64, F64);
521impl_scalar!(String, Text);
522impl_scalar!(Vec<u8>, Bytes);
523
524impl IntoScalarValue for &str {
525    fn into_scalar_value(self) -> ScalarValue {
526        ScalarValue::Text(self.to_owned())
527    }
528}
529
530#[cfg(feature = "decimal")]
531impl_scalar!(rust_decimal::Decimal, Decimal);
532#[cfg(feature = "chrono")]
533impl_scalar!(chrono::NaiveDate, Date);
534#[cfg(feature = "chrono")]
535impl_scalar!(chrono::NaiveTime, Time);
536#[cfg(feature = "chrono")]
537impl_scalar!(chrono::NaiveDateTime, DateTime);
538#[cfg(feature = "chrono")]
539impl_scalar!(chrono::DateTime<chrono::Utc>, Timestamp);
540#[cfg(feature = "uuid")]
541impl_scalar!(uuid::Uuid, Uuid);
542#[cfg(feature = "json")]
543impl IntoScalarValue for serde_json::Value {
544    fn into_scalar_value(self) -> ScalarValue {
545        ScalarValue::Json(Box::new(self))
546    }
547}
548#[cfg(feature = "postgres-types")]
549impl_scalar!(Vec<Option<i64>>, ArrayI64);
550#[cfg(feature = "postgres-types")]
551impl_scalar!(Vec<Option<String>>, ArrayText);
552#[cfg(feature = "postgres-types")]
553impl IntoScalarValue for crate::Int8Range {
554    fn into_scalar_value(self) -> ScalarValue {
555        ScalarValue::Int8Range(Box::new(self))
556    }
557}
558#[cfg(feature = "postgres-types")]
559impl IntoScalarValue for ipnet::IpNet {
560    fn into_scalar_value(self) -> ScalarValue {
561        ScalarValue::Network(Box::new(self))
562    }
563}
564#[cfg(feature = "postgres-types")]
565impl IntoScalarValue for Vec<i64> {
566    fn into_scalar_value(self) -> ScalarValue {
567        ScalarValue::ArrayI64(self.into_iter().map(Some).collect())
568    }
569}
570#[cfg(feature = "postgres-types")]
571impl IntoScalarValue for Vec<String> {
572    fn into_scalar_value(self) -> ScalarValue {
573        ScalarValue::ArrayText(self.into_iter().map(Some).collect())
574    }
575}