Skip to content

Commit

Permalink
Support (Pg)Interval with chrono::Duration and time::Duration
Browse files Browse the repository at this point in the history
  • Loading branch information
Rudi3 committed Sep 22, 2023
1 parent 03e95e4 commit 69c6352
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 0 deletions.
20 changes: 20 additions & 0 deletions sea-query-binder/src/sqlx_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ impl sqlx::IntoArguments<'_, sqlx::postgres::Postgres> for SqlxValues {
Value::ChronoDateTimeWithTimeZone(t) => {
args.add(t.as_deref());
}
#[cfg(all(feature = "postgres-interval", feature = "with-chrono"))]
Value::ChronoDuration(t) => {
args.add(t.as_deref());
}
#[cfg(feature = "with-time")]
Value::TimeDate(t) => {
args.add(t.as_deref());
Expand All @@ -105,6 +109,10 @@ impl sqlx::IntoArguments<'_, sqlx::postgres::Postgres> for SqlxValues {
Value::TimeDateTimeWithTimeZone(t) => {
args.add(t.as_deref());
}
#[cfg(all(feature = "postgres-interval", feature = "with-time"))]
Value::TimeDuration(t) => {
args.add(t.as_deref());
}
#[cfg(feature = "with-uuid")]
Value::Uuid(uuid) => {
args.add(uuid.as_deref());
Expand Down Expand Up @@ -252,6 +260,12 @@ impl sqlx::IntoArguments<'_, sqlx::postgres::Postgres> for SqlxValues {
);
args.add(value);
}
#[cfg(all(feature = "postgres-interval", feature = "with-chrono"))]
ArrayType::ChronoDuration => {
let value: Option<Vec<Duration>> = Value::Array(ty, v)
.expect("This Value::Array should consist of Value::ChronoDuration");
args.add(value);
}
#[cfg(feature = "with-time")]
ArrayType::TimeDate => {
let value: Option<Vec<time::Date>> = Value::Array(ty, v)
Expand All @@ -277,6 +291,12 @@ impl sqlx::IntoArguments<'_, sqlx::postgres::Postgres> for SqlxValues {
);
args.add(value);
}
#[cfg(all(feature = "postgres-interval", feature = "with-time"))]
ArrayType::TimeDuration => {
let value: Option<Vec<Duration>> = Value::Array(ty, v)
.expect("This Value::Array should consist of Value::TimeDuration");
args.add(value);
}
#[cfg(feature = "with-uuid")]
ArrayType::Uuid => {
let value: Option<Vec<Uuid>> = Value::Array(ty, v)
Expand Down
4 changes: 4 additions & 0 deletions sea-query-postgres/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ impl ToSql for PostgresValue {
Value::ChronoDateTimeLocal(v) => v.as_deref().to_sql(ty, out),
#[cfg(feature = "with-chrono")]
Value::ChronoDateTimeWithTimeZone(v) => v.as_deref().to_sql(ty, out),
#[cfg(all(feature = "postgres-interval", feature = "with-chrono"))]
Value::ChronoDuration(v) => v.as_deref().to_sql(ty, out),
#[cfg(feature = "with-time")]
Value::TimeDate(v) => v.as_deref().to_sql(ty, out),
#[cfg(feature = "with-time")]
Expand All @@ -97,6 +99,8 @@ impl ToSql for PostgresValue {
Value::TimeDateTime(v) => v.as_deref().to_sql(ty, out),
#[cfg(feature = "with-time")]
Value::TimeDateTimeWithTimeZone(v) => v.as_deref().to_sql(ty, out),
#[cfg(all(feature = "postgres-interval", feature = "with-time"))]
Value::TimeDuration(v) => v.as_deref().to_sql(ty, out),
#[cfg(feature = "with-rust_decimal")]
Value::Decimal(v) => v.as_deref().to_sql(ty, out),
#[cfg(feature = "with-bigdecimal")]
Expand Down
11 changes: 11 additions & 0 deletions src/backend/query_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,8 @@ pub trait QueryBuilder:
Value::ChronoDateTimeLocal(None) => write!(s, "NULL").unwrap(),
#[cfg(feature = "with-chrono")]
Value::ChronoDateTimeWithTimeZone(None) => write!(s, "NULL").unwrap(),
#[cfg(all(feature = "postgres-interval", feature = "with-chrono"))]
Value::ChronoDuration(None) => write!(s, "NULL").unwrap(),
#[cfg(feature = "with-time")]
Value::TimeDate(None) => write!(s, "NULL").unwrap(),
#[cfg(feature = "with-time")]
Expand All @@ -1010,6 +1012,8 @@ pub trait QueryBuilder:
Value::TimeDateTime(None) => write!(s, "NULL").unwrap(),
#[cfg(feature = "with-time")]
Value::TimeDateTimeWithTimeZone(None) => write!(s, "NULL").unwrap(),
#[cfg(all(feature = "postgres-interval", feature = "with-time"))]
Value::TimeDuration(None) => write!(s, "NULL").unwrap(),
#[cfg(feature = "with-rust_decimal")]
Value::Decimal(None) => write!(s, "NULL").unwrap(),
#[cfg(feature = "with-bigdecimal")]
Expand Down Expand Up @@ -1060,6 +1064,8 @@ pub trait QueryBuilder:
Value::ChronoDateTimeWithTimeZone(Some(v)) => {
write!(s, "'{}'", v.format("%Y-%m-%d %H:%M:%S %:z")).unwrap()
}
#[cfg(all(feature = "postgres-interval", feature = "with-chrono"))]
Value::ChronoDuration(Some(v)) => self.write_string_quoted(&v.to_string(), &mut s),
#[cfg(feature = "with-time")]
Value::TimeDate(Some(v)) => {
write!(s, "'{}'", v.format(time_format::FORMAT_DATE).unwrap()).unwrap()
Expand All @@ -1079,6 +1085,11 @@ pub trait QueryBuilder:
v.format(time_format::FORMAT_DATETIME_TZ).unwrap()
)
.unwrap(),
#[cfg(all(feature = "postgres-interval", feature = "with-time"))]
Value::TimeDuration(Some(v)) => self.write_string_quoted(
&time_duration_format::ISO8601Duration::from(v).to_string(),
&mut s,
),
#[cfg(feature = "with-rust_decimal")]
Value::Decimal(Some(v)) => write!(s, "{v}").unwrap(),
#[cfg(feature = "with-bigdecimal")]
Expand Down
156 changes: 156 additions & 0 deletions src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use std::str::from_utf8;
#[cfg(feature = "with-chrono")]
use chrono::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, Utc};

#[cfg(all(feature = "postgres-interval", feature = "with-chrono"))]
use chrono::Duration;

#[cfg(feature = "with-time")]
use time::{OffsetDateTime, PrimitiveDateTime};

Expand Down Expand Up @@ -79,6 +82,13 @@ pub enum ArrayType {
#[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))]
ChronoDateTimeWithTimeZone,

#[cfg(all(feature = "postgres-interval", feature = "with-chrono"))]
#[cfg_attr(
docsrs,
doc(cfg(all(feature = "postgres-interval", feature = "with-chrono")))
)]
ChronoDuration,

#[cfg(feature = "with-time")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-time")))]
TimeDate,
Expand All @@ -95,6 +105,13 @@ pub enum ArrayType {
#[cfg_attr(docsrs, doc(cfg(feature = "with-time")))]
TimeDateTimeWithTimeZone,

#[cfg(all(feature = "postgres-interval", feature = "with-time"))]
#[cfg_attr(
docsrs,
doc(cfg(all(feature = "postgres-interval", feature = "with-time")))
)]
TimeDuration,

