Add Sqlite support for the time crate (#1865)

* feat(sqlite): Add 'time' crate support for date/time types
docs(sqlite): Update types module docs for JSON and Chrono
docs(mysql): Update types module docs for JSON

* More efficient time crate decoding with FormatItem::First and hand-crafting of format descriptions

* Replace temporary testing code with original intention

* Replace duplicated formatting test with intended test

* Performance improvements to decoding OffsetDateTime, PrimitiveDateTime, and Time

* Use correct iteration for OffsetDateTime

* Reduce visibility of format constants

Co-authored-by: John B Codes <johnbcodes@users.noreply.github.com>
This commit is contained in:
John B Codes 2022-07-08 19:51:50 -04:00 committed by GitHub
parent b3bbdab705
commit cfef70a796
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 389 additions and 3 deletions

View File

@ -64,13 +64,15 @@
//! | `uuid::Uuid` | BYTE(16), VARCHAR, CHAR, TEXT |
//! | `uuid::fmt::Hyphenated` | CHAR(36) |
//!
//! ### [`json`](https://crates.io/crates/json)
//! ### [`json`](https://crates.io/crates/serde_json)
//!
//! Requires the `json` Cargo feature flag.
//!
//! | Rust type | MySQL type(s) |
//! |---------------------------------------|------------------------------------------------------|
//! | `json::JsonValue` | JSON
//! | [`Json<T>`] | JSON |
//! | `serde_json::JsonValue` | JSON |
//! | `&serde_json::value::RawValue` | JSON |
//!
//! # Nullable
//!

View File

@ -22,11 +22,24 @@
//!
//! Requires the `chrono` Cargo feature flag.
//!
//! | Rust type | Sqlite type(s) |
//! | Rust type | Sqlite type(s) |
//! |---------------------------------------|------------------------------------------------------|
//! | `chrono::NaiveDateTime` | DATETIME |
//! | `chrono::DateTime<Utc>` | DATETIME |
//! | `chrono::DateTime<Local>` | DATETIME |
//! | `chrono::NaiveDate` | DATE |
//! | `chrono::NaiveTime` | TIME |
//!
//! ### [`time`](https://crates.io/crates/time)
//!
//! Requires the `time` Cargo feature flag.
//!
//! | Rust type | Sqlite type(s) |
//! |---------------------------------------|------------------------------------------------------|
//! | `time::PrimitiveDateTime` | DATETIME |
//! | `time::OffsetDateTime` | DATETIME |
//! | `time::Date` | DATE |
//! | `time::Time` | TIME |
//!
//! ### [`uuid`](https://crates.io/crates/uuid)
//!
@ -37,6 +50,16 @@
//! | `uuid::Uuid` | BLOB, TEXT |
//! | `uuid::fmt::Hyphenated` | TEXT |
//!
//! ### [`json`](https://crates.io/crates/serde_json)
//!
//! Requires the `json` Cargo feature flag.
//!
//! | Rust type | Sqlite type(s) |
//! |---------------------------------------|------------------------------------------------------|
//! | [`Json<T>`] | TEXT |
//! | `serde_json::JsonValue` | TEXT |
//! | `&serde_json::value::RawValue` | TEXT |
//!
//! # Nullable
//!
//! In addition, `Option<T>` is supported where `T` implements `Type`. An `Option<T>` represents
@ -52,6 +75,8 @@ mod int;
#[cfg(feature = "json")]
mod json;
mod str;
#[cfg(feature = "time")]
mod time;
mod uint;
#[cfg(feature = "uuid")]
mod uuid;

View File

@ -0,0 +1,309 @@
use crate::value::ValueRef;
use crate::{
decode::Decode,
encode::{Encode, IsNull},
error::BoxDynError,
sqlite::{type_info::DataType, Sqlite, SqliteArgumentValue, SqliteTypeInfo, SqliteValueRef},
types::Type,
};
use time::format_description::{well_known::Rfc3339, FormatItem};
use time::macros::format_description as fd;
use time::{Date, OffsetDateTime, PrimitiveDateTime, Time};
impl Type<Sqlite> for OffsetDateTime {
fn type_info() -> SqliteTypeInfo {
SqliteTypeInfo(DataType::Datetime)
}
fn compatible(ty: &SqliteTypeInfo) -> bool {
<PrimitiveDateTime as Type<Sqlite>>::compatible(ty)
}
}
impl Type<Sqlite> for PrimitiveDateTime {
fn type_info() -> SqliteTypeInfo {
SqliteTypeInfo(DataType::Datetime)
}
fn compatible(ty: &SqliteTypeInfo) -> bool {
matches!(
ty.0,
DataType::Datetime | DataType::Text | DataType::Int64 | DataType::Int
)
}
}
impl Type<Sqlite> for Date {
fn type_info() -> SqliteTypeInfo {
SqliteTypeInfo(DataType::Date)
}
fn compatible(ty: &SqliteTypeInfo) -> bool {
matches!(ty.0, DataType::Date | DataType::Text)
}
}
impl Type<Sqlite> for Time {
fn type_info() -> SqliteTypeInfo {
SqliteTypeInfo(DataType::Time)
}
fn compatible(ty: &SqliteTypeInfo) -> bool {
matches!(ty.0, DataType::Time | DataType::Text)
}
}
impl Encode<'_, Sqlite> for OffsetDateTime {
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> IsNull {
Encode::<Sqlite>::encode(self.format(&Rfc3339).unwrap(), buf)
}
}
impl Encode<'_, Sqlite> for PrimitiveDateTime {
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> IsNull {
let format = fd!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]");
Encode::<Sqlite>::encode(self.format(&format).unwrap(), buf)
}
}
impl Encode<'_, Sqlite> for Date {
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> IsNull {
let format = fd!("[year]-[month]-[day]");
Encode::<Sqlite>::encode(self.format(&format).unwrap(), buf)
}
}
impl Encode<'_, Sqlite> for Time {
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> IsNull {
let format = fd!("[hour]:[minute]:[second].[subsecond]");
Encode::<Sqlite>::encode(self.format(&format).unwrap(), buf)
}
}
impl<'r> Decode<'r, Sqlite> for OffsetDateTime {
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
decode_offset_datetime(value)
}
}
impl<'r> Decode<'r, Sqlite> for PrimitiveDateTime {
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
decode_datetime(value)
}
}
impl<'r> Decode<'r, Sqlite> for Date {
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
Ok(Date::parse(value.text()?, &fd!("[year]-[month]-[day]"))?)
}
}
impl<'r> Decode<'r, Sqlite> for Time {
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
let value = value.text()?;
let sqlite_time_formats = &[
fd!("[hour]:[minute]:[second].[subsecond]"),
fd!("[hour]:[minute]:[second]"),
fd!("[hour]:[minute]"),
];
for format in sqlite_time_formats {
if let Ok(dt) = Time::parse(value, &format) {
return Ok(dt);
}
}
Err(format!("invalid time: {}", value).into())
}
}
fn decode_offset_datetime(value: SqliteValueRef<'_>) -> Result<OffsetDateTime, BoxDynError> {
let dt = match value.type_info().0 {
DataType::Text => decode_offset_datetime_from_text(value.text()?),
DataType::Int | DataType::Int64 => {
Some(OffsetDateTime::from_unix_timestamp(value.int64())?)
}
_ => None,
};
if let Some(dt) = dt {
Ok(dt)
} else {
Err(format!("invalid offset datetime: {}", value.text()?).into())
}
}
fn decode_offset_datetime_from_text(value: &str) -> Option<OffsetDateTime> {
if let Ok(dt) = OffsetDateTime::parse(value, &Rfc3339) {
return Some(dt);
}
if let Ok(dt) = OffsetDateTime::parse(value, formats::OFFSET_DATE_TIME) {
return Some(dt);
}
None
}
fn decode_datetime(value: SqliteValueRef<'_>) -> Result<PrimitiveDateTime, BoxDynError> {
let dt = match value.type_info().0 {
DataType::Text => decode_datetime_from_text(value.text()?),
DataType::Int | DataType::Int64 => {
let parsed = OffsetDateTime::from_unix_timestamp(value.int64()).unwrap();
Some(PrimitiveDateTime::new(parsed.date(), parsed.time()))
}
_ => None,
};
if let Some(dt) = dt {
Ok(dt)
} else {
Err(format!("invalid datetime: {}", value.text()?).into())
}
}
fn decode_datetime_from_text(value: &str) -> Option<PrimitiveDateTime> {
let default_format = fd!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]");
if let Ok(dt) = PrimitiveDateTime::parse(value, &default_format) {
return Some(dt);
}
let formats = [
FormatItem::Compound(formats::PRIMITIVE_DATE_TIME_SPACE_SEPARATED),
FormatItem::Compound(formats::PRIMITIVE_DATE_TIME_T_SEPARATED),
];
if let Ok(dt) = PrimitiveDateTime::parse(value, &FormatItem::First(&formats)) {
return Some(dt);
}
None
}
mod formats {
use time::format_description::{modifier, Component::*, FormatItem, FormatItem::*};
const YEAR: FormatItem<'_> = Component(Year({
let mut value = modifier::Year::default();
value.padding = modifier::Padding::Zero;
value.repr = modifier::YearRepr::Full;
value.iso_week_based = false;
value.sign_is_mandatory = false;
value
}));
const MONTH: FormatItem<'_> = Component(Month({
let mut value = modifier::Month::default();
value.padding = modifier::Padding::Zero;
value.repr = modifier::MonthRepr::Numerical;
value.case_sensitive = true;
value
}));
const DAY: FormatItem<'_> = Component(Day({
let mut value = modifier::Day::default();
value.padding = modifier::Padding::Zero;
value
}));
const HOUR: FormatItem<'_> = Component(Hour({
let mut value = modifier::Hour::default();
value.padding = modifier::Padding::Zero;
value.is_12_hour_clock = false;
value
}));
const MINUTE: FormatItem<'_> = Component(Minute({
let mut value = modifier::Minute::default();
value.padding = modifier::Padding::Zero;
value
}));
const SECOND: FormatItem<'_> = Component(Second({
let mut value = modifier::Second::default();
value.padding = modifier::Padding::Zero;
value
}));
const SUBSECOND: FormatItem<'_> = Component(Subsecond({
let mut value = modifier::Subsecond::default();
value.digits = modifier::SubsecondDigits::OneOrMore;
value
}));
const OFFSET_HOUR: FormatItem<'_> = Component(OffsetHour({
let mut value = modifier::OffsetHour::default();
value.sign_is_mandatory = true;
value.padding = modifier::Padding::Zero;
value
}));
const OFFSET_MINUTE: FormatItem<'_> = Component(OffsetMinute({
let mut value = modifier::OffsetMinute::default();
value.padding = modifier::Padding::Zero;
value
}));
pub(super) const OFFSET_DATE_TIME: &[FormatItem<'_>] = {
&[
YEAR,
Literal(b"-"),
MONTH,
Literal(b"-"),
DAY,
Optional(&Literal(b" ")),
Optional(&Literal(b"T")),
HOUR,
Literal(b":"),
MINUTE,
Optional(&Literal(b":")),
Optional(&SECOND),
Optional(&Literal(b".")),
Optional(&SUBSECOND),
Optional(&OFFSET_HOUR),
Optional(&Literal(b":")),
Optional(&OFFSET_MINUTE),
]
};
pub(super) const PRIMITIVE_DATE_TIME_SPACE_SEPARATED: &[FormatItem<'_>] = {
&[
YEAR,
Literal(b"-"),
MONTH,
Literal(b"-"),
DAY,
Literal(b" "),
HOUR,
Literal(b":"),
MINUTE,
Optional(&Literal(b":")),
Optional(&SECOND),
Optional(&Literal(b".")),
Optional(&SUBSECOND),
Optional(&Literal(b"Z")),
]
};
pub(super) const PRIMITIVE_DATE_TIME_T_SEPARATED: &[FormatItem<'_>] = {
&[
YEAR,
Literal(b"-"),
MONTH,
Literal(b"-"),
DAY,
Literal(b"T"),
HOUR,
Literal(b":"),
MINUTE,
Optional(&Literal(b":")),
Optional(&SECOND),
Optional(&Literal(b".")),
Optional(&SUBSECOND),
Optional(&Literal(b"Z")),
]
};
}

