feat(sqlite): clean up chrono support, add support for NaiveTime and NaiveDate

This commit is contained in:
Ryan Leckey 2020-07-04 05:51:46 -07:00
parent 7d22c0917f
commit 3b503a7764
4 changed files with 158 additions and 53 deletions

View file

@ -1,6 +1,6 @@
use std::cmp::Ordering;
use std::ffi::CString;
use std::os::raw::{c_char, c_int, c_void};
use std::os::raw::{c_int, c_void};
use std::slice;
use std::str::from_utf8_unchecked;

View file

@ -23,6 +23,8 @@ pub(crate) enum DataType {
// non-standard extensions
Bool,
Int64,
Date,
Time,
Datetime,
}
@ -49,6 +51,8 @@ impl TypeInfo for SqliteTypeInfo {
// non-standard extensions
DataType::Bool => "BOOLEAN",
DataType::Date => "DATE",
DataType::Time => "TIME",
DataType::Datetime => "DATETIME",
}
}
@ -81,6 +85,9 @@ impl FromStr for DataType {
"int4" => DataType::Int,
"int8" => DataType::Int64,
"boolean" | "bool" => DataType::Bool,
"date" => DataType::Date,
"time" => DataType::Time,
"datetime" | "timestamp" => DataType::Datetime,
_ if s.contains("int") => DataType::Int64,
@ -126,6 +133,8 @@ fn test_data_type_from_str() -> Result<(), BoxDynError> {
assert_eq!(DataType::Bool, "BOOL".parse()?);
assert_eq!(DataType::Datetime, "DATETIME".parse()?);
assert_eq!(DataType::Time, "TIME".parse()?);
assert_eq!(DataType::Date, "DATE".parse()?);
Ok(())
}

View file

@ -1,3 +1,5 @@
use crate::types::chrono::FixedOffset;
use crate::value::ValueRef;
use crate::{
decode::Decode,
encode::{Encode, IsNull},
@ -5,30 +7,120 @@ use crate::{
sqlite::{type_info::DataType, Sqlite, SqliteArgumentValue, SqliteTypeInfo, SqliteValueRef},
types::Type,
};
use chrono::prelude::*;
use bitflags::_core::fmt::Display;
use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Utc};
impl<Tz: TimeZone> Type<Sqlite> for DateTime<Tz> {
fn type_info() -> SqliteTypeInfo {
SqliteTypeInfo(DataType::Datetime)
}
fn compatible(ty: &SqliteTypeInfo) -> bool {
<NaiveDateTime as Type<Sqlite>>::compatible(ty)
}
}
impl Type<Sqlite> for NaiveDateTime {
fn type_info() -> SqliteTypeInfo {
SqliteTypeInfo(DataType::Datetime)
}
fn compatible(ty: &SqliteTypeInfo) -> bool {
matches!(
ty.0,
DataType::Datetime | DataType::Text | DataType::Int64 | DataType::Int | DataType::Float
)
}
}
impl Type<Sqlite> for NaiveDate {
fn type_info() -> SqliteTypeInfo {
SqliteTypeInfo(DataType::Date)
}
fn compatible(ty: &SqliteTypeInfo) -> bool {
matches!(ty.0, DataType::Date | DataType::Text)
}
}
impl Type<Sqlite> for NaiveTime {
fn type_info() -> SqliteTypeInfo {
SqliteTypeInfo(DataType::Time)
}
fn compatible(ty: &SqliteTypeInfo) -> bool {
matches!(ty.0, DataType::Time | DataType::Text)
}
}
impl<Tz: TimeZone> Encode<'_, Sqlite> for DateTime<Tz>
where
Tz::Offset: Display,
{
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> IsNull {
Encode::<Sqlite>::encode(self.naive_utc().format("%F %T%.f").to_string(), buf)
}
}
impl Encode<'_, Sqlite> for NaiveDateTime {
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> IsNull {
let text: String = self.format("%F %T%.f").to_string();
Encode::<Sqlite>::encode(text, buf)
Encode::<Sqlite>::encode(self.format("%F %T%.f").to_string(), buf)
}
}
impl<'a> Decode<'a, Sqlite> for NaiveDateTime {
fn decode(value: SqliteValueRef<'a>) -> Result<Self, BoxDynError> {
decode_naive_from_text(value.text()?)
impl Encode<'_, Sqlite> for NaiveDate {
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> IsNull {
Encode::<Sqlite>::encode(self.format("%F").to_string(), buf)
}
}
fn decode_naive_from_text(text: &str) -> Result<NaiveDateTime, BoxDynError> {
impl Encode<'_, Sqlite> for NaiveTime {
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> IsNull {
Encode::<Sqlite>::encode(self.format("%T%.f%").to_string(), buf)
}
}
impl<'r> Decode<'r, Sqlite> for DateTime<Utc> {
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
Ok(Utc.from_utc_datetime(&decode_datetime(value)?.naive_utc()))
}
}
impl<'r> Decode<'r, Sqlite> for DateTime<Local> {
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
Ok(Local.from_utc_datetime(&decode_datetime(value)?.naive_utc()))
}
}
impl<'r> Decode<'r, Sqlite> for DateTime<FixedOffset> {
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
decode_datetime(value)
}
}
fn decode_datetime(value: SqliteValueRef<'_>) -> Result<DateTime<FixedOffset>, BoxDynError> {
let dt = match value.type_info().map(|ty| ty.0) {
None | Some(DataType::Text) => decode_datetime_from_text(value.text()?),
Some(DataType::Int) | Some(DataType::Int64) => decode_datetime_from_int(value.int64()),
Some(DataType::Float) => decode_datetime_from_float(value.double()),
_ => None,
};
if let Some(dt) = dt {
Ok(dt)
} else {
Err(format!("invalid datetime: {}", value.text()?).into())
}
}
fn decode_datetime_from_text(value: &str) -> Option<DateTime<FixedOffset>> {
println!("decode_datetime_from_text --> {}", value);
// Loop over common date time patterns, inspired by Diesel
// https://docs.diesel.rs/src/diesel/sqlite/types/date_and_time/chrono.rs.html#56-97
// https://github.com/diesel-rs/diesel/blob/93ab183bcb06c69c0aee4a7557b6798fd52dd0d8/diesel/src/sqlite/types/date_and_time/chrono.rs#L56-L97
let sqlite_datetime_formats = &[
// Most likely format
"%F %T%.f",
@ -47,52 +139,62 @@ fn decode_naive_from_text(text: &str) -> Result<NaiveDateTime, BoxDynError> {
];
for format in sqlite_datetime_formats {
if let Ok(dt) = NaiveDateTime::parse_from_str(text, format) {
return Ok(dt);
if let Ok(dt) = DateTime::parse_from_str(value, format) {
return Some(dt);
}
if let Ok(dt) = NaiveDateTime::parse_from_str(value, format) {
return Some(Utc.fix().from_utc_datetime(&dt));
}
}
return Err(err_protocol!("Did not find a matching pattern").into());
None
}
impl<Tz: TimeZone> Type<Sqlite> for DateTime<Tz> {
fn type_info() -> SqliteTypeInfo {
SqliteTypeInfo(DataType::Datetime)
fn decode_datetime_from_int(value: i64) -> Option<DateTime<FixedOffset>> {
NaiveDateTime::from_timestamp_opt(value, 0).map(|dt| Utc.fix().from_utc_datetime(&dt))
}
fn decode_datetime_from_float(value: f64) -> Option<DateTime<FixedOffset>> {
let epoch_in_julian_days = 2_440_587.5;
let seconds_in_day = 86400.0;
let timestamp = (value - epoch_in_julian_days) * seconds_in_day;
let seconds = timestamp as i64;
let nanos = (timestamp.fract() * 1E9) as u32;
NaiveDateTime::from_timestamp_opt(seconds, nanos).map(|dt| Utc.fix().from_utc_datetime(&dt))
}
impl<'r> Decode<'r, Sqlite> for NaiveDateTime {
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
Ok(decode_datetime(value)?.naive_local())
}
}
impl<Tz: TimeZone> Encode<'_, Sqlite> for DateTime<Tz>
where
<Tz as TimeZone>::Offset: std::fmt::Display,
{
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> IsNull {
let text = self.to_rfc3339();
Encode::<Sqlite>::encode(text, buf)
impl<'r> Decode<'r, Sqlite> for NaiveDate {
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
Ok(NaiveDate::parse_from_str("%F", value.text()?)?)
}
}
impl<'a> Decode<'a, Sqlite> for DateTime<Utc> {
fn decode(value: SqliteValueRef<'a>) -> Result<Self, BoxDynError> {
let text = value.text()?;
if let Ok(dt) = DateTime::parse_from_rfc3339(text) {
Ok(dt.with_timezone(&Utc))
} else {
let dt = decode_naive_from_text(text)?;
Ok(Utc.from_utc_datetime(&dt))
impl<'r> Decode<'r, Sqlite> for NaiveTime {
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
let value = value.text()?;
// Loop over common time patterns, inspired by Diesel
// https://github.com/diesel-rs/diesel/blob/93ab183bcb06c69c0aee4a7557b6798fd52dd0d8/diesel/src/sqlite/types/date_and_time/chrono.rs#L29-L47
let sqlite_time_formats = &[
// Most likely format
"%T.f", // Other formats in order of appearance in docs
"%R", "%RZ", "%T%.fZ", "%R%:z", "%T%.f%:z",
];
for format in sqlite_time_formats {
if let Ok(dt) = NaiveTime::parse_from_str(value, format) {
return Ok(dt);
}
}
}
}
impl<'a> Decode<'a, Sqlite> for DateTime<FixedOffset> {
fn decode(value: SqliteValueRef<'a>) -> Result<Self, BoxDynError> {
let text = value.text()?;
Ok(DateTime::parse_from_rfc3339(text)?)
}
}
impl<'a> Decode<'a, Sqlite> for DateTime<Local> {
fn decode(value: SqliteValueRef<'a>) -> Result<Self, BoxDynError> {
let as_utc: DateTime<Utc> = Decode::<Sqlite>::decode(value)?;
Ok(as_utc.with_timezone(&Local))
Err(format!("invalid time: {}", value).into())
}
}

View file

@ -35,23 +35,17 @@ test_type!(bytes<Vec<u8>>(Sqlite,
#[cfg(feature = "chrono")]
mod chrono {
use super::*;
use sqlx::types::chrono::{
DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeZone, Utc,
};
use sqlx::types::chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, TimeZone, Utc};
test_type!(chrono_naive_date_time<NaiveDateTime>(Sqlite,
"'2019-01-02 05:10:20'" == NaiveDate::from_ymd(2019, 1, 2).and_hms(5, 10, 20)
"datetime('2019-01-02 05:10:20')" == NaiveDate::from_ymd(2019, 1, 2).and_hms(5, 10, 20)
));
test_type!(chrono_date_time_utc<DateTime::<Utc>>(Sqlite,
"'1996-12-20T00:39:57+00:00'" == Utc.ymd(1996, 12, 20).and_hms(0, 39, 57)
"datetime('1996-12-20T00:39:57+00:00')" == Utc.ymd(1996, 12, 20).and_hms(0, 39, 57)
));
test_type!(chrono_date_time_fixed_offset<DateTime::<FixedOffset>>(Sqlite,
"'2016-11-08T03:50:23-05:00'" == FixedOffset::west(5 * 3600).ymd(2016, 11, 08).and_hms(3, 50, 23)
));
test_type!(chrono_date_time_local<DateTime::<Local>>(Sqlite,
"'2016-11-08T03:50:23+00:00'" == Local.ymd(2016, 11, 08).and_hms(3, 50, 23)
"datetime('2016-11-08T03:50:23-05:00')" == FixedOffset::west(5 * 3600).ymd(2016, 11, 08).and_hms(3, 50, 23)
));
}