#[cfg(feature = "with-uuid")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-uuid")))]
Uuid,
Expand Down Expand Up @@ -202,6 +219,13 @@ pub enum Value {
#[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))]
ChronoDateTimeWithTimeZone(Option<Box<DateTime<FixedOffset>>>),

#[cfg(all(feature = "postgres-interval", feature = "with-chrono"))]
#[cfg_attr(
docsrs,
doc(cfg(all(feature = "postgres-interval", feature = "with-chrono")))
)]
ChronoDuration(Option<Box<Duration>>),

#[cfg(feature = "with-time")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-time")))]
TimeDate(Option<Box<time::Date>>),
Expand All @@ -218,6 +242,13 @@ pub enum Value {
#[cfg_attr(docsrs, doc(cfg(feature = "with-time")))]
TimeDateTimeWithTimeZone(Option<Box<OffsetDateTime>>),

#[cfg(all(feature = "postgres-interval", feature = "with-time"))]
#[cfg_attr(
docsrs,
doc(cfg(all(feature = "postgres-interval", feature = "with-time")))
)]
TimeDuration(Option<Box<time::Duration>>),

#[cfg(feature = "with-uuid")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-uuid")))]
Uuid(Option<Box<Uuid>>),
Expand Down Expand Up @@ -621,6 +652,18 @@ mod with_chrono {
}
}

