diff --git a/sea-query-binder/Cargo.toml b/sea-query-binder/Cargo.toml index ca121a2a2..691065cb8 100644 --- a/sea-query-binder/Cargo.toml +++ b/sea-query-binder/Cargo.toml @@ -42,6 +42,7 @@ with-time = ["sqlx?/time", "sea-query/with-time", "time"] with-ipnetwork = ["sqlx?/ipnetwork", "sea-query/with-ipnetwork", "ipnetwork"] with-mac_address = ["sqlx?/mac_address", "sea-query/with-mac_address", "mac_address"] postgres-array = ["sea-query/postgres-array"] +postgres-interval = ["sqlx-postgres", "sqlx/chrono"] runtime-async-std-native-tls = ["sqlx?/runtime-async-std-native-tls"] runtime-async-std-rustls = ["sqlx?/runtime-async-std-rustls", ] runtime-actix-native-tls = ["sqlx?/runtime-tokio-native-tls"] diff --git a/sea-query-binder/src/sqlx_mysql.rs b/sea-query-binder/src/sqlx_mysql.rs index abeac3862..cb15c2cd9 100644 --- a/sea-query-binder/src/sqlx_mysql.rs +++ b/sea-query-binder/src/sqlx_mysql.rs @@ -89,6 +89,8 @@ impl sqlx::IntoArguments<'_, sqlx::mysql::MySql> for SqlxValues { Value::TimeDateTimeWithTimeZone(t) => { args.add(t.as_deref()); } + #[cfg(feature = "postgres-interval")] + Value::Interval(_) => {} #[cfg(feature = "with-uuid")] Value::Uuid(uuid) => { args.add(uuid.as_deref()); diff --git a/sea-query-binder/src/sqlx_postgres.rs b/sea-query-binder/src/sqlx_postgres.rs index b0fce77a0..6bbd795d3 100644 --- a/sea-query-binder/src/sqlx_postgres.rs +++ b/sea-query-binder/src/sqlx_postgres.rs @@ -8,6 +8,8 @@ use ipnetwork::IpNetwork; use mac_address::MacAddress; #[cfg(feature = "with-rust_decimal")] use rust_decimal::Decimal; +#[cfg(feature = "postgres-interval")] +use sea_query::types::PgInterval; #[cfg(feature = "with-json")] use serde_json::Value as Json; #[cfg(feature = "with-uuid")] @@ -105,6 +107,10 @@ impl sqlx::IntoArguments<'_, sqlx::postgres::Postgres> for SqlxValues { Value::TimeDateTimeWithTimeZone(t) => { args.add(t.as_deref()); } + #[cfg(feature = "postgres-interval")] + Value::Interval(t) => { + args.add(t.as_deref()); + } #[cfg(feature = "with-uuid")] Value::Uuid(uuid) => { args.add(uuid.as_deref()); @@ -277,6 +283,12 @@ impl sqlx::IntoArguments<'_, sqlx::postgres::Postgres> for SqlxValues { ); args.add(value); } + #[cfg(feature = "postgres-interval")] + ArrayType::Interval => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::Interval"); + args.add(value); + } #[cfg(feature = "with-uuid")] ArrayType::Uuid => { let value: Option> = Value::Array(ty, v) diff --git a/sea-query-binder/src/sqlx_sqlite.rs b/sea-query-binder/src/sqlx_sqlite.rs index d100ef6d2..2425603bf 100644 --- a/sea-query-binder/src/sqlx_sqlite.rs +++ b/sea-query-binder/src/sqlx_sqlite.rs @@ -89,6 +89,8 @@ impl<'q> sqlx::IntoArguments<'q, sqlx::sqlite::Sqlite> for SqlxValues { Value::TimeDateTimeWithTimeZone(t) => { args.add(t.map(|t| *t)); } + #[cfg(feature = "postgres-interval")] + Value::Interval(_) => {} #[cfg(feature = "with-uuid")] Value::Uuid(uuid) => { args.add(uuid.map(|uuid| *uuid)); diff --git a/src/backend/query_builder.rs b/src/backend/query_builder.rs index fd8c7677d..705654777 100644 --- a/src/backend/query_builder.rs +++ b/src/backend/query_builder.rs @@ -1010,6 +1010,8 @@ pub trait QueryBuilder: Value::TimeDateTime(None) => write!(s, "NULL").unwrap(), #[cfg(feature = "with-time")] Value::TimeDateTimeWithTimeZone(None) => write!(s, "NULL").unwrap(), + #[cfg(feature = "postgres-interval")] + Value::Interval(None) => write!(s, "NULL").unwrap(), #[cfg(feature = "with-rust_decimal")] Value::Decimal(None) => write!(s, "NULL").unwrap(), #[cfg(feature = "with-bigdecimal")] @@ -1079,6 +1081,34 @@ pub trait QueryBuilder: v.format(time_format::FORMAT_DATETIME_TZ).unwrap() ) .unwrap(), + #[cfg(feature = "postgres-interval")] + Value::Interval(Some(v)) => { + let mut space = false; + + write!(s, "'").unwrap(); + + if v.months > 0 { + write!(s, "{} MONTHS", v.months).unwrap(); + space = true; + } + + if v.days > 0 { + if space { + write!(s, " ").unwrap(); + } + write!(s, "{} DAYS", v.days).unwrap(); + space = true; + } + + if v.microseconds > 0 { + if space { + write!(s, " ").unwrap(); + } + write!(s, "{} MICROSECONDS", v.microseconds).unwrap(); + } + + write!(s, "'::interval").unwrap(); + } #[cfg(feature = "with-rust_decimal")] Value::Decimal(Some(v)) => write!(s, "{v}").unwrap(), #[cfg(feature = "with-bigdecimal")] diff --git a/src/value.rs b/src/value.rs index 7049d744a..0e1b28971 100644 --- a/src/value.rs +++ b/src/value.rs @@ -95,6 +95,9 @@ pub enum ArrayType { #[cfg_attr(docsrs, doc(cfg(feature = "with-time")))] TimeDateTimeWithTimeZone, + #[cfg(feature = "postgres-interval")] + Interval, + #[cfg(feature = "with-uuid")] #[cfg_attr(docsrs, doc(cfg(feature = "with-uuid")))] Uuid, @@ -218,6 +221,9 @@ pub enum Value { #[cfg_attr(docsrs, doc(cfg(feature = "with-time")))] TimeDateTimeWithTimeZone(Option>), + #[cfg(feature = "postgres-interval")] + Interval(Option>), + #[cfg(feature = "with-uuid")] #[cfg_attr(docsrs, doc(cfg(feature = "with-uuid")))] Uuid(Option>), @@ -502,6 +508,14 @@ impl ValueType for Cow<'_, str> { type_to_box_value!(Vec, Bytes, Binary(BlobSize::Blob(None))); type_to_box_value!(String, String, String(None)); +#[cfg(feature = "postgres-interval")] +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] +pub struct PgIntervalValue { + pub months: i32, + pub days: i32, + pub microseconds: i64, +} + #[cfg(feature = "with-json")] #[cfg_attr(docsrs, doc(cfg(feature = "with-json")))] mod with_json { @@ -681,6 +695,38 @@ mod with_time { } } +#[cfg(feature = "postgres-interval")] +mod with_postgres_interval { + use super::*; + + impl From for Value { + fn from(v: PgIntervalValue) -> Self { + Value::Interval(Some(Box::new(v))) + } + } + + impl ValueType for PgIntervalValue { + fn try_from(v: Value) -> Result { + match v { + Value::Interval(Some(x)) => Ok(*x), + _ => Err(ValueTypeErr), + } + } + + fn type_name() -> String { + stringify!(PgIntervalValue).to_owned() + } + + fn array_type() -> ArrayType { + ArrayType::Interval + } + + fn column_type() -> ColumnType { + ColumnType::Interval(None, None) + } + } +} + #[cfg(feature = "with-rust_decimal")] #[cfg_attr(docsrs, doc(cfg(feature = "with-rust_decimal")))] mod with_rust_decimal { @@ -817,6 +863,9 @@ pub mod with_array { #[cfg(feature = "with-time")] impl NotU8 for OffsetDateTime {} + #[cfg(feature = "postgres-interval")] + impl NotU8 for PgIntervalValue {} + #[cfg(feature = "with-rust_decimal")] impl NotU8 for Decimal {} @@ -1095,6 +1144,20 @@ impl Value { } } +#[cfg(feature = "postgres-interval")] +impl Value { + pub fn is_interval(&self) -> bool { + matches!(self, Self::Interval(_)) + } + + pub fn as_ref_interval(&self) -> Option<&PgIntervalValue> { + match self { + Self::Interval(v) => box_to_opt_ref!(v), + _ => panic!("not Value::Interval"), + } + } +} + #[cfg(feature = "with-rust_decimal")] impl Value { pub fn is_decimal(&self) -> bool { @@ -1431,6 +1494,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(feature = "postgres-interval")] + Value::Interval(_) => CommonSqlQueryBuilder.value_to_string(value).into(), #[cfg(feature = "with-rust_decimal")] Value::Decimal(Some(v)) => { use rust_decimal::prelude::ToPrimitive; @@ -1855,6 +1920,116 @@ mod tests { ); } + #[test] + #[cfg(feature = "postgres-interval")] + fn test_pginterval_value() { + let interval = PgIntervalValue { + months: 1, + days: 2, + microseconds: 300, + }; + let value: Value = interval.into(); + let out: PgIntervalValue = value.unwrap(); + assert_eq!(out, interval); + } + + #[test] + #[cfg(feature = "postgres-interval")] + fn test_pginterval_query() { + use crate::*; + + const VALUES: [(PgIntervalValue, &str); 10] = [ + ( + PgIntervalValue { + months: 0, + days: 0, + microseconds: 1, + }, + "1 MICROSECONDS", + ), + ( + PgIntervalValue { + months: 0, + days: 0, + microseconds: 100, + }, + "100 MICROSECONDS", + ), + ( + PgIntervalValue { + months: 0, + days: 1, + microseconds: 0, + }, + "1 DAYS", + ), + ( + PgIntervalValue { + months: 0, + days: 2, + microseconds: 0, + }, + "2 DAYS", + ), + ( + PgIntervalValue { + months: 0, + days: 2, + microseconds: 100, + }, + "2 DAYS 100 MICROSECONDS", + ), + ( + PgIntervalValue { + months: 1, + days: 0, + microseconds: 0, + }, + "1 MONTHS", + ), + ( + PgIntervalValue { + months: 2, + days: 0, + microseconds: 0, + }, + "2 MONTHS", + ), + ( + PgIntervalValue { + months: 2, + days: 0, + microseconds: 100, + }, + "2 MONTHS 100 MICROSECONDS", + ), + ( + PgIntervalValue { + months: 2, + days: 2, + microseconds: 0, + }, + "2 MONTHS 2 DAYS", + ), + ( + PgIntervalValue { + months: 2, + days: 2, + microseconds: 100, + }, + "2 MONTHS 2 DAYS 100 MICROSECONDS", + ), + ]; + + for (interval, formatted) in VALUES { + let query = Query::select().expr(interval).to_owned(); + assert_eq!( + query.to_string(PostgresQueryBuilder), + format!("SELECT '{formatted}'::interval") + ); + } + } + #[test] #[cfg(feature = "with-uuid")] fn test_uuid_value() {