From 02facb3289aaaceb48dd41989200d3e9d9b64a5d Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 11 Oct 2024 13:45:53 -0600 Subject: [PATCH] implement integration as a separate crate Signed-off-by: tison --- sqlx-jiff/Cargo.toml | 3 +- sqlx-jiff/src/lib.rs | 34 ++++++++++++++++++++- sqlx-jiff/src/postgres.rs | 62 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) 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..f7ddad00 100644 --- a/sqlx-jiff/src/lib.rs +++ b/sqlx-jiff/src/lib.rs @@ -1,2 +1,34 @@ +use std::ops::{Deref, DerefMut}; +use serde::{Deserialize, Serialize}; + #[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..61b245f6 100644 --- a/sqlx-jiff/src/postgres.rs +++ b/sqlx-jiff/src/postgres.rs @@ -0,0 +1,62 @@ +use std::str::FromStr; +use sqlx::{Database, Decode, Encode, Postgres, Type}; +use sqlx::encode::IsNull; +use sqlx::error::BoxDynError; +use sqlx::postgres::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat}; +use sqlx::postgres::types::Oid; +use jiff::SignedDuration; +use crate::Timestamp; + +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") +}