Skip to main content

ocpp_service/db/
ts.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)
3
4//! `TIMESTAMPTZ` ↔ [`DateTime`] without pulling a date-time crate into sqlx.
5
6use ocpp_core::DateTime;
7use sqlx::encode::IsNull;
8use sqlx::error::BoxDynError;
9use sqlx::postgres::{PgArgumentBuffer, PgTypeInfo, PgValueFormat, PgValueRef};
10use sqlx::{Decode, Encode, Postgres, Type};
11
12/// Microseconds between 1970-01-01 and the PostgreSQL epoch 2000-01-01.
13const PG_EPOCH_OFFSET_MICROS: i64 = 946_684_800_000_000;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
16pub struct Ts(pub DateTime);
17
18impl From<DateTime> for Ts {
19    fn from(d: DateTime) -> Self {
20        Ts(d)
21    }
22}
23
24impl Type<Postgres> for Ts {
25    fn type_info() -> PgTypeInfo {
26        PgTypeInfo::with_name("timestamptz")
27    }
28}
29
30impl Encode<'_, Postgres> for Ts {
31    fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
32        buf.extend_from_slice(&(self.0.unix_micros() - PG_EPOCH_OFFSET_MICROS).to_be_bytes());
33        Ok(IsNull::No)
34    }
35}
36
37impl<'r> Decode<'r, Postgres> for Ts {
38    fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
39        match value.format() {
40            PgValueFormat::Binary => {
41                let b: [u8; 8] = value.as_bytes()?.try_into()?;
42                Ok(Ts(DateTime::from_unix_micros(
43                    i64::from_be_bytes(b) + PG_EPOCH_OFFSET_MICROS,
44                )))
45            }
46            PgValueFormat::Text => Ok(Ts(DateTime::parse(value.as_str()?)?)),
47        }
48    }
49}
50
51impl serde::Serialize for Ts {
52    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
53        self.0.serialize(s)
54    }
55}
56
57impl<'de> serde::Deserialize<'de> for Ts {
58    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
59        DateTime::deserialize(d).map(Ts)
60    }
61}