Skip to content

Commit

Permalink
Add support for timestamp with time zone
Browse files Browse the repository at this point in the history
This adds support for the `timestamp with time zone` type. This is part
of the SQL standard, but not supported explicitly by SQLite as far as I
can tell. As such I've made it a PG specific type for now.

timestamp with time zone does not actually mean that a time zone is
stored. It instead means that when dealing with strings, Postgres will
no longer ignore the time zone portion. It will convert the time zone to
UTC for storage. When transmitted as text, it will be converted to the
database's local time zone. When transmitted as binary, it will be sent
as UTC.

As such, I've provided `ToSql` implementations for basically all flavors
of `DateTime`, but I've only provided a `FromSql` implementation for
`DateTime<UTC>` and `NaiveDateTime`. I have not provided any
implementation for `std::time::SystemTime`, as it implies local time
zone for the machine and we do not have the tools to handle the
conversion in the standard library.

Fixes #106.
Fixes #295.
Fixes #402.
  • Loading branch information
sgrif committed Aug 18, 2016
1 parent 8549f5c commit 2b1f748
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 14 deletions.
12 changes: 9 additions & 3 deletions diesel/src/pg/expression/date_and_time.rs
Expand Up @@ -3,7 +3,13 @@ use expression::{Expression, SelectableExpression, NonAggregate};
use pg::{Pg, PgQueryBuilder};
use query_builder::*;
use result::QueryResult;
use types::{Timestamp, VarChar};
use types::{Timestamp, Timestamptz, Date, VarChar};

/// Marker trait for types which are valid in `AT TIME ZONE` expressions
pub trait DateTimeLike {}
impl DateTimeLike for Date {}
impl DateTimeLike for Timestamp {}
impl DateTimeLike for Timestamptz {}

