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}