Skip to main content

spg_sqlx/types/
chrono.rs

1//! v7.16.0 — `Type` / `Encode` / `Decode` for the chrono date
2//! / time types. SPG stores TIMESTAMP and TIMESTAMPTZ as `i64`
3//! microseconds since the Unix epoch (always UTC for storage);
4//! DATE as `i32` days. The Encode side renders to PG-canonical
5//! text via the engine's `Value::Bytes`/text-coerce path so
6//! the conversion matches v7.15.0's TIMESTAMPTZ literal parser.
7
8#![cfg(feature = "chrono")]
9
10use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, Timelike, Utc};
11use sqlx_core::decode::Decode;
12use sqlx_core::encode::{Encode, IsNull};
13use sqlx_core::error::BoxDynError;
14use sqlx_core::types::Type;
15
16use spg_embedded::Value as EngineValue;
17
18use crate::arguments::SpgArgumentValue;
19use crate::database::Spg;
20use crate::type_info::{Kind, SpgTypeInfo};
21use crate::value::SpgValueRef;
22
23// ---- DateTime<Utc> (TIMESTAMPTZ) ----
24
25impl Type<Spg> for DateTime<Utc> {
26    fn type_info() -> SpgTypeInfo {
27        SpgTypeInfo::of(Kind::Timestamptz)
28    }
29    fn compatible(ty: &SpgTypeInfo) -> bool {
30        matches!(ty.kind(), Kind::Timestamptz | Kind::Timestamp)
31    }
32}
33
34impl<'q> Encode<'q, Spg> for DateTime<Utc> {
35    fn encode_by_ref(
36        &self,
37        buf: &mut Vec<SpgArgumentValue<'q>>,
38    ) -> Result<IsNull, BoxDynError> {
39        // Convert to PG-canonical `YYYY-MM-DD HH:MM:SS.fff+00`
40        // text. The engine's v7.15.0 TIMESTAMPTZ parser accepts
41        // this shape and stores as i64 µs UTC.
42        let s = self
43            .format("%Y-%m-%d %H:%M:%S%.6f+00")
44            .to_string();
45        buf.push(SpgArgumentValue {
46            value: EngineValue::Text(s),
47            type_info: Some(<DateTime<Utc> as Type<Spg>>::type_info()),
48            _phantom: core::marker::PhantomData,
49        });
50        Ok(IsNull::No)
51    }
52}
53
54impl<'r> Decode<'r, Spg> for DateTime<Utc> {
55    fn decode(value: SpgValueRef<'r>) -> Result<Self, BoxDynError> {
56        match value.engine() {
57            EngineValue::Timestamp(micros) => {
58                let secs = micros.div_euclid(1_000_000);
59                let nanos = (micros.rem_euclid(1_000_000) as u32) * 1_000;
60                let dt = DateTime::<Utc>::from_timestamp(secs, nanos)
61                    .ok_or_else(|| {
62                        format!("TIMESTAMPTZ value {micros} µs out of chrono range")
63                    })?;
64                Ok(dt)
65            }
66            other => Err(format!("cannot decode {other:?} as chrono::DateTime<Utc>").into()),
67        }
68    }
69}
70
71// ---- NaiveDateTime (TIMESTAMP without TZ) ----
72
73impl Type<Spg> for NaiveDateTime {
74    fn type_info() -> SpgTypeInfo {
75        SpgTypeInfo::of(Kind::Timestamp)
76    }
77    fn compatible(ty: &SpgTypeInfo) -> bool {
78        matches!(ty.kind(), Kind::Timestamp | Kind::Timestamptz)
79    }
80}
81
82impl<'q> Encode<'q, Spg> for NaiveDateTime {
83    fn encode_by_ref(
84        &self,
85        buf: &mut Vec<SpgArgumentValue<'q>>,
86    ) -> Result<IsNull, BoxDynError> {
87        let s = self.format("%Y-%m-%d %H:%M:%S%.6f").to_string();
88        buf.push(SpgArgumentValue {
89            value: EngineValue::Text(s),
90            type_info: Some(<NaiveDateTime as Type<Spg>>::type_info()),
91            _phantom: core::marker::PhantomData,
92        });
93        Ok(IsNull::No)
94    }
95}
96
97impl<'r> Decode<'r, Spg> for NaiveDateTime {
98    fn decode(value: SpgValueRef<'r>) -> Result<Self, BoxDynError> {
99        match value.engine() {
100            EngineValue::Timestamp(micros) => {
101                let secs = micros.div_euclid(1_000_000);
102                let nanos = (micros.rem_euclid(1_000_000) as u32) * 1_000;
103                let dt = DateTime::<Utc>::from_timestamp(secs, nanos)
104                    .ok_or_else(|| {
105                        format!("TIMESTAMP value {micros} µs out of chrono range")
106                    })?;
107                Ok(dt.naive_utc())
108            }
109            other => Err(format!("cannot decode {other:?} as chrono::NaiveDateTime").into()),
110        }
111    }
112}
113
114// ---- NaiveDate (DATE) ----
115
116impl Type<Spg> for NaiveDate {
117    fn type_info() -> SpgTypeInfo {
118        SpgTypeInfo::of(Kind::Date)
119    }
120    fn compatible(ty: &SpgTypeInfo) -> bool {
121        matches!(ty.kind(), Kind::Date)
122    }
123}
124
125impl<'q> Encode<'q, Spg> for NaiveDate {
126    fn encode_by_ref(
127        &self,
128        buf: &mut Vec<SpgArgumentValue<'q>>,
129    ) -> Result<IsNull, BoxDynError> {
130        let s = format!("{:04}-{:02}-{:02}", self.year(), self.month(), self.day());
131        buf.push(SpgArgumentValue {
132            value: EngineValue::Text(s),
133            type_info: Some(<NaiveDate as Type<Spg>>::type_info()),
134            _phantom: core::marker::PhantomData,
135        });
136        Ok(IsNull::No)
137    }
138}
139
140impl<'r> Decode<'r, Spg> for NaiveDate {
141    fn decode(value: SpgValueRef<'r>) -> Result<Self, BoxDynError> {
142        match value.engine() {
143            EngineValue::Date(d) => {
144                // SPG stores DATE as i32 days since Unix epoch.
145                let epoch = NaiveDate::from_ymd_opt(1970, 1, 1)
146                    .ok_or("1970-01-01 should be a valid date")?;
147                epoch
148                    .checked_add_signed(chrono::Duration::days(i64::from(*d)))
149                    .ok_or_else(|| format!("DATE value {d} out of chrono range").into())
150            }
151            other => Err(format!("cannot decode {other:?} as chrono::NaiveDate").into()),
152        }
153    }
154}
155
156// Hush unused-imports — Timelike is reached only via the
157// format!() chain on NaiveDateTime.
158#[allow(dead_code)]
159fn _timelike_marker(t: NaiveDateTime) -> u32 {
160    t.hour()
161}