diff --git a/sqlx-jiff/Cargo.toml b/sqlx-jiff/Cargo.toml index 0b5daa55..67d3837f 100644 --- a/sqlx-jiff/Cargo.toml +++ b/sqlx-jiff/Cargo.toml @@ -18,4 +18,5 @@ postgres = ["sqlx/postgres"] [dependencies] jiff = { path = ".." } -sqlx = { version = "0.8.2" } +serde = { version = "1.0" } +sqlx = { version = "0.8" } diff --git a/sqlx-jiff/src/lib.rs b/sqlx-jiff/src/lib.rs index e55fad4a..b5cab558 100644 --- a/sqlx-jiff/src/lib.rs +++ b/sqlx-jiff/src/lib.rs @@ -1,2 +1,34 @@ +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; + #[cfg(feature = "postgres")] -mod postgres; \ No newline at end of file +mod postgres; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Timestamp(pub jiff::Timestamp); + +impl From for jiff::Timestamp { + fn from(ts: Timestamp) -> Self { + ts.0 + } +} + +impl From for Timestamp { + fn from(ts: jiff::Timestamp) -> Self { + Self(ts) + } +} + +impl Deref for Timestamp { + type Target = jiff::Timestamp; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Timestamp { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/sqlx-jiff/src/postgres.rs b/sqlx-jiff/src/postgres.rs index e69de29b..6ad36a0a 100644 --- a/sqlx-jiff/src/postgres.rs +++ b/sqlx-jiff/src/postgres.rs @@ -0,0 +1,69 @@ +use crate::Timestamp; +use jiff::SignedDuration; +use sqlx::encode::IsNull; +use sqlx::error::BoxDynError; +use sqlx::postgres::types::Oid; +use sqlx::postgres::{ + PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, +}; +use sqlx::{Database, Decode, Encode, Postgres, Type}; +use std::str::FromStr; + +impl Type for Timestamp { + fn type_info() -> PgTypeInfo { + // 1184 => PgType::Timestamptz + PgTypeInfo::with_oid(Oid(1184)) + } +} + +impl PgHasArrayType for Timestamp { + fn array_type_info() -> PgTypeInfo { + // 1185 => PgType::TimestamptzArray + PgTypeInfo::with_oid(Oid(1185)) + } +} + +impl Encode<'_, Postgres> for Timestamp { + fn encode_by_ref( + &self, + buf: &mut PgArgumentBuffer, + ) -> Result { + // TIMESTAMP is encoded as the microseconds since the epoch + let micros = + self.0.duration_since(postgres_epoch_timestamp()).as_micros(); + let micros = i64::try_from(micros).map_err(|_| { + format!("Timestamp {} out of range for Postgres: {micros}", self.0) + })?; + Encode::::encode(micros, buf) + } + + fn size_hint(&self) -> usize { + size_of::() + } +} + +impl<'r> Decode<'r, Postgres> for Timestamp { + fn decode( + value: ::ValueRef<'r>, + ) -> Result { + Ok(match value.format() { + PgValueFormat::Binary => { + // TIMESTAMP is encoded as the microseconds since the epoch + let us = Decode::::decode(value)?; + let ts = postgres_epoch_timestamp() + .checked_add(SignedDuration::from_micros(us))?; + Timestamp(ts) + } + PgValueFormat::Text => { + let s = value.as_str()?; + let ts = jiff::Timestamp::from_str(s)?; + Timestamp(ts) + } + }) + } +} + +fn postgres_epoch_timestamp() -> jiff::Timestamp { + jiff::Timestamp::from_str("2000-01-01T00:00:00Z") + .expect("2000-01-01T00:00:00Z is a valid timestamp") +}