Skip to content

Commit 4ecc5db

Browse files
committed
Add support for the PG time type
I'm actually working on interval support, and being able to do things like `datetime_col + 90.days()`. Interval turns out to actually be a more complex type than I expected, and the first 8 bits of it are literally a time value, so I'm adding this piece separately.
1 parent 15c1cc3 commit 4ecc5db

4 files changed

Lines changed: 82 additions & 40 deletions

File tree

src/types/impls/date_and_time.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,16 @@ pub struct PgTimestamp(pub i64);
1313
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1414
pub struct PgDate(pub i32);
1515

16+
/// Time is represented in Postgres as a 64 bit signed integer representing the number of
17+
/// microseconds since midnight. This struct is a dumb wrapper type, meant only to indicate the
18+
/// integer's meaning.
19+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
20+
pub struct PgTime(pub i64);
21+
1622
primitive_impls! {
1723
Timestamp -> (PgTimestamp, 1114),
1824
Date -> (PgDate, 1082),
25+
Time -> (PgTime, 1083),
1926
}
2027

2128
impl ToSql<types::Timestamp> for PgTimestamp {
@@ -43,3 +50,16 @@ impl FromSql<types::Date> for PgDate {
4350
.map(PgDate)
4451
}
4552
}
53+
54+
impl ToSql<types::Time> for PgTime {
55+
fn to_sql<W: Write>(&self, out: &mut W) -> Result<IsNull, Box<Error>> {
56+
ToSql::<types::BigInt>::to_sql(&self.0, out)
57+
}
58+
}
59+
60+
impl FromSql<types::Time> for PgTime {
61+
fn from_sql(bytes: Option<&[u8]>) -> Result<Self, Box<Error>> {
62+
FromSql::<types::BigInt>::from_sql(bytes)
63+
.map(PgTime)
64+
}
65+
}

src/types/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ mod ord;
22
mod impls;
33

44
pub mod structs {
5-
pub use super::impls::date_and_time::{PgTimestamp, PgDate};
5+
pub use super::impls::date_and_time::{PgTimestamp, PgDate, PgTime};
66
}
77

88
pub use self::ord::SqlOrd;
@@ -30,6 +30,7 @@ pub type BigSerial = BigInt;
3030
#[derive(Clone, Copy)] pub struct Binary;
3131

3232
#[derive(Clone, Copy)] pub struct Date;
33+
#[derive(Clone, Copy)] pub struct Time;
3334
#[derive(Clone, Copy)] pub struct Timestamp;
3435

3536
#[derive(Clone, Copy)] pub struct Nullable<T: NativeSqlType>(T);

tests/expressions/date_and_time.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use schema::connection;
22
use yaqb::*;
3+
use yaqb::types::structs::*;
34
use yaqb::expression::dsl::*;
45

56
table! {
@@ -10,6 +11,13 @@ table! {
1011
}
1112
}
1213

14+
table! {
15+
has_time {
16+
id -> Serial,
17+
time -> Time,
18+
}
19+
}
20+
1321
#[test]
1422
fn now_executes_sql_function_now() {
1523
use self::has_timestamps::dsl::*;
@@ -52,10 +60,35 @@ fn date_uses_sql_function_date() {
5260
assert_eq!(expected_data, actual_data);
5361
}
5462

63+
#[test]
64+
fn time_is_deserialized_properly() {
65+
use self::has_time::dsl::*;
66+
67+
let connection = connection();
68+
setup_test_table(&connection);
69+
connection.execute("INSERT INTO has_time (\"time\") VALUES
70+
('00:00:01'), ('00:02:00'), ('03:00:00')
71+
").unwrap();
72+
let one_second = PgTime(1_000_000);
73+
let two_minutes = PgTime(120_000_000);
74+
let three_hours = PgTime(10_800_000_000);
75+
let expected_data = vec![one_second, two_minutes, three_hours];
76+
77+
let actual_data: Vec<_> = has_time.select(time)
78+
.load(&connection)
79+
.unwrap().collect();
80+
assert_eq!(expected_data, actual_data);
81+
}
82+
83+
5584
fn setup_test_table(conn: &Connection) {
5685
conn.execute("CREATE TABLE has_timestamps (
5786
id SERIAL PRIMARY KEY,
5887
created_at TIMESTAMP NOT NULL,
5988
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
6089
)").unwrap();
90+
conn.execute("CREATE TABLE has_time (
91+
id SERIAL PRIMARY KEY,
92+
\"time\" TIME NOT NULL
93+
)").unwrap();
6194
}

tests/types_roundtrip.rs

Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use quickcheck::quickcheck;
33
use schema::connection;
44
use yaqb::*;
55
use yaqb::result::Error;
6+
use yaqb::types::structs::*;
67
use yaqb::types::{NativeSqlType, ToSql, Nullable, Array};
78

89
fn test_type_round_trips<ST, T>(value: T, type_name: &str) -> bool where
@@ -16,7 +17,7 @@ fn test_type_round_trips<ST, T>(value: T, type_name: &str) -> bool where
1617
Ok(mut val) => value == val.nth(0).unwrap(),
1718
Err(Error::DatabaseError(msg)) =>
1819
&msg == "ERROR: invalid byte sequence for encoding \"UTF8\": 0x00\n",
19-
_ => false,
20+
Err(e) => panic!("Query failed: {:?}", e),
2021
}
2122
}
2223