#[derive(Debug, Copy, Clone)]
pub struct AtTimeZone<Ts, Tz> {
Expand All @@ -21,10 +27,10 @@ impl<Ts, Tz> AtTimeZone<Ts, Tz> {
}

impl<Ts, Tz> Expression for AtTimeZone<Ts, Tz> where
Ts: Expression<SqlType=Timestamp>,
Ts: Expression,
Ts::SqlType: DateTimeLike,
Tz: Expression<SqlType=VarChar>,
{
// FIXME: This should be Timestamptz when we support that type
type SqlType = Timestamp;
}

Expand Down
4 changes: 3 additions & 1 deletion diesel/src/pg/expression/expression_methods.rs
Expand Up @@ -40,7 +40,7 @@ use super::date_and_time::AtTimeZone;
use types::{VarChar, Timestamp};

#[doc(hidden)]
pub trait PgTimestampExpressionMethods: Expression<SqlType=Timestamp> + Sized {
pub trait PgTimestampExpressionMethods: Expression + Sized {
/// Returns a PostgreSQL "AT TIME ZONE" expression
fn at_time_zone<T>(self, timezone: T) -> AtTimeZone<Self, T::Expression> where
T: AsExpression<VarChar>,
Expand All @@ -50,6 +50,8 @@ pub trait PgTimestampExpressionMethods: Expression<SqlType=Timestamp> + Sized {
}

impl<T: Expression<SqlType=Timestamp>> PgTimestampExpressionMethods for T {}
impl<T: Expression<SqlType=Timestamptz>> PgTimestampExpressionMethods for T {}
impl<T: Expression<SqlType=Date>> PgTimestampExpressionMethods for T {}

pub trait ArrayExpressionMethods<ST>: Expression<SqlType=Array<ST>> + Sized {
/// Compares two arrays for common elements, using the `&&` operator in
Expand Down
62 changes: 58 additions & 4 deletions diesel/src/pg/types/date_and_time/chrono.rs
Expand Up @@ -4,23 +4,27 @@ extern crate chrono;

use std::error::Error;
use std::io::Write;
use self::chrono::{Duration, NaiveDateTime, NaiveDate, NaiveTime};
use self::chrono::{Duration, NaiveDateTime, NaiveDate, NaiveTime, DateTime, TimeZone, UTC, FixedOffset, Local};
use self::chrono::naive::date;

use pg::Pg;
use super::{PgDate, PgTime, PgTimestamp};
use types::{self, Date, FromSql, IsNull, Time, Timestamp, ToSql};
use types::{self, Date, FromSql, IsNull, Time, Timestamp, Timestamptz, ToSql};

expression_impls! {
Date -> NaiveDate,
Time -> NaiveTime,
Timestamp -> NaiveDateTime,
Timestamptz -> DateTime<UTC>,
Timestamptz -> DateTime<FixedOffset>,
Timestamptz -> DateTime<Local>,
}

queryable_impls! {
Date -> NaiveDate,
Time -> NaiveTime,
Timestamp -> NaiveDateTime,
Timestamptz -> DateTime<UTC>,
}

// Postgres timestamps start from January 1st 2000.
Expand Down Expand Up @@ -54,6 +58,31 @@ impl ToSql<Timestamp, Pg> for NaiveDateTime {
}
}

impl FromSql<Timestamptz, Pg> for NaiveDateTime {
fn from_sql(bytes: Option<&[u8]>) -> Result<Self, Box<Error+Send+Sync>> {
FromSql::<Timestamp, Pg>::from_sql(bytes)
}
}

impl ToSql<Timestamptz, Pg> for NaiveDateTime {
fn to_sql<W: Write>(&self, out: &mut W) -> Result<IsNull, Box<Error+Send+Sync>> {
ToSql::<Timestamp, Pg>::to_sql(self, out)
}
}

impl FromSql<Timestamptz, Pg> for DateTime<UTC> {
fn from_sql(bytes: Option<&[u8]>) -> Result<Self, Box<Error+Send+Sync>> {
let naive_date_time = try!(<NaiveDateTime as FromSql<Timestamptz, Pg>>::from_sql(bytes));
Ok(DateTime::from_utc(naive_date_time, UTC))
}
}

impl<TZ: TimeZone> ToSql<Timestamptz, Pg> for DateTime<TZ> {
fn to_sql<W: Write>(&self, out: &mut W) -> Result<IsNull, Box<Error+Send+Sync>> {
ToSql::<Timestamptz, Pg>::to_sql(&self.naive_utc(), out)
}
}

fn midnight() -> NaiveTime {
NaiveTime::from_hms(0, 0, 0)
}
Expand Down Expand Up @@ -106,15 +135,15 @@ mod tests {
extern crate dotenv;
extern crate chrono;

use self::chrono::{Duration, NaiveDate, NaiveTime, UTC};
use self::chrono::{Duration, NaiveDate, NaiveTime, UTC, TimeZone, FixedOffset};
use self::chrono::naive::date;
use self::dotenv::dotenv;

use ::select;
use expression::dsl::{sql, now};
use pg::PgConnection;
use prelude::*;
use types::{Date, Time, Timestamp};
use types::{Date, Time, Timestamp, Timestamptz};

fn connection() -> PgConnection {
dotenv().ok();
Expand All @@ -132,6 +161,22 @@ mod tests {
assert!(query.get_result::<bool>(&connection).unwrap());
}

#[test]
fn unix_epoch_encodes_correctly_with_utc_timezone() {
let connection = connection();
let time = UTC.ymd(1970, 1, 1).and_hms(0, 0, 0);
let query = select(sql::<Timestamptz>("'1970-01-01Z'::timestamptz").eq(time));
assert!(query.get_result::<bool>(&connection).unwrap());
}

#[test]
fn unix_epoch_encodes_correctly_with_timezone() {
let connection = connection();
let time = FixedOffset::west(3600).ymd(1970, 1, 1).and_hms(0, 0, 0);
let query = select(sql::<Timestamptz>("'1970-01-01 01:00:00Z'::timestamptz").eq(time));
assert!(query.get_result::<bool>(&connection).unwrap());
}

#[test]
fn unix_epoch_decodes_correctly() {
let connection = connection();
Expand All @@ -141,6 +186,15 @@ mod tests {
assert_eq!(Ok(time), epoch_from_sql);
}

#[test]
fn unix_epoch_decodes_correctly_with_timezone() {
let connection = connection();
let time = UTC.ymd(1970, 1, 1).and_hms(0, 0, 0);
let epoch_from_sql = select(sql::<Timestamptz>("'1970-01-01Z'::timestamptz"))
.get_result(&connection);
assert_eq!(Ok(time), epoch_from_sql);
}

#[test]
fn times_relative_to_now_encode_correctly() {
let connection = connection();
Expand Down
17 changes: 17 additions & 0 deletions diesel/src/pg/types/date_and_time/mod.rs
Expand Up @@ -5,6 +5,9 @@ use std::ops::Add;
use pg::{Pg, PgTypeMetadata};
use types::{self, FromSql, ToSql, IsNull};

primitive_impls!(Timestamptz -> (pg: (1184, 1185)));
primitive_impls!(Timestamptz);

#[cfg(feature = "quickcheck")]
mod quickcheck_impls;
#[cfg(feature = "unstable")]
Expand Down Expand Up @@ -66,9 +69,11 @@ impl PgInterval {
queryable_impls!(Date -> PgDate,);
queryable_impls!(Time -> PgTime,);
queryable_impls!(Timestamp -> PgTimestamp,);
queryable_impls!(Timestamptz -> PgTimestamp,);
expression_impls!(Date -> PgDate,);
expression_impls!(Time -> PgTime,);
expression_impls!(Timestamp -> PgTimestamp,);
expression_impls!(Timestamptz -> PgTimestamp,);

primitive_impls!(Interval -> (PgInterval, pg: (1186, 1187)));

Expand Down Expand Up @@ -114,6 +119,18 @@ impl FromSql<types::Timestamp, Pg> for PgTimestamp {
}
}

impl ToSql<types::Timestamptz, Pg> for PgTimestamp {
fn to_sql<W: Write>(&self, out: &mut W) -> Result<IsNull, Box<Error+Send+Sync>> {
ToSql::<types::Timestamp, Pg>::to_sql(self, out)
}
}

impl FromSql<types::Timestamptz, Pg> for PgTimestamp {
fn from_sql(bytes: Option<&[u8]>) -> Result<Self, Box<Error+Send+Sync>> {
FromSql::<types::Timestamp, Pg>::from_sql(bytes)
}
}

impl ToSql<types::Date, Pg> for PgDate {
fn to_sql<W: Write>(&self, out: &mut W) -> Result<IsNull, Box<Error+Send+Sync>> {
ToSql::<types::Integer, Pg>::to_sql(&self.0, out)
Expand Down
9 changes: 6 additions & 3 deletions diesel/src/pg/types/mod.rs
Expand Up @@ -8,13 +8,16 @@ mod uuid;

#[doc(hidden)]
pub mod sql_types {
#[derive(Debug, Clone, Copy, Default)] pub struct Oid;
#[derive(Debug, Clone, Copy, Default)] pub struct Array<T>(T);
#[derive(Debug, Clone, Copy, Default)] pub struct Oid;
#[derive(Debug, Clone, Copy, Default)] pub struct Timestamptz;
#[cfg(feature = "uuid")]
#[derive(Debug, Clone, Copy, Default)] pub struct Uuid;

pub type SmallSerial = ::types::SmallInt;
pub type Serial = ::types::Integer;
pub type BigSerial = ::types::BigInt;
#[cfg(feature = "uuid")]
#[derive(Debug, Clone, Copy, Default)] pub struct Uuid;

pub type Bytea = ::types::Binary;
#[doc(hidden)]
pub type Bpchar = ::types::VarChar;
Expand Down
7 changes: 5 additions & 2 deletions diesel/src/types/impls/mod.rs
Expand Up @@ -103,6 +103,11 @@ macro_rules! primitive_impls {
};

($Source:ident -> ($Target:ty, pg: ($oid:expr, $array_oid:expr))) => {
primitive_impls!($Source -> (pg: ($oid, $array_oid)));
primitive_impls!($Source -> $Target);
};

($Source:ident -> (pg: ($oid:expr, $array_oid:expr))) => {
#[cfg(feature = "postgres")]
impl types::HasSqlType<types::$Source> for $crate::pg::Pg {
fn metadata() -> $crate::pg::PgTypeMetadata {
Expand All @@ -112,8 +117,6 @@ macro_rules! primitive_impls {
}
}
}

primitive_impls!($Source -> $Target);
};

($Source:ident -> $Target:ty) => {
Expand Down
7 changes: 6 additions & 1 deletion diesel_tests/tests/types_roundtrip.rs
@@ -1,7 +1,7 @@
extern crate chrono;

pub use quickcheck::quickcheck;
use self::chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
use self::chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime, DateTime, UTC};
use self::chrono::naive::date;

pub use schema::{connection, TestConnection};
Expand Down Expand Up @@ -98,6 +98,7 @@ mod pg_types {
test_round_trip!(naive_datetime_roundtrips, Timestamp, (i64, u32), mk_naive_datetime);
test_round_trip!(naive_time_roundtrips, Time, (u32, u32), mk_naive_time);
test_round_trip!(naive_date_roundtrips, Date, u32, mk_naive_date);
test_round_trip!(datetime_roundtrips, Timestamptz, (i64, u32), mk_datetime);
test_round_trip!(uuid_roundtrips, Uuid, (u32, u16, u16, (u8, u8, u8, u8, u8, u8, u8, u8)), mk_uuid);

fn mk_uuid(data: (u32, u16, u16, (u8, u8, u8, u8, u8, u8, u8, u8))) -> self::uuid::Uuid {
Expand All @@ -115,6 +116,10 @@ pub fn mk_naive_time(data: (u32, u32)) -> NaiveTime {
NaiveTime::from_num_seconds_from_midnight(data.0, data.1 / 1000)
}

pub fn mk_datetime(data: (i64, u32)) -> DateTime<UTC> {
DateTime::from_utc(mk_naive_datetime(data), UTC)
}

pub fn mk_naive_date(days: u32) -> NaiveDate {
let earliest_pg_date = NaiveDate::from_ymd(-4713, 11, 24);
let latest_chrono_date = date::MAX;
Expand Down

0 comments on commit 2b1f748

Please sign in to comment.