sqlx_postgres_interval/
lib.rs

1use std::mem;
2
3use serde::{Deserialize, Serialize};
4use sqlx::{
5    Decode, Encode, Postgres, Type,
6    encode::IsNull,
7    error::BoxDynError,
8    postgres::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef, types::PgInterval},
9};
10
11/// A type that mimics [`sqlx::postgres::types::PgInterval`] but provides
12/// both [`serde::Serialize`] and [`serde::Deserialize`]
13/// into and from ISO 8601 string format.
14///
15/// ISO 8601 Duration Format:
16/// `P(n)Y(n)M(n)DT(n)H(n)M(n)S`
17/// Where:
18/// P - "period"/duration designator (always present at beginning)
19/// (n) - integer
20/// Y - follows number of years
21/// M - follows number of months
22/// W - follows number of weeks
23/// D - follows number of days
24/// T - designator that precedes time components
25/// H - follows number of hours
26/// M - follows number of minutes
27/// S - follows number of seconds
28///
29/// See also:
30///   - https://en.wikipedia.org/wiki/ISO_8601#Durations
31///   - https://www.digi.com/resources/documentation/digidocs/90001488-13/reference/r_iso_8601_duration_format.htm
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Interval {
34    pub months: i32,
35    pub days: i32,
36    pub microseconds: i64,
37}
38
39impl Serialize for Interval {
40    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
41    where
42        S: serde::Serializer,
43    {
44        let Self {
45            months,
46            days,
47            microseconds,
48        } = self.clone();
49        let pgi = pg_interval::Interval {
50            months,
51            days,
52            microseconds,
53        };
54        serializer.serialize_str(&pgi.to_iso_8601())
55    }
56}
57
58impl<'de> Deserialize<'de> for Interval {
59    fn deserialize<D>(deserializer: D) -> Result<Interval, D::Error>
60    where
61        D: serde::Deserializer<'de>,
62    {
63        let s = String::deserialize(deserializer)?;
64        let pgi = pg_interval::Interval::from_iso(&s).map_err(|error| {
65            serde::de::Error::custom(match error {
66                pg_interval::ParseError::ParseIntErr(parse_int_error) => {
67                    parse_int_error.to_string()
68                }
69                pg_interval::ParseError::ParseFloatErr(parse_float_error) => {
70                    parse_float_error.to_string()
71                }
72                pg_interval::ParseError::InvalidYearMonth(invalid_year_month) => invalid_year_month,
73                pg_interval::ParseError::InvalidTime(invalid_time) => invalid_time,
74                pg_interval::ParseError::InvalidInterval(invalid_interval) => invalid_interval,
75            })
76        })?;
77        Ok(Interval {
78            months: pgi.months,
79            days: pgi.days,
80            microseconds: pgi.microseconds,
81        })
82    }
83}
84
85impl Type<Postgres> for Interval {
86    fn type_info() -> PgTypeInfo {
87        PgInterval::type_info()
88    }
89}
90
91impl PgHasArrayType for Interval {
92    fn array_type_info() -> PgTypeInfo {
93        PgInterval::array_type_info()
94    }
95}
96
97impl<'de> Decode<'de, Postgres> for Interval {
98    fn decode(value: PgValueRef<'de>) -> Result<Self, BoxDynError> {
99        let PgInterval {
100            months,
101            days,
102            microseconds,
103        } = PgInterval::decode(value)?;
104        Ok(Interval {
105            months,
106            days,
107            microseconds,
108        })
109    }
110}
111
112impl Encode<'_, Postgres> for Interval {
113    fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
114        let Self {
115            months,
116            days,
117            microseconds,
118        } = self.clone();
119        let pg_interval = PgInterval {
120            months,
121            days,
122            microseconds,
123        };
124        pg_interval.encode_by_ref(buf)
125    }
126
127    fn size_hint(&self) -> usize {
128        2 * mem::size_of::<i64>()
129    }
130}
131
132impl TryFrom<std::time::Duration> for Interval {
133    type Error = BoxDynError;
134
135    /// Convert a `std::time::Duration` to a `PgInterval`
136    ///
137    /// This returns an error if there is a loss of precision using nanoseconds or if there is a
138    /// microsecond overflow.
139    fn try_from(value: std::time::Duration) -> Result<Self, BoxDynError> {
140        if value.as_nanos() % 1000 != 0 {
141            return Err("PostgreSQL `INTERVAL` does not support nanoseconds precision".into());
142        }
143
144        Ok(Self {
145            months: 0,
146            days: 0,
147            microseconds: value.as_micros().try_into()?,
148        })
149    }
150}
151
152#[cfg(feature = "chrono")]
153impl TryFrom<chrono::Duration> for Interval {
154    type Error = BoxDynError;
155
156    /// Convert a `chrono::Duration` to an `Interval`.
157    ///
158    /// This returns an error if there is a loss of precision using nanoseconds or if there is a
159    /// nanosecond overflow.
160    fn try_from(value: chrono::Duration) -> Result<Self, BoxDynError> {
161        value
162            .num_nanoseconds()
163            .map_or::<Result<_, Self::Error>, _>(
164                Err("Overflow has occurred for PostgreSQL `INTERVAL`".into()),
165                |nanoseconds| {
166                    if nanoseconds % 1000 != 0 {
167                        return Err(
168                            "PostgreSQL `INTERVAL` does not support nanoseconds precision".into(),
169                        );
170                    }
171                    Ok(())
172                },
173            )?;
174
175        value.num_microseconds().map_or(
176            Err("Overflow has occurred for PostgreSQL `INTERVAL`".into()),
177            |microseconds| {
178                Ok(Self {
179                    months: 0,
180                    days: 0,
181                    microseconds,
182                })
183            },
184        )
185    }
186}
187
188#[cfg(feature = "time")]
189impl TryFrom<time::Duration> for Interval {
190    type Error = BoxDynError;
191
192    /// Convert a `time::Duration` to a `PgInterval`.
193    ///
194    /// This returns an error if there is a loss of precision using nanoseconds or if there is a
195    /// microsecond overflow.
196    fn try_from(value: time::Duration) -> Result<Self, BoxDynError> {
197        if value.whole_nanoseconds() % 1000 != 0 {
198            return Err("PostgreSQL `INTERVAL` does not support nanoseconds precision".into());
199        }
200
201        Ok(Self {
202            months: 0,
203            days: 0,
204            microseconds: value.whole_microseconds().try_into()?,
205        })
206    }
207}