@@ -53,48 +54,35 @@ test_round_trip!(string_roundtrips, VarChar, String, "varchar");
5354
test_round_trip!(text_roundtrips, Text, String, "text");
5455
test_round_trip!(binary_roundtrips, Binary, Vec<u8>, "bytea");
5556

56-
#[test]
57-
fn timestamp_roundtrip() {
58-
use yaqb::types::structs::PgTimestamp;
57+
macro_rules! test_newtype_round_trip {
58+
($test_name:ident, $sql_type:ident, $newtype:ident, $tpe:ty, $sql_type_name:expr) => {
59+
#[test]
60+
fn $test_name() {
61+
fn round_trip(val: $tpe) -> bool {
62+
test_type_round_trips::<types::$sql_type, _>($newtype(val), $sql_type_name)
63+
}
5964

60-
fn round_trip(val: i64) -> bool {
61-
test_type_round_trips::<types::Timestamp, _>(PgTimestamp(val), "timestamp")
62-
}
65+
fn option_round_trip(val: Option<$tpe>) -> bool {
66+
let val = val.map($newtype);
67+
test_type_round_trips::<Nullable<types::$sql_type>, _>(val, $sql_type_name)
68+
}
6369

64-
fn option_round_trip(val: Option<i64>) -> bool {
65-
let val = val.map(PgTimestamp);
66-
test_type_round_trips::<Nullable<types::Timestamp>, _>(val, "timestamp")
67-
}
70+
fn vec_round_trip(val: Vec<$tpe>) -> bool {
71+
let val: Vec<_> = val.into_iter().map($newtype).collect();
72+
test_type_round_trips::<Array<types::$sql_type>, _>(val, concat!($sql_type_name, "[]"))
73+
}
6874

69-
fn vec_round_trip(val: Vec<i64>) -> bool {
70-
let val: Vec<_> = val.into_iter().map(PgTimestamp).collect();
71-
test_type_round_trips::<Array<types::Timestamp>, _>(val, "timestamp[]")
75+
quickcheck(round_trip as fn($tpe) -> bool);
76+
quickcheck(option_round_trip as fn(Option<$tpe>) -> bool);
77+
quickcheck(vec_round_trip as fn(Vec<$tpe>) -> bool);
78+
}
7279
}
73-
74-
quickcheck(round_trip as fn(i64) -> bool);
75-
quickcheck(option_round_trip as fn(Option<i64>) -> bool);
76-
quickcheck(vec_round_trip as fn(Vec<i64>) -> bool);
7780
}
7881

79-
#[test]
80-
fn date_roundtrip() {
81-
use yaqb::types::structs::PgDate;
82-
83-
fn round_trip(val: i32) -> bool {
84-
test_type_round_trips::<types::Date, _>(PgDate(val), "date")
85-
}
86-
87-
fn option_round_trip(val: Option<i32>) -> bool {
88-
let val = val.map(PgDate);
89-
test_type_round_trips::<Nullable<types::Date>, _>(val, "date")
90-
}
91-
92-
fn vec_round_trip(val: Vec<i32>) -> bool {
93-
let val: Vec<_> = val.into_iter().map(PgDate).collect();
94-
test_type_round_trips::<Array<types::Date>, _>(val, "date[]")
95-
}
96-
97-
quickcheck(round_trip as fn(i32) -> bool);
98-
quickcheck(option_round_trip as fn(Option<i32>) -> bool);
99-
quickcheck(vec_round_trip as fn(Vec<i32>) -> bool);
82+
fn to_pg_time(int: i64) -> ::yaqb::types::structs::PgTime {
83+
PgTime(::std::cmp::max(0, int))
10084
}
85+
86+
test_newtype_round_trip!(date_roundtrips, Date, PgDate, i32, "date");
87+
test_newtype_round_trip!(time_roundtrips, Time, to_pg_time, i64, "time");
88+
test_newtype_round_trip!(timestamp_roundtrips, Timestamp, PgTimestamp, i64, "timestamp");

0 commit comments

Comments
 (0)