View File

@ -1,3 +1,5 @@
extern crate time_ as time;
use sqlx::sqlite::{Sqlite, SqliteRow};
use sqlx_core::row::Row;
use sqlx_test::new;
@ -107,6 +109,54 @@ mod chrono {
));
}
#[cfg(feature = "time")]
mod time_tests {
use super::*;
use sqlx::types::time::{Date, OffsetDateTime, PrimitiveDateTime, Time};
use time::macros::{date, datetime, time};
test_type!(time_offset_date_time<OffsetDateTime>(
Sqlite,
"SELECT datetime({0}) is datetime(?), {0}, ?",
"'2015-11-19 01:01:39+01:00'" == datetime!(2015 - 11 - 19 1:01:39 +1),
"'2014-10-18 00:00:38.697+00:00'" == datetime!(2014 - 10 - 18 00:00:38.697 +0),
"'2013-09-17 23:59-01:00'" == datetime!(2013 - 9 - 17 23:59 -1),
"'2016-03-07T22:36:55.135+03:30'" == datetime!(2016 - 3 - 7 22:36:55.135 +3:30),
"'2017-04-11T14:35+02:00'" == datetime!(2017 - 4 - 11 14:35 +2),
));
test_type!(time_primitive_date_time<PrimitiveDateTime>(
Sqlite,
"SELECT datetime({0}) is datetime(?), {0}, ?",
"'2019-01-02 05:10:20'" == datetime!(2019 - 1 - 2 5:10:20),
"'2018-12-01 04:09:19.543'" == datetime!(2018 - 12 - 1 4:09:19.543),
"'2017-11-30 03:08'" == datetime!(2017 - 11 - 30 3:08),
"'2016-10-29T02:07:17'" == datetime!(2016 - 10 - 29 2:07:17),
"'2015-09-28T01:06:16.432'" == datetime!(2015 - 9 - 28 1:06:16.432),
"'2014-08-27T00:05'" == datetime!(2014 - 8 - 27 0:05),
"'2013-07-26 23:04:14Z'" == datetime!(2013 - 7 - 26 23:04:14),
"'2012-06-25 22:03:13.321Z'" == datetime!(2012 - 6 - 25 22:03:13.321),
"'2011-05-24 21:02Z'" == datetime!(2011 - 5 - 24 21:02),
"'2010-04-23T20:01:11Z'" == datetime!(2010 - 4 - 23 20:01:11),
"'2009-03-22T19:00:10.21Z'" == datetime!(2009 - 3 - 22 19:00:10.21),
"'2008-02-21T18:59Z'" == datetime!(2008 - 2 - 21 18:59:00),
));
test_type!(time_date<Date>(
Sqlite,
"SELECT date({0}) is date(?), {0}, ?",
"'2002-06-04'" == date!(2002 - 6 - 4),
));
test_type!(time_time<Time>(
Sqlite,
"SELECT time({0}) is time(?), {0}, ?",
"'21:46:32'" == time!(21:46:32),
"'20:45:31.133'" == time!(20:45:31.133),
"'19:44'" == time!(19:44),
));
}
#[cfg(feature = "bstr")]
mod bstr {
use super::*;