#[cfg(all(feature = "postgres-interval", feature = "with-chrono"))]
#[cfg_attr(
docsrs,
doc(cfg(all(feature = "postgres-interval", feature = "with-chrono")))
)]
mod with_chrono_duration {
use super::*;
use chrono::Duration;

type_to_box_value!(Duration, ChronoDuration, Interval(None, None));
}

#[cfg(feature = "with-time")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-time")))]
pub mod time_format {
Expand Down Expand Up @@ -680,6 +723,81 @@ mod with_time {
}
}

#[cfg(all(feature = "postgres-interval", feature = "with-time"))]
#[cfg_attr(
docsrs,
doc(cfg(all(feature = "postgres-interval", feature = "with-time")))
)]
mod with_time_duration {
use super::*;

type_to_box_value!(time::Duration, TimeDuration, Interval(None, None));
}

#[cfg(all(feature = "postgres-interval", feature = "with-time"))]
#[cfg_attr(
docsrs,
doc(cfg(all(feature = "postgres-interval", feature = "with-time")))
)]
pub mod time_duration_format {
use core::fmt;

// from chrono/0.4.31/src/chrono/duration.rs.html#22-39
/// The number of nanoseconds in a microsecond.
const NANOS_PER_MICRO: i32 = 1000;
/// The number of nanoseconds in a millisecond.
const NANOS_PER_MILLI: i32 = 1_000_000;
/// The number of (non-leap) seconds in days.
const SECS_PER_DAY: i64 = 86_400;

#[derive(Debug)]
pub struct ISO8601Duration {
inner: time::Duration,
}

impl ISO8601Duration {
pub fn from(d: &time::Duration) -> Self {
Self { inner: *d }
}
}

impl fmt::Display for ISO8601Duration {
// adapted from chrono/0.4.31/src/chrono/duration.rs.html#407-434
/// Format a duration using the [ISO 8601] format
///
/// [ISO 8601]: https://en.wikipedia.org/wiki/ISO_8601#Durations
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// technically speaking, negative duration is not valid ISO 8601,
// but we need to print it anyway.
let sign = if self.inner.is_negative() { "-" } else { "" };

let days = self.inner.whole_seconds() / SECS_PER_DAY;
let secs = self.inner.whole_seconds() - days * SECS_PER_DAY;
let nanos = self.inner.subsec_nanoseconds();
let hasdate = days != 0;
let hastime = (secs != 0 || nanos != 0) || !hasdate;

write!(f, "{}P", sign)?;

if hasdate {
write!(f, "{}D", days)?;
}
if hastime {
if nanos == 0 {
write!(f, "T{}S", secs)?;
} else if nanos % NANOS_PER_MILLI == 0 {
write!(f, "T{}.{:03}S", secs, nanos / NANOS_PER_MILLI)?;
} else if nanos % NANOS_PER_MICRO == 0 {
write!(f, "T{}.{:06}S", secs, nanos / NANOS_PER_MICRO)?;
} else {
write!(f, "T{}.{:09}S", secs, nanos)?;
}
}
Ok(())
}
}
}

