mirror of
https://github.com/launchbadge/sqlx.git
synced 2025-12-29 21:00:54 +00:00
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:
parent
b3bbdab705
commit
cfef70a796
@ -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
|
||||
//!
|
||||
|
||||
@ -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;
|
||||
|
||||
309
sqlx-core/src/sqlite/types/time.rs
Normal file
309
sqlx-core/src/sqlite/types/time.rs
Normal 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")),
|
||||
]
|
||||
};
|
||||
}
|
||||
@ -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::*;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user