jiff_sqlx/
wrappers.rs

1/// A trait for convenient conversions from Jiff types to SQLx types.
2///
3/// # Example
4///
5/// This shows how to convert a [`jiff::Timestamp`] to a [`Timestamp`]:
6///
7/// ```
8/// use jiff_sqlx::ToSqlx;
9///
10/// let ts: jiff::Timestamp = "2025-02-20T17:00-05".parse()?;
11/// let wrapper = ts.to_sqlx();
12/// assert_eq!(format!("{wrapper:?}"), "Timestamp(2025-02-20T22:00:00Z)");
13///
14/// # Ok::<(), Box<dyn std::error::Error>>(())
15/// ```
16pub trait ToSqlx {
17    /// The wrapper type to convert to.
18    type Target;
19
20    /// A conversion method that converts a Jiff type to a SQLx wrapper type.
21    fn to_sqlx(self) -> Self::Target;
22}
23
24// We currently don't support `Zoned` integration in this wrapper crate. To
25// briefly explain why, a `Zoned` is _both_ a timestamp and a time zone. And
26// it isn't necessarily a dumb time zone like `-05:00`. It is intended to be a
27// real time zone like `America/New_York` or `Australia/Tasmania`.
28//
29// However, PostgreSQL doesn't really have a primitive type that specifically
30// supports "timestamp with time zone." Comically, it does have a `TIMESTAMP
31// WITH TIME ZONE` type (from the SQL standard, as I understand it), but it's
32// actually just a timestamp. It doesn't store any other data than a timestamp.
33// The difference between `TIMESTAMP WITHOUT TIME ZONE` and `TIMESTAMP WITH
34// TIME ZONE` is, principally, that fixed offsets are respected in the former
35// but completely ignored in the latter. (PostgreSQL's documentation refers
36// to fixed offsets as "time zones," which is rather antiquated IMO.) And,
37// conventionally, `TIMESTAMP WITHOUT TIME ZONE` is civil (local) time.
38//
39// So what's the problem? Well, if we try to stuff a `Zoned` into a
40// `TIMESTAMP WITH TIME ZONE`, then the *only* thing that actually
41// gets stored is a timestamp. No time zone. No offset. Nothing. That
42// means the time zone attached to `Zoned` gets dropped. So if you put
43// `2025-02-15T17:00-05[America/New_york]` into the database, then you'll
44// always get `2025-02-15T22:00+00[UTC]` out. That's a silent dropping of the
45// time zone data. I personally think this would be extremely surprising,
46// and it could lead to bugs if you assume the time zone is correctly
47// round-tripped. (For example, DST safe arithmetic would no longer apply.)
48// And indeed, this is a principle design goal of Jiff: `Zoned` values can be
49// losslessly transmitted.
50//
51// An alternative here is to provide a `Zoned` wrapper, but store it in
52// a `TEXT` field as an RFC 9557 timestamp. This would permit lossless
53// round-tripping. The problem is that this may also violate user expectations
54// because a datetime is not stored in a datetime field. Thus, you won't be
55// able to use PostgreSQL native functionality to handle it as a date.
56//
57// So for now, I opted to take the conservative choice and just not provide a
58// `Zoned` impl. This way, we can gather use cases and make a better informed
59// decision of what to do. Plus, the maintainer of `sqlx` seemed unconvinced
60// that a `Zoned` impl that drops time zone data was a problem, so out of
61// deference there, I started with this more conservative strategy.
62//
63// Of course, if you want to store something in `TIMESTAMP WITHOUT TIME ZONE`,
64// then you can just use `Timestamp`.
65//
66// Ref: https://github.com/launchbadge/sqlx/issues/3487#issuecomment-2636542379
67/*
68#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
69pub struct Zoned(jiff::Zoned);
70
71impl Zoned {
72    pub fn to_jiff(&self) -> jiff::Zoned {
73        self.0.clone()
74    }
75}
76
77impl<'a> ToSqlx for &'a jiff::Zoned {
78    type Target = Zoned;
79
80    fn to_sqlx(self) -> Zoned {
81        Zoned(self.clone())
82    }
83}
84
85impl From<jiff::Zoned> for Zoned {
86    fn from(x: jiff::Zoned) -> Zoned {
87        Zoned(x)
88    }
89}
90
91impl From<Zoned> for jiff::Zoned {
92    fn from(x: Zoned) -> jiff::Zoned {
93        x.0
94    }
95}
96
97impl core::ops::Deref for Zoned {
98    type Target = jiff::Zoned;
99
100    fn deref(&self) -> &jiff::Zoned {
101        &self.0
102    }
103}
104*/
105
106/// A wrapper type for [`jiff::Timestamp`].
107#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
108pub struct Timestamp(jiff::Timestamp);
109
110impl Timestamp {
111    /// Converts this wrapper to a [`jiff::Timestamp`].
112    pub fn to_jiff(self) -> jiff::Timestamp {
113        self.0
114    }
115}
116
117impl ToSqlx for jiff::Timestamp {
118    type Target = Timestamp;
119
120    fn to_sqlx(self) -> Timestamp {
121        Timestamp(self)
122    }
123}
124
125impl From<jiff::Timestamp> for Timestamp {
126    fn from(x: jiff::Timestamp) -> Timestamp {
127        Timestamp(x)
128    }
129}
130
131impl From<Timestamp> for jiff::Timestamp {
132    fn from(x: Timestamp) -> jiff::Timestamp {
133        x.0
134    }
135}
136
137/// A wrapper type for [`jiff::civil::DateTime`].
138#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
139pub struct DateTime(jiff::civil::DateTime);
140
141impl DateTime {
142    /// Converts this wrapper to a [`jiff::civil::DateTime`].
143    pub fn to_jiff(self) -> jiff::civil::DateTime {
144        self.0
145    }
146}
147
148impl ToSqlx for jiff::civil::DateTime {
149    type Target = DateTime;
150
151    fn to_sqlx(self) -> DateTime {
152        DateTime(self)
153    }
154}
155
156impl From<jiff::civil::DateTime> for DateTime {
157    fn from(x: jiff::civil::DateTime) -> DateTime {
158        DateTime(x)
159    }
160}
161
162impl From<DateTime> for jiff::civil::DateTime {
163    fn from(x: DateTime) -> jiff::civil::DateTime {
164        x.0
165    }
166}
167
168/// A wrapper type for [`jiff::civil::Date`].
169#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
170pub struct Date(jiff::civil::Date);
171
172impl Date {
173    /// Converts this wrapper to a [`jiff::civil::Date`].
174    pub fn to_jiff(self) -> jiff::civil::Date {
175        self.0
176    }
177}
178
179impl ToSqlx for jiff::civil::Date {
180    type Target = Date;
181
182    fn to_sqlx(self) -> Date {
183        Date(self)
184    }
185}
186
187impl From<jiff::civil::Date> for Date {
188    fn from(x: jiff::civil::Date) -> Date {
189        Date(x)
190    }
191}
192
193impl From<Date> for jiff::civil::Date {
194    fn from(x: Date) -> jiff::civil::Date {
195        x.0
196    }
197}
198
199/// A wrapper type for [`jiff::civil::Time`].
200#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
201pub struct Time(jiff::civil::Time);
202
203impl Time {
204    /// Converts this wrapper to a [`jiff::civil::Time`].
205    pub fn to_jiff(self) -> jiff::civil::Time {
206        self.0
207    }
208}
209
210impl ToSqlx for jiff::civil::Time {
211    type Target = Time;
212
213    fn to_sqlx(self) -> Time {
214        Time(self)
215    }
216}
217
218impl From<jiff::civil::Time> for Time {
219    fn from(x: jiff::civil::Time) -> Time {
220        Time(x)
221    }
222}
223
224impl From<Time> for jiff::civil::Time {
225    fn from(x: Time) -> jiff::civil::Time {
226        x.0
227    }
228}
229
230/// A wrapper type for [`jiff::Span`].
231///
232/// # PostgreSQL: Limited support
233///
234/// This type _only_ has a [`sqlx_core::decode::Decode`] trait implementation
235/// for PostgreSQL. The reason for this is that encoding an arbitrary
236/// `Span` into a PostgreSQL interval requires a relative datetime.
237/// Therefore, users wanting to store a `Span` will need to explicitly use a
238/// [`sqlx_postgres::types::PgInterval`] at least at encoding time.
239#[derive(Clone, Copy, Debug)]
240pub struct Span(jiff::Span);
241
242impl Span {
243    /// Converts this wrapper to a [`jiff::Span`].
244    pub fn to_jiff(self) -> jiff::Span {
245        self.0
246    }
247}
248
249impl ToSqlx for jiff::Span {
250    type Target = Span;
251
252    fn to_sqlx(self) -> Span {
253        Span(self)
254    }
255}
256
257impl From<jiff::Span> for Span {
258    fn from(x: jiff::Span) -> Span {
259        Span(x)
260    }
261}
262
263impl From<Span> for jiff::Span {
264    fn from(x: Span) -> jiff::Span {
265        x.0
266    }
267}