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(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
36        // Convert to PG-canonical `YYYY-MM-DD HH:MM:SS.fff+00`
37        // text. The engine's v7.15.0 TIMESTAMPTZ parser accepts
38        // this shape and stores as i64 µs UTC.
39        let s = self.format("%Y-%m-%d %H:%M:%S%.6f+00").to_string();
40        buf.push(SpgArgumentValue {
41            value: EngineValue::Text(s),
42            type_info: Some(<DateTime<Utc> as Type<Spg>>::type_info()),
43            _phantom: core::marker::PhantomData,
44        });
45        Ok(IsNull::No)
46    }
47}
48
49impl<'r> Decode<'r, Spg> for DateTime<Utc> {
50    fn decode(value: SpgValueRef<'r>) -> Result<Self, BoxDynError> {
51        match value.engine() {
52            EngineValue::Timestamp(micros) => {
53                let secs = micros.div_euclid(1_000_000);
54                let nanos = (micros.rem_euclid(1_000_000) as u32) * 1_000;
55                let dt = DateTime::<Utc>::from_timestamp(secs, nanos)
56                    .ok_or_else(|| format!("TIMESTAMPTZ value {micros} µs out of chrono range"))?;
57                Ok(dt)
58            }
59            other => Err(format!("cannot decode {other:?} as chrono::DateTime<Utc>").into()),
60        }
61    }
62}
63
64// ---- NaiveDateTime (TIMESTAMP without TZ) ----
65
66impl Type<Spg> for NaiveDateTime {
67    fn type_info() -> SpgTypeInfo {
68        SpgTypeInfo::of(Kind::Timestamp)
69    }
70    fn compatible(ty: &SpgTypeInfo) -> bool {
71        matches!(ty.kind(), Kind::Timestamp | Kind::Timestamptz)
72    }
73}
74
75impl<'q> Encode<'q, Spg> for NaiveDateTime {
76    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
77        let s = self.format("%Y-%m-%d %H:%M:%S%.6f").to_string();
78        buf.push(SpgArgumentValue {
79            value: EngineValue::Text(s),
80            type_info: Some(<NaiveDateTime as Type<Spg>>::type_info()),
81            _phantom: core::marker::PhantomData,
82        });
83        Ok(IsNull::No)
84    }
85}
86
87impl<'r> Decode<'r, Spg> for NaiveDateTime {
88    fn decode(value: SpgValueRef<'r>) -> Result<Self, BoxDynError> {
89        match value.engine() {
90            EngineValue::Timestamp(micros) => {
91                let secs = micros.div_euclid(1_000_000);
92                let nanos = (micros.rem_euclid(1_000_000) as u32) * 1_000;
93                let dt = DateTime::<Utc>::from_timestamp(secs, nanos)
94                    .ok_or_else(|| format!("TIMESTAMP value {micros} µs out of chrono range"))?;
95                Ok(dt.naive_utc())
96            }
97            other => Err(format!("cannot decode {other:?} as chrono::NaiveDateTime").into()),
98        }
99    }
100}
101
102// ---- NaiveDate (DATE) ----
103
104impl Type<Spg> for NaiveDate {
105    fn type_info() -> SpgTypeInfo {
106        SpgTypeInfo::of(Kind::Date)
107    }
108    fn compatible(ty: &SpgTypeInfo) -> bool {
109        matches!(ty.kind(), Kind::Date)
110    }
111}
112
113impl<'q> Encode<'q, Spg> for NaiveDate {
114    fn encode_by_ref(&self, buf: &mut Vec<SpgArgumentValue<'q>>) -> Result<IsNull, BoxDynError> {
115        let s = format!("{:04}-{:02}-{:02}", self.year(), self.month(), self.day());
116        buf.push(SpgArgumentValue {
117            value: EngineValue::Text(s),
118            type_info: Some(<NaiveDate as Type<Spg>>::type_info()),
119            _phantom: core::marker::PhantomData,
120        });
121        Ok(IsNull::No)
122    }
123}
124
125impl<'r> Decode<'r, Spg> for NaiveDate {
126    fn decode(value: SpgValueRef<'r>) -> Result<Self, BoxDynError> {
127        match value.engine() {
128            EngineValue::Date(d) => {
129                // SPG stores DATE as i32 days since Unix epoch.
130                let epoch = NaiveDate::from_ymd_opt(1970, 1, 1)
131                    .ok_or("1970-01-01 should be a valid date")?;
132                epoch
133                    .checked_add_signed(chrono::Duration::days(i64::from(*d)))
134                    .ok_or_else(|| format!("DATE value {d} out of chrono range").into())
135            }
136            other => Err(format!("cannot decode {other:?} as chrono::NaiveDate").into()),
137        }
138    }
139}
140
141// Hush unused-imports — Timelike is reached only via the
142// format!() chain on NaiveDateTime.
143#[allow(dead_code)]
144fn _timelike_marker(t: NaiveDateTime) -> u32 {
145    t.hour()
146}