Merge pull request #45 from mehcode/ab/outatime

implement support for chrono
This commit is contained in:
Daniel Akhterov 2019-12-12 18:02:56 -08:00 committed by GitHub
commit 0865549175
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 434 additions and 21 deletions

View File

@ -23,6 +23,7 @@ unstable = [ "sqlx-core/unstable" ]
postgres = [ "sqlx-core/postgres", "sqlx-macros/postgres" ]
mariadb = [ "sqlx-core/mariadb", "sqlx-macros/mariadb" ]
macros = [ "sqlx-macros", "proc-macro-hack" ]
chrono = ["sqlx-core/chrono", "sqlx-macros/chrono"]
uuid = [ "sqlx-core/uuid", "sqlx-macros/uuid" ]
[dependencies]

View File

@ -20,6 +20,7 @@ async-std = { version = "1.2.0", default-features = false, features = [ "unstabl
async-stream = "0.2.0"
bitflags = "1.2.1"
byteorder = { version = "1.3.2", default-features = false }
chrono = { version = "0.4", optional = true }
futures-channel = "0.3.1"
futures-core = "0.3.1"
futures-util = "0.3.1"

View File

@ -1,6 +1,6 @@
use byteorder::ByteOrder;
use memchr::memchr;
use std::{io, str};
use std::{io, slice, str};
pub trait Buf {
fn advance(&mut self, cnt: usize);
@ -98,3 +98,15 @@ impl<'a> Buf for &'a [u8] {
Ok(s)
}
}
pub trait ToBuf {
fn to_buf(&self) -> &[u8];
}
impl ToBuf for [u8] {
fn to_buf(&self) -> &[u8] { self }
}
impl ToBuf for u8 {
fn to_buf(&self) -> &[u8] { slice::from_ref(self) }
}

View File

@ -5,4 +5,4 @@ mod buf;
mod buf_mut;
mod byte_str;
pub use self::{buf::Buf, buf_mut::BufMut, buf_stream::BufStream, byte_str::ByteStr};
pub use self::{buf::{Buf, ToBuf}, buf_mut::BufMut, buf_stream::BufStream, byte_str::ByteStr};

View File

@ -3,18 +3,12 @@
#[macro_export]
macro_rules! __bytes_builder (
($($b: expr), *) => {{
use bytes::Buf;
use bytes::IntoBuf;
use bytes::BufMut;
use $crate::io::ToBuf;
let mut bytes = bytes::BytesMut::new();
let mut buf = Vec::new();
$(
{
let buf = $b.into_buf();
bytes.reserve(buf.remaining());
bytes.put(buf);
}
buf.extend_from_slice($b.to_buf());
)*
bytes.freeze()
buf
}}
);

View File

@ -52,7 +52,7 @@ mod tests {
use crate::__bytes_builder;
#[test]
fn it_decodes_com_stmt_prepare_ok() -> io::Result<()> {
fn it_decodes_com_stmt_prepare_ok() -> crate::Result<()> {
#[rustfmt::skip]
let buf = &__bytes_builder!(
// int<1> 0x00 COM_STMT_PREPARE_OK header

View File

@ -38,7 +38,7 @@ mod test {
use std::io;
#[test]
fn it_decodes_eof_packet() -> io::Result<()> {
fn it_decodes_eof_packet() -> crate::Result<()> {
#[rustfmt::skip]
let buf = __bytes_builder!(
// int<1> 0xfe : EOF header

View File

@ -69,7 +69,7 @@ mod test {
use crate::__bytes_builder;
#[test]
fn it_decodes_ok_packet() -> io::Result<()> {
fn it_decodes_ok_packet() -> crate::Result<()> {
#[rustfmt::skip]
let buf = __bytes_builder!(
// 0x00 : OK_Packet header or (0xFE if CLIENT_DEPRECATE_EOF is set)

View File

@ -10,7 +10,7 @@ impl HasSqlType<bool> for MariaDb {
fn metadata() -> MariaDbTypeMetadata {
MariaDbTypeMetadata {
// MYSQL_TYPE_TINY
field_type: FieldType(1),
field_type: FieldType::MYSQL_TYPE_TINY,
param_flag: ParameterFlag::empty(),
}
}

View File

@ -14,7 +14,7 @@ impl HasSqlType<str> for MariaDb {
fn metadata() -> MariaDbTypeMetadata {
MariaDbTypeMetadata {
// MYSQL_TYPE_VAR_STRING
field_type: FieldType(254),
field_type: FieldType::MYSQL_TYPE_VAR_STRING,
param_flag: ParameterFlag::empty(),
}
}

View File

@ -0,0 +1,177 @@
use crate::{HasSqlType, MariaDb, HasTypeMetadata, Encode, Decode};
use chrono::{NaiveDateTime, Datelike, Timelike, NaiveTime, NaiveDate};
use crate::mariadb::types::MariaDbTypeMetadata;
use crate::mariadb::protocol::{FieldType, ParameterFlag};
use crate::encode::IsNull;
use crate::io::Buf;
use std::convert::{TryFrom, TryInto};
use byteorder::{LittleEndian, ByteOrder};
use chrono::format::Item::Literal;
impl HasSqlType<NaiveDateTime> for MariaDb {
fn metadata() -> Self::TypeMetadata {
MariaDbTypeMetadata {
field_type: FieldType::MYSQL_TYPE_DATETIME,
param_flag: ParameterFlag::empty()
}
}
}
impl Encode<MariaDb> for NaiveDateTime {
fn encode(&self, buf: &mut Vec<u8>) -> IsNull {
// subtract the length byte
let length = Encode::<MariaDb>::size_hint(self) - 1;
buf.push(length as u8);
encode_date(self.date(), buf);
if length >= 7 {
buf.push(self.hour() as u8);
buf.push(self.minute() as u8);
buf.push(self.second() as u8);
}
if length == 11 {
buf.extend_from_slice(&self.timestamp_subsec_micros().to_le_bytes());
}
IsNull::No
}
fn size_hint(&self) -> usize {
match (self.hour(), self.minute(), self.second(), self.timestamp_subsec_micros()) {
// include the length byte
(0, 0, 0, 0) => 5,
(_, _, _, 0) => 8,
(_, _, _, _) => 12,
}
}
}
impl Decode<MariaDb> for NaiveDateTime {
fn decode(raw: Option<&[u8]>) -> Self {
let raw = raw.unwrap();
let len = raw[0];
assert_ne!(len, 0, "MySQL zero-dates are not supported");
let date = decode_date(&raw[1..]);
if len >= 7 {
date.and_hms_micro(
raw[5] as u32,
raw[6] as u32,
raw[7] as u32,
if len == 11 {
LittleEndian::read_u32(&raw[8..])
} else {
0
}
)
} else {
date.and_hms(0, 0, 0)
}
}
}
impl HasSqlType<NaiveDate> for MariaDb {
fn metadata() -> Self::TypeMetadata {
MariaDbTypeMetadata {
field_type: FieldType::MYSQL_TYPE_DATE,
param_flag: ParameterFlag::empty()
}
}
}
impl Encode<MariaDb> for NaiveDate {
fn encode(&self, buf: &mut Vec<u8>) -> IsNull {
buf.push(4);
encode_date(*self, buf);
IsNull::No
}
fn size_hint(&self) -> usize {
5
}
}
impl Decode<MariaDb> for NaiveDate {
fn decode(raw: Option<&[u8]>) -> Self {
let raw = raw.unwrap();
assert_eq!(raw[0], 4, "expected only 4 bytes");
decode_date(&raw[1..])
}
}
fn encode_date(date: NaiveDate, buf: &mut Vec<u8>) {
// MySQL supports years from 1000 - 9999
let year = u16::try_from(date.year())
.unwrap_or_else(|_| panic!("NaiveDateTime out of range for MariaDB: {}", date));
buf.extend_from_slice(&year.to_le_bytes());
buf.push(date.month() as u8);
buf.push(date.day() as u8);
}
fn decode_date(raw: &[u8]) -> NaiveDate {
NaiveDate::from_ymd(
LittleEndian::read_u16(raw) as i32,
raw[2] as u32,
raw[3] as u32
)
}
#[test]
fn test_encode_date_time() {
let mut buf = Vec::new();
// test values from https://dev.mysql.com/doc/internals/en/binary-protocol-value.html
let date1: NaiveDateTime = "2010-10-17T19:27:30.000001".parse().unwrap();
Encode::<MariaDb>::encode(&date1, &mut buf);
assert_eq!(*buf, [11, 218, 7, 10, 17, 19, 27, 30, 1, 0, 0, 0]);
buf.clear();
let date2: NaiveDateTime = "2010-10-17T19:27:30".parse().unwrap();
Encode::<MariaDb>::encode(&date2, &mut buf);
assert_eq!(*buf, [7, 218, 7, 10, 17, 19, 27, 30]);
buf.clear();
let date3: NaiveDateTime = "2010-10-17T00:00:00".parse().unwrap();
Encode::<MariaDb>::encode(&date3, &mut buf);
assert_eq!(*buf, [4, 218, 7, 10, 17]);
}
#[test]
fn test_decode_date_time() {
// test values from https://dev.mysql.com/doc/internals/en/binary-protocol-value.html
let buf = [11, 218, 7, 10, 17, 19, 27, 30, 1, 0, 0, 0];
let date1 = <NaiveDateTime as Decode<MariaDb>>::decode(Some(&buf));
assert_eq!(date1.to_string(), "2010-10-17 19:27:30.000001");
let buf = [7, 218, 7, 10, 17, 19, 27, 30];
let date2 = <NaiveDateTime as Decode<MariaDb>>::decode(Some(&buf));
assert_eq!(date2.to_string(), "2010-10-17 19:27:30");
let buf = [4, 218, 7, 10, 17];
let date3 = <NaiveDateTime as Decode<MariaDb>>::decode(Some(&buf));
assert_eq!(date3.to_string(), "2010-10-17 00:00:00");
}
#[test]
fn test_encode_date() {
let mut buf = Vec::new();
let date: NaiveDate = "2010-10-17".parse().unwrap();
Encode::<MariaDb>::encode(&date, &mut buf);
assert_eq!(*buf, [4, 218, 7, 10, 17]);
}
#[test]
fn test_decode_date() {
let buf = [4, 218, 7, 10, 17];
let date = <NaiveDate as Decode<MariaDb>>::decode(Some(&buf));
assert_eq!(date.to_string(), "2010-10-17");
}

View File

@ -9,6 +9,9 @@ pub mod boolean;
pub mod character;
pub mod numeric;
#[cfg(feature = "chrono")]
pub mod chrono;
#[derive(Debug)]
pub struct MariaDbTypeMetadata {
pub field_type: FieldType,

View File

@ -11,7 +11,7 @@ impl HasSqlType<i8> for MariaDb {
#[inline]
fn metadata() -> MariaDbTypeMetadata {
MariaDbTypeMetadata {
field_type: FieldType(1),
field_type: FieldType::MYSQL_TYPE_TINY,
param_flag: ParameterFlag::empty(),
}
}

View File

@ -65,6 +65,7 @@ impl Debug for DataRow {
#[cfg(test)]
mod tests {
use super::{DataRow, Decode};
use crate::Row;
const DATA_ROW: &[u8] = b"\0\x03\0\0\0\x011\0\0\0\x012\0\0\0\x013";
@ -74,9 +75,9 @@ mod tests {
assert_eq!(m.len(), 3);
assert_eq!(m.get(0), Some(&b"1"[..]));
assert_eq!(m.get(1), Some(&b"2"[..]));
assert_eq!(m.get(2), Some(&b"3"[..]));
assert_eq!(m.get_raw(0), Some(&b"1"[..]));
assert_eq!(m.get_raw(1), Some(&b"2"[..]));
assert_eq!(m.get_raw(2), Some(&b"3"[..]));
assert_eq!(
format!("{:?}", m),

View File

@ -0,0 +1,208 @@
use crate::{Decode, Postgres, Encode, HasSqlType, HasTypeMetadata};
use chrono::{NaiveTime, Timelike, NaiveDate, TimeZone, DateTime, NaiveDateTime, Utc, Local, Duration, Date};
use crate::postgres::types::{PostgresTypeMetadata, PostgresTypeFormat};
use crate::encode::IsNull;
use std::convert::TryInto;
use std::mem::size_of;
postgres_metadata!(
// time
NaiveTime: PostgresTypeMetadata {
format: PostgresTypeFormat::Binary,
oid: 1083,
array_oid: 1183
},
// date
NaiveDate: PostgresTypeMetadata {
format: PostgresTypeFormat::Binary,
oid: 1082,
array_oid: 1182
},
// timestamp
NaiveDateTime: PostgresTypeMetadata {
format: PostgresTypeFormat::Binary,
oid: 1114,
array_oid: 1115
},
// timestamptz
{ Tz: TimeZone } DateTime<Tz>: PostgresTypeMetadata {
format: PostgresTypeFormat::Binary,
oid: 1184,
array_oid: 1185
},
// Date<Tz: TimeZone> is not covered as Postgres does not have a "date with timezone" type
);
fn decode<T: Decode<Postgres>>(raw: Option<&[u8]>) -> T {
Decode::<Postgres>::decode(raw)
}
impl Decode<Postgres> for NaiveTime {
fn decode(raw: Option<&[u8]>) -> Self {
let micros: i64 = decode(raw);
NaiveTime::from_hms(0, 0, 0) + Duration::microseconds(micros)
}
}
impl Encode<Postgres> for NaiveTime {
fn encode(&self, buf: &mut Vec<u8>) -> IsNull {
let micros = (*self - NaiveTime::from_hms(0, 0, 0))
.num_microseconds()
.expect("shouldn't overflow");
Encode::<Postgres>::encode(&micros, buf)
}
fn size_hint(&self) -> usize {
size_of::<i64>()
}
}
impl Decode<Postgres> for NaiveDate {
fn decode(raw: Option<&[u8]>) -> Self {
let days: i32 = decode(raw);
NaiveDate::from_ymd(2000, 1, 1) + Duration::days(days as i64)
}
}
impl Encode<Postgres> for NaiveDate {
fn encode(&self, buf: &mut Vec<u8>) -> IsNull {
let days: i32 = self.signed_duration_since(NaiveDate::from_ymd(2000, 1, 1))
.num_days()
.try_into()
.unwrap_or_else(|_| panic!("NaiveDate out of range for Postgres: {:?}", self));
Encode::<Postgres>::encode(&days, buf)
}
fn size_hint(&self) -> usize {
size_of::<i32>()
}
}
impl Decode<Postgres> for NaiveDateTime {
fn decode(raw: Option<&[u8]>) -> Self {
let micros: i64 = decode(raw);
postgres_epoch().naive_utc()
.checked_add_signed(Duration::microseconds(micros))
.unwrap_or_else(|| panic!("Postgres timestamp out of range for NaiveDateTime: {:?}", micros))
}
}
impl Encode<Postgres> for NaiveDateTime {
fn encode(&self, buf: &mut Vec<u8>) -> IsNull {
let micros = self.signed_duration_since(postgres_epoch().naive_utc())
.num_microseconds()
.unwrap_or_else(|| panic!("NaiveDateTime out of range for Postgres: {:?}", self));
Encode::<Postgres>::encode(&micros, buf)
}
fn size_hint(&self) -> usize {
size_of::<i64>()
}
}
impl Decode<Postgres> for DateTime<Utc> {
fn decode(raw: Option<&[u8]>) -> Self {
let date_time = <NaiveDateTime as Decode<Postgres>>::decode(raw);
DateTime::from_utc(date_time, Utc)
}
}
impl Decode<Postgres> for DateTime<Local> {
fn decode(raw: Option<&[u8]>) -> Self {
let date_time = <NaiveDateTime as Decode<Postgres>>::decode(raw);
Local.from_utc_datetime(&date_time)
}
}
impl<Tz: TimeZone> Encode<Postgres> for DateTime<Tz> where Tz::Offset: Copy {
fn encode(&self, buf: &mut Vec<u8>) -> IsNull {
Encode::<Postgres>::encode(&self.naive_utc(), buf)
}
fn size_hint(&self) -> usize {
size_of::<i64>()
}
}
fn postgres_epoch() -> DateTime<Utc> {
Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)
}
#[test]
fn test_encode_datetime() {
let mut buf = Vec::new();
let date = postgres_epoch();
Encode::<Postgres>::encode(&date, &mut buf);
assert_eq!(buf, [0; 8]);
buf.clear();
// one hour past epoch
let date2 = postgres_epoch() + Duration::hours(1);
Encode::<Postgres>::encode(&date2, &mut buf);
assert_eq!(buf, 3_600_000_000i64.to_be_bytes());
buf.clear();
// some random date
let date3: NaiveDateTime = "2019-12-11T11:01:05".parse().unwrap();
let expected = dbg!((date3 - postgres_epoch().naive_utc()).num_microseconds().unwrap());
Encode::<Postgres>::encode(&date3, &mut buf);
assert_eq!(buf, expected.to_be_bytes());
buf.clear();
}
#[test]
fn test_decode_datetime() {
let buf = [0u8; 8];
let date: NaiveDateTime = Decode::<Postgres>::decode(Some(&buf));
assert_eq!(date.to_string(), "2000-01-01 00:00:00");
let buf = 3_600_000_000i64.to_be_bytes();
let date: NaiveDateTime = Decode::<Postgres>::decode(Some(&buf));
assert_eq!(date.to_string(), "2000-01-01 01:00:00");
let buf = 629_377_265_000_000i64.to_be_bytes();
let date: NaiveDateTime = Decode::<Postgres>::decode(Some(&buf));
assert_eq!(date.to_string(), "2019-12-11 11:01:05");
}
#[test]
fn test_encode_date() {
let mut buf = Vec::new();
let date = NaiveDate::from_ymd(2000, 1, 1);
Encode::<Postgres>::encode(&date, &mut buf);
assert_eq!(buf, [0; 4]);
buf.clear();
let date2 = NaiveDate::from_ymd(2001, 1, 1);
Encode::<Postgres>::encode(&date2, &mut buf);
// 2000 was a leap year
assert_eq!(buf, 366i32.to_be_bytes());
buf.clear();
let date3 = NaiveDate::from_ymd(2019, 12, 11);
Encode::<Postgres>::encode(&date3, &mut buf);
assert_eq!(buf, 7284i32.to_be_bytes());
buf.clear();
}
#[test]
fn test_decode_date() {
let buf = [0; 4];
let date: NaiveDate = Decode::<Postgres>::decode(Some(&buf));
assert_eq!(date.to_string(), "2000-01-01");
let buf = 366i32.to_be_bytes();
let date: NaiveDate = Decode::<Postgres>::decode(Some(&buf));
assert_eq!(date.to_string(), "2001-01-01");
let buf = 7284i32.to_be_bytes();
let date: NaiveDate = Decode::<Postgres>::decode(Some(&buf));
assert_eq!(date.to_string(), "2019-12-11");
}

View File

@ -30,6 +30,18 @@
use super::Postgres;
use crate::types::{HasTypeMetadata, TypeMetadata};
macro_rules! postgres_metadata {
($($({ $($typarams:tt)* })? $type:path: $meta:expr),*$(,)?) => {
$(
impl$(<$($typarams)*>)? crate::types::HasSqlType<$type> for Postgres {
fn metadata() -> PostgresTypeMetadata {
$meta
}
}
)*
};
}
mod binary;
mod boolean;
mod character;
@ -38,6 +50,9 @@ mod numeric;
#[cfg(feature = "uuid")]
mod uuid;
#[cfg(feature = "chrono")]
mod chrono;
pub enum PostgresTypeFormat {
Text = 0,
Binary = 1,

View File

@ -19,6 +19,7 @@ quote = "1.0.2"
url = "2.1.0"
[features]
chrono = ["sqlx/chrono"]
mariadb = ["sqlx/mariadb"]
postgres = ["sqlx/postgres"]
uuid = ["sqlx/uuid"]