#[cfg(feature = "with-rust_decimal")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-rust_decimal")))]
mod with_rust_decimal {
Expand Down Expand Up @@ -804,6 +922,9 @@ pub mod with_array {
#[cfg(feature = "with-chrono")]
impl<Tz> NotU8 for DateTime<Tz> where Tz: chrono::TimeZone {}

#[cfg(all(feature = "postgres-interval", feature = "with-chrono"))]
impl NotU8 for Duration {}

#[cfg(feature = "with-time")]
impl NotU8 for time::Date {}

Expand All @@ -816,6 +937,9 @@ pub mod with_array {
#[cfg(feature = "with-time")]
impl NotU8 for OffsetDateTime {}

#[cfg(all(feature = "postgres-interval", feature = "with-time"))]
impl NotU8 for time::Duration {}

#[cfg(feature = "with-rust_decimal")]
impl NotU8 for Decimal {}

Expand Down Expand Up @@ -1094,6 +1218,34 @@ impl Value {
}
}

#[cfg(all(feature = "postgres-interval", feature = "with-chrono"))]
impl Value {
pub fn is_chrono_duration(&self) -> bool {
matches!(self, Self::ChronoDuration(_))
}

pub fn as_ref_chrono_duration(&self) -> Option<&Duration> {
match self {
Self::ChronoDuration(v) => box_to_opt_ref!(v),
_ => panic!("not Value::Interval"),
}
}
}

#[cfg(all(feature = "postgres-interval", feature = "with-time"))]
impl Value {
pub fn is_time_duration(&self) -> bool {
matches!(self, Self::TimeDuration(_))
}

pub fn as_ref_time_duration(&self) -> Option<&time::Duration> {
match self {
Self::TimeDuration(v) => box_to_opt_ref!(v),
_ => panic!("not Value::Interval"),
}
}
}

#[cfg(feature = "with-rust_decimal")]
impl Value {
pub fn is_decimal(&self) -> bool {
Expand Down Expand Up @@ -1422,6 +1574,8 @@ pub fn sea_value_to_json_value(value: &Value) -> Json {
Value::ChronoDateTimeUtc(_) => CommonSqlQueryBuilder.value_to_string(value).into(),
#[cfg(feature = "with-chrono")]
Value::ChronoDateTimeLocal(_) => CommonSqlQueryBuilder.value_to_string(value).into(),
#[cfg(all(feature = "postgres-interval", feature = "with-chrono"))]
Value::ChronoDuration(_) => CommonSqlQueryBuilder.value_to_string(value).into(),
#[cfg(feature = "with-time")]
Value::TimeDate(_) => CommonSqlQueryBuilder.value_to_string(value).into(),
#[cfg(feature = "with-time")]
Expand All @@ -1430,6 +1584,8 @@ pub fn sea_value_to_json_value(value: &Value) -> Json {
Value::TimeDateTime(_) => CommonSqlQueryBuilder.value_to_string(value).into(),
#[cfg(feature = "with-time")]
Value::TimeDateTimeWithTimeZone(_) => CommonSqlQueryBuilder.value_to_string(value).into(),
#[cfg(all(feature = "postgres-interval", feature = "with-time"))]
Value::TimeDuration(_) => CommonSqlQueryBuilder.value_to_string(value).into(),
#[cfg(feature = "with-rust_decimal")]
Value::Decimal(Some(v)) => {
use rust_decimal::prelude::ToPrimitive;
Expand Down

0 comments on commit 69c6352

Please sign in to comment.