sqlx_postgres/types/
time_tz.rs

1use crate::decode::Decode;
2use crate::encode::{Encode, IsNull};
3use crate::error::BoxDynError;
4use crate::types::Type;
5use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};
6use byteorder::{BigEndian, ReadBytesExt};
7use std::io::Cursor;
8use std::mem;
9
10#[cfg(feature = "time")]
11type DefaultTime = ::time::Time;
12
13#[cfg(all(not(feature = "time"), feature = "chrono"))]
14type DefaultTime = ::chrono::NaiveTime;
15
16#[cfg(feature = "time")]
17type DefaultOffset = ::time::UtcOffset;
18
19#[cfg(all(not(feature = "time"), feature = "chrono"))]
20type DefaultOffset = ::chrono::FixedOffset;
21
22/// Represents a moment of time, in a specified timezone.
23///
24/// # Warning
25///
26/// `PgTimeTz` provides `TIMETZ` and is supported only for reading from legacy databases.
27/// [PostgreSQL recommends] to use `TIMESTAMPTZ` instead.
28///
29/// [PostgreSQL recommends]: https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_timetz
30#[derive(Debug, PartialEq, Clone, Copy)]
31pub struct PgTimeTz<Time = DefaultTime, Offset = DefaultOffset> {
32    pub time: Time,
33    pub offset: Offset,
34}
35
36impl<Time, Offset> PgHasArrayType for PgTimeTz<Time, Offset> {
37    fn array_type_info() -> PgTypeInfo {
38        PgTypeInfo::TIMETZ_ARRAY
39    }
40}
41
42#[cfg(feature = "chrono")]
43mod chrono {
44    use super::*;
45    use ::chrono::{DateTime, Duration, FixedOffset, NaiveTime};
46
47    impl Type<Postgres> for PgTimeTz<NaiveTime, FixedOffset> {
48        fn type_info() -> PgTypeInfo {
49            PgTypeInfo::TIMETZ
50        }
51    }
52
53    impl Encode<'_, Postgres> for PgTimeTz<NaiveTime, FixedOffset> {
54        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
55            let _: IsNull = <NaiveTime as Encode<'_, Postgres>>::encode(self.time, buf)?;
56            let _: IsNull =
57                <i32 as Encode<'_, Postgres>>::encode(self.offset.utc_minus_local(), buf)?;
58
59            Ok(IsNull::No)
60        }
61
62        fn size_hint(&self) -> usize {
63            mem::size_of::<i64>() + mem::size_of::<i32>()
64        }
65    }
66
67    impl<'r> Decode<'r, Postgres> for PgTimeTz<NaiveTime, FixedOffset> {
68        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
69            match value.format() {
70                PgValueFormat::Binary => {
71                    let mut buf = Cursor::new(value.as_bytes()?);
72
73                    // TIME is encoded as the microseconds since midnight
74                    let us = buf.read_i64::<BigEndian>()?;
75                    // default is midnight, there is a canary test for this
76                    // in `sqlx-postgres/src/types/chrono/time.rs`
77                    let time = NaiveTime::default() + Duration::microseconds(us);
78
79                    // OFFSET is encoded as seconds from UTC
80                    let offset_seconds = buf.read_i32::<BigEndian>()?;
81
82                    let offset = FixedOffset::west_opt(offset_seconds).ok_or_else(|| {
83                        format!(
84                            "server returned out-of-range offset for `TIMETZ`: {offset_seconds} seconds"
85                        )
86                    })?;
87
88                    Ok(PgTimeTz { time, offset })
89                }
90
91                PgValueFormat::Text => try_parse_timetz(value.as_str()?),
92            }
93        }
94    }
95
96    fn try_parse_timetz(s: &str) -> Result<PgTimeTz<NaiveTime, FixedOffset>, BoxDynError> {
97        let mut tmp = String::with_capacity(11 + s.len());
98        tmp.push_str("2001-07-08 ");
99        tmp.push_str(s);
100
101        let mut err = None;
102
103        for fmt in &["%Y-%m-%d %H:%M:%S%.f%#z", "%Y-%m-%d %H:%M:%S%.f"] {
104            match DateTime::parse_from_str(&tmp, fmt) {
105                Ok(dt) => {
106                    let time = dt.time();
107                    let offset = *dt.offset();
108
109                    return Ok(PgTimeTz { time, offset });
110                }
111
112                Err(error) => {
113                    err = Some(error);
114                }
115            }
116        }
117
118        Err(err
119            .expect("BUG: loop should have set `err` to `Some()` before exiting")
120            .into())
121    }
122}
123
124#[cfg(feature = "time")]
125mod time {
126    use super::*;
127    use ::time::{Duration, Time, UtcOffset};
128
129    impl Type<Postgres> for PgTimeTz<Time, UtcOffset> {
130        fn type_info() -> PgTypeInfo {
131            PgTypeInfo::TIMETZ
132        }
133    }
134
135    impl Encode<'_, Postgres> for PgTimeTz<Time, UtcOffset> {
136        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
137            let _: IsNull = <Time as Encode<'_, Postgres>>::encode(self.time, buf)?;
138            let _: IsNull =
139                <i32 as Encode<'_, Postgres>>::encode(-self.offset.whole_seconds(), buf)?;
140
141            Ok(IsNull::No)
142        }
143
144        fn size_hint(&self) -> usize {
145            mem::size_of::<i64>() + mem::size_of::<i32>()
146        }
147    }
148
149    impl<'r> Decode<'r, Postgres> for PgTimeTz<Time, UtcOffset> {
150        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
151            match value.format() {
152                PgValueFormat::Binary => {
153                    let mut buf = Cursor::new(value.as_bytes()?);
154
155                    // TIME is encoded as the microseconds since midnight
156                    let us = buf.read_i64::<BigEndian>()?;
157                    let time = Time::MIDNIGHT + Duration::microseconds(us);
158
159                    // OFFSET is encoded as seconds from UTC
160                    let seconds = buf.read_i32::<BigEndian>()?;
161
162                    Ok(PgTimeTz {
163                        time,
164                        offset: -UtcOffset::from_whole_seconds(seconds)?,
165                    })
166                }
167
168                PgValueFormat::Text => {
169                    // the `time` crate has a limited ability to parse and can't parse the
170                    // timezone format
171                    Err("reading a `TIMETZ` value in text format is not supported.".into())
172                }
173            }
174        }
175    }
176}