mirror of
https://github.com/launchbadge/sqlx.git
synced 2026-03-27 13:46:32 +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:
@@ -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")),
|
||||
]
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user