From cfef70a79657292a760a31f3d53bb2565996fa0e Mon Sep 17 00:00:00 2001 From: John B Codes <59550+johnbcodes@users.noreply.github.com> Date: Fri, 8 Jul 2022 19:51:50 -0400 Subject: [PATCH] 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 --- sqlx-core/src/mysql/types/mod.rs | 6 +- sqlx-core/src/sqlite/types/mod.rs | 27 ++- sqlx-core/src/sqlite/types/time.rs | 309 +++++++++++++++++++++++++++++ tests/sqlite/types.rs | 50 +++++ 4 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 sqlx-core/src/sqlite/types/time.rs diff --git a/sqlx-core/src/mysql/types/mod.rs b/sqlx-core/src/mysql/types/mod.rs index 33b5fe93..cf07f7d7 100644 --- a/sqlx-core/src/mysql/types/mod.rs +++ b/sqlx-core/src/mysql/types/mod.rs @@ -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`] | JSON | +//! | `serde_json::JsonValue` | JSON | +//! | `&serde_json::value::RawValue` | JSON | //! //! # Nullable //! diff --git a/sqlx-core/src/sqlite/types/mod.rs b/sqlx-core/src/sqlite/types/mod.rs index 45cd77cb..3059588a 100644 --- a/sqlx-core/src/sqlite/types/mod.rs +++ b/sqlx-core/src/sqlite/types/mod.rs @@ -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` | DATETIME | //! | `chrono::DateTime` | 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`] | TEXT | +//! | `serde_json::JsonValue` | TEXT | +//! | `&serde_json::value::RawValue` | TEXT | +//! //! # Nullable //! //! In addition, `Option` is supported where `T` implements `Type`. An `Option` 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; diff --git a/sqlx-core/src/sqlite/types/time.rs b/sqlx-core/src/sqlite/types/time.rs new file mode 100644 index 00000000..1984cc6a --- /dev/null +++ b/sqlx-core/src/sqlite/types/time.rs @@ -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 for OffsetDateTime { + fn type_info() -> SqliteTypeInfo { + SqliteTypeInfo(DataType::Datetime) + } + + fn compatible(ty: &SqliteTypeInfo) -> bool { + >::compatible(ty) + } +} + +impl Type 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 for Date { + fn type_info() -> SqliteTypeInfo { + SqliteTypeInfo(DataType::Date) + } + + fn compatible(ty: &SqliteTypeInfo) -> bool { + matches!(ty.0, DataType::Date | DataType::Text) + } +} + +impl Type 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>) -> IsNull { + Encode::::encode(self.format(&Rfc3339).unwrap(), buf) + } +} + +impl Encode<'_, Sqlite> for PrimitiveDateTime { + fn encode_by_ref(&self, buf: &mut Vec>) -> IsNull { + let format = fd!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]"); + Encode::::encode(self.format(&format).unwrap(), buf) + } +} + +impl Encode<'_, Sqlite> for Date { + fn encode_by_ref(&self, buf: &mut Vec>) -> IsNull { + let format = fd!("[year]-[month]-[day]"); + Encode::::encode(self.format(&format).unwrap(), buf) + } +} + +impl Encode<'_, Sqlite> for Time { + fn encode_by_ref(&self, buf: &mut Vec>) -> IsNull { + let format = fd!("[hour]:[minute]:[second].[subsecond]"); + Encode::::encode(self.format(&format).unwrap(), buf) + } +} + +impl<'r> Decode<'r, Sqlite> for OffsetDateTime { + fn decode(value: SqliteValueRef<'r>) -> Result { + decode_offset_datetime(value) + } +} + +impl<'r> Decode<'r, Sqlite> for PrimitiveDateTime { + fn decode(value: SqliteValueRef<'r>) -> Result { + decode_datetime(value) + } +} + +impl<'r> Decode<'r, Sqlite> for Date { + fn decode(value: SqliteValueRef<'r>) -> Result { + Ok(Date::parse(value.text()?, &fd!("[year]-[month]-[day]"))?) + } +} + +impl<'r> Decode<'r, Sqlite> for Time { + fn decode(value: SqliteValueRef<'r>) -> Result { + 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 { + 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 { + 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 { + 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 { + 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")), + ] + }; +} diff --git a/tests/sqlite/types.rs b/tests/sqlite/types.rs index f2ea0470..f3db59c5 100644 --- a/tests/sqlite/types.rs +++ b/tests/sqlite/types.rs @@ -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( + 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( + 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( + Sqlite, + "SELECT date({0}) is date(?), {0}, ?", + "'2002-06-04'" == date!(2002 - 6 - 4), + )); + + test_type!(time_time