Skip to main content

toasty_core/stmt/
document_storage_text.rs

1use std::fmt;
2
3use crate::stmt::Value;
4
5impl Value {
6    /// The text form this value takes when rendered for document storage, or
7    /// `None` if the value has no document text form.
8    ///
9    /// Values that are stored as JSON strings inside a `#[document]` column
10    /// take this form: jiff temporal values (truncated to microseconds — the
11    /// precision the SQL temporal types hold — and formatted with *fixed*
12    /// six-digit subsecond precision), decimals, and network addresses (their
13    /// `Display` forms).
14    /// Fixed temporal precision matters on backends that compare document
15    /// leaves as plain text (SQLite has no native temporal types, so
16    /// `json_extract` comparisons are text comparisons): uniform-precision
17    /// ISO 8601 strings sort lexicographically in chronological order, while
18    /// trimmed subseconds do not (`...T00:00:00Z` sorts *after*
19    /// `...T00:00:00.000001Z`).
20    ///
21    /// Both the JSON document codec (`toasty-sql`) and the engine's document
22    /// lowering (which rewrites comparison operands to text on those
23    /// backends) render document text through this one method, so the stored
24    /// form and a bound comparison operand cannot drift apart.
25    ///
26    /// `Zoned` has no document text form: its RFC 9557 `[IANA]` annotation is
27    /// rejected at schema build.
28    pub fn document_storage_text(&self) -> Option<DocumentStorageText<'_>> {
29        match self {
30            #[cfg(feature = "jiff")]
31            Value::Timestamp(_) | Value::Date(_) | Value::Time(_) | Value::DateTime(_) => {
32                Some(DocumentStorageText(self))
33            }
34            #[cfg(feature = "rust_decimal")]
35            Value::Decimal(_) => Some(DocumentStorageText(self)),
36            #[cfg(feature = "bigdecimal")]
37            Value::BigDecimal(_) => Some(DocumentStorageText(self)),
38            #[cfg(feature = "net")]
39            Value::Cidr(_) | Value::Inet(_) | Value::MacAddr(_) | Value::MacAddr8(_) => {
40                Some(DocumentStorageText(self))
41            }
42            _ => None,
43        }
44    }
45}
46
47/// Helper struct for rendering a [`Value`]'s document storage text form.
48///
49/// Returned by [`Value::document_storage_text`]; see its documentation for
50/// the format contract. Like [`std::path::Display`], this is an opaque
51/// adapter — the only way to obtain one is the method that guarantees the
52/// value has a document text form.
53#[derive(Debug)]
54pub struct DocumentStorageText<'a>(&'a Value);
55
56impl fmt::Display for DocumentStorageText<'_> {
57    // With none of the temporal or decimal features enabled, every arm below
58    // is compiled out except the unreachable one, leaving `f` unused.
59    #[cfg_attr(
60        not(any(
61            feature = "jiff",
62            feature = "rust_decimal",
63            feature = "bigdecimal",
64            feature = "net"
65        )),
66        allow(unused_variables)
67    )]
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self.0 {
70            #[cfg(feature = "jiff")]
71            Value::Timestamp(v) => write!(f, "{:.6}", trunc_timestamp_us(*v)),
72            #[cfg(feature = "jiff")]
73            Value::Date(v) => write!(f, "{v}"),
74            #[cfg(feature = "jiff")]
75            Value::Time(v) => write!(f, "{:.6}", trunc_time_us(*v)),
76            #[cfg(feature = "jiff")]
77            Value::DateTime(v) => write!(f, "{:.6}", trunc_datetime_us(*v)),
78            #[cfg(feature = "rust_decimal")]
79            Value::Decimal(v) => write!(f, "{v}"),
80            #[cfg(feature = "bigdecimal")]
81            Value::BigDecimal(v) => write!(f, "{v}"),
82            #[cfg(feature = "net")]
83            Value::Cidr(v) => write!(f, "{v}"),
84            #[cfg(feature = "net")]
85            Value::Inet(v) => write!(f, "{v}"),
86            #[cfg(feature = "net")]
87            Value::MacAddr(v) => write!(f, "{v}"),
88            #[cfg(feature = "net")]
89            Value::MacAddr8(v) => write!(f, "{v}"),
90            // `document_storage_text` only constructs the adapter for the
91            // variants above.
92            _ => unreachable!(),
93        }
94    }
95}
96
97/// Truncate a timestamp to microsecond precision, toward zero, dropping any
98/// sub-microsecond nanoseconds. Rounding can only fail at the extreme ends of
99/// the representable range; fall back to the original value there rather than
100/// failing the whole encode.
101#[cfg(feature = "jiff")]
102fn trunc_timestamp_us(v: jiff::Timestamp) -> jiff::Timestamp {
103    v.round(
104        jiff::TimestampRound::new()
105            .smallest(jiff::Unit::Microsecond)
106            .mode(jiff::RoundMode::Trunc),
107    )
108    .unwrap_or(v)
109}
110
111/// Truncate a civil time to microsecond precision, toward zero. See
112/// [`trunc_timestamp_us`].
113#[cfg(feature = "jiff")]
114fn trunc_time_us(v: jiff::civil::Time) -> jiff::civil::Time {
115    v.round(
116        jiff::civil::TimeRound::new()
117            .smallest(jiff::Unit::Microsecond)
118            .mode(jiff::RoundMode::Trunc),
119    )
120    .unwrap_or(v)
121}
122
123/// Truncate a civil datetime to microsecond precision, toward zero. See
124/// [`trunc_timestamp_us`].
125#[cfg(feature = "jiff")]
126fn trunc_datetime_us(v: jiff::civil::DateTime) -> jiff::civil::DateTime {
127    v.round(
128        jiff::civil::DateTimeRound::new()
129            .smallest(jiff::Unit::Microsecond)
130            .mode(jiff::RoundMode::Trunc),
131    )
132    .unwrap_or(v)
133}