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
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")),
]
};
}