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}