From 688129c548d8d0f591149d5ffe6594060abb2ab7 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 23 Oct 2024 15:18:17 +0800 Subject: [PATCH] bridge Date and Time Signed-off-by: tison --- jiff-sqlx/src/postgres/date.rs | 63 +++++++++++++++++++++++++++ jiff-sqlx/src/postgres/datetime.rs | 68 ++++++++++++++++++++++++++++++ jiff-sqlx/src/postgres/mod.rs | 2 +- jiff-sqlx/src/postgres/time.rs | 63 +++++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 1 deletion(-) diff --git a/jiff-sqlx/src/postgres/date.rs b/jiff-sqlx/src/postgres/date.rs index e69de29b..c39cd53a 100644 --- a/jiff-sqlx/src/postgres/date.rs +++ b/jiff-sqlx/src/postgres/date.rs @@ -0,0 +1,63 @@ +use crate::{Date, ToDate}; +use sqlx::encode::IsNull; +use sqlx::error::BoxDynError; +use sqlx::postgres::types::Oid; +use sqlx::postgres::{ + PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, +}; +use sqlx::{Decode, Encode, Postgres, Type}; + +impl Type for Date { + fn type_info() -> PgTypeInfo { + // 1082 => PgType::Date + PgTypeInfo::with_oid(Oid(1082)) + } +} + +impl PgHasArrayType for Date { + fn array_type_info() -> PgTypeInfo { + // 1182 => PgType::DateArray + PgTypeInfo::with_oid(Oid(1182)) + } +} + +impl Encode<'_, Postgres> for Date { + fn encode_by_ref( + &self, + buf: &mut PgArgumentBuffer, + ) -> Result { + let date = self.to_jiff(); + + // DATE is encoded as the days since epoch + let days = date.since(postgres_epoch_date())?.get_days(); + Encode::::encode(days, buf) + } + + fn size_hint(&self) -> usize { + size_of::() + } +} + +impl<'r> Decode<'r, Postgres> for Date { + fn decode(value: PgValueRef<'r>) -> Result { + Ok(match value.format() { + PgValueFormat::Binary => { + // DATE is encoded as the days since epoch + let days: i32 = Decode::::decode(value)?; + let date = jiff::Span::new() + .try_days(days) + .and_then(|s| postgres_epoch_date().checked_add(s))?; + date.to_sqlx() + } + PgValueFormat::Text => { + let s = value.as_str()?; + let date = jiff::civil::Date::strptime("%Y-%m-%d", s)?; + date.to_sqlx() + } + }) + } +} + +const fn postgres_epoch_date() -> jiff::civil::Date { + jiff::civil::Date::constant(2000, 1, 1) +} diff --git a/jiff-sqlx/src/postgres/datetime.rs b/jiff-sqlx/src/postgres/datetime.rs index e69de29b..d1821544 100644 --- a/jiff-sqlx/src/postgres/datetime.rs +++ b/jiff-sqlx/src/postgres/datetime.rs @@ -0,0 +1,68 @@ +use crate::{DateTime, ToDateTime}; +use jiff::SignedDuration; +use sqlx::encode::IsNull; +use sqlx::error::BoxDynError; +use sqlx::postgres::types::Oid; +use sqlx::postgres::{ + PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, +}; +use sqlx::{Decode, Encode, Postgres, Type}; +use std::str::FromStr; + +impl Type for DateTime { + fn type_info() -> PgTypeInfo { + // 1114 => PgType::Timestamp + PgTypeInfo::with_oid(Oid(1114)) + } +} + +impl PgHasArrayType for DateTime { + fn array_type_info() -> PgTypeInfo { + // 1115 => PgType::TimestampArray + PgTypeInfo::with_oid(Oid(1115)) + } +} + +impl Encode<'_, Postgres> for DateTime { + fn encode_by_ref( + &self, + buf: &mut PgArgumentBuffer, + ) -> Result { + let datetime = self.to_jiff(); + + // TIMESTAMP is encoded as the microseconds since the epoch + let micros = + datetime.duration_since(postgres_epoch_datetime()).as_micros(); + let micros = i64::try_from(micros).map_err(|_| { + format!("DateTime {datetime} out of range for Postgres: {micros}") + })?; + Encode::::encode(micros, buf) + } + + fn size_hint(&self) -> usize { + size_of::() + } +} + +impl<'r> Decode<'r, Postgres> for DateTime { + fn decode(value: PgValueRef<'r>) -> Result { + Ok(match value.format() { + PgValueFormat::Binary => { + // TIMESTAMP is encoded as the microseconds since the epoch + let us = Decode::::decode(value)?; + let datetime = postgres_epoch_datetime() + .checked_add(SignedDuration::from_micros(us))?; + datetime.to_sqlx() + } + PgValueFormat::Text => { + let s = value.as_str()?; + let datetime = jiff::civil::DateTime::from_str(s)?; + datetime.to_sqlx() + } + }) + } +} + +const fn postgres_epoch_datetime() -> jiff::civil::DateTime { + jiff::civil::DateTime::constant(2000, 1, 1, 0, 0, 0, 0) +} diff --git a/jiff-sqlx/src/postgres/mod.rs b/jiff-sqlx/src/postgres/mod.rs index e0bc0c1c..d7750fc8 100644 --- a/jiff-sqlx/src/postgres/mod.rs +++ b/jiff-sqlx/src/postgres/mod.rs @@ -2,4 +2,4 @@ mod date; mod datetime; mod interval; mod time; -mod timestamp; \ No newline at end of file +mod timestamp; diff --git a/jiff-sqlx/src/postgres/time.rs b/jiff-sqlx/src/postgres/time.rs index e69de29b..ef2a349e 100644 --- a/jiff-sqlx/src/postgres/time.rs +++ b/jiff-sqlx/src/postgres/time.rs @@ -0,0 +1,63 @@ +use crate::{Time, ToTime}; +use jiff::SignedDuration; +use sqlx::encode::IsNull; +use sqlx::error::BoxDynError; +use sqlx::postgres::types::Oid; +use sqlx::postgres::{ + PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, +}; +use sqlx::{Decode, Encode, Postgres, Type}; + +impl Type for Time { + fn type_info() -> PgTypeInfo { + // 1083 => PgType::Time + PgTypeInfo::with_oid(Oid(1083)) + } +} + +impl PgHasArrayType for Time { + fn array_type_info() -> PgTypeInfo { + // 1183 => PgType::TimeArray + PgTypeInfo::with_oid(Oid(1183)) + } +} + +impl Encode<'_, Postgres> for Time { + fn encode_by_ref( + &self, + buf: &mut PgArgumentBuffer, + ) -> Result { + let time = self.to_jiff(); + + // TIME is encoded as the microseconds since midnight + let micros = + time.duration_since(jiff::civil::Time::midnight()).as_micros(); + let micros = i64::try_from(micros).map_err(|_| { + format!("Time {time} out of range for Postgres: {micros}") + })?; + Encode::::encode(micros, buf) + } + + fn size_hint(&self) -> usize { + size_of::() + } +} + +impl<'r> Decode<'r, Postgres> for Time { + fn decode(value: PgValueRef<'r>) -> Result { + Ok(match value.format() { + PgValueFormat::Binary => { + // TIME is encoded as the microseconds since midnight + let us: i64 = Decode::::decode(value)?; + let time = jiff::civil::Time::midnight() + .checked_add(SignedDuration::from_micros(us))?; + time.to_sqlx() + } + PgValueFormat::Text => { + let s = value.as_str()?; + let time = jiff::civil::Time::strptime("%H:%M:%S%.f", s)?; + time.to_sqlx() + } + }) + } +}