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