Skip to content

Commit cfef70a

Browse files
authored
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 <[email protected]>
1 parent b3bbdab commit cfef70a

File tree

4 files changed

+389
-3
lines changed

4 files changed

+389
-3
lines changed

sqlx-core/src/mysql/types/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,15 @@
6464
//! | `uuid::Uuid` | BYTE(16), VARCHAR, CHAR, TEXT |
6565
//! | `uuid::fmt::Hyphenated` | CHAR(36) |
6666
//!
67-
//! ### [`json`](https://crates.io/crates/json)
67+
//! ### [`json`](https://crates.io/crates/serde_json)
6868
//!
6969
//! Requires the `json` Cargo feature flag.
7070
//!
7171
//! | Rust type | MySQL type(s) |
7272
//! |---------------------------------------|------------------------------------------------------|
73-
//! | `json::JsonValue` | JSON
73+
//! | [`Json<T>`] | JSON |
74+
//! | `serde_json::JsonValue` | JSON |
75+
//! | `&serde_json::value::RawValue` | JSON |
7476
//!
7577
//! # Nullable
7678
//!

sqlx-core/src/sqlite/types/mod.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,24 @@
2222
//!
2323
//! Requires the `chrono` Cargo feature flag.
2424
//!
25-
//! | Rust type | Sqlite type(s) |
25+
//! | Rust type | Sqlite type(s) |
2626
//! |---------------------------------------|------------------------------------------------------|
2727
//! | `chrono::NaiveDateTime` | DATETIME |
2828
//! | `chrono::DateTime<Utc>` | DATETIME |
2929
//! | `chrono::DateTime<Local>` | DATETIME |
30+
//! | `chrono::NaiveDate` | DATE |
31+
//! | `chrono::NaiveTime` | TIME |
32+
//!
33+
//! ### [`time`](https://crates.io/crates/time)
34+
//!
35+
//! Requires the `time` Cargo feature flag.
36+
//!
37+
//! | Rust type | Sqlite type(s) |
38+
//! |---------------------------------------|------------------------------------------------------|
39+
//! | `time::PrimitiveDateTime` | DATETIME |
40+
//! | `time::OffsetDateTime` | DATETIME |
41+
//! | `time::Date` | DATE |
42+
//! | `time::Time` | TIME |
3043
//!
3144
//! ### [`uuid`](https://crates.io/crates/uuid)
3245
//!
@@ -37,6 +50,16 @@
3750
//! | `uuid::Uuid` | BLOB, TEXT |
3851
//! | `uuid::fmt::Hyphenated` | TEXT |
3952
//!
53+
//! ### [`json`](https://crates.io/crates/serde_json)
54+
//!
55+
//! Requires the `json` Cargo feature flag.
56+
//!
57+
//! | Rust type | Sqlite type(s) |
58+
//! |---------------------------------------|------------------------------------------------------|
59+
//! | [`Json<T>`] | TEXT |
60+
//! | `serde_json::JsonValue` | TEXT |
61+
//! | `&serde_json::value::RawValue` | TEXT |
62+
//!
4063
//! # Nullable
4164
//!
4265
//! In addition, `Option<T>` is supported where `T` implements `Type`. An `Option<T>` represents
@@ -52,6 +75,8 @@ mod int;
5275
#[cfg(feature = "json")]
5376
mod json;
5477
mod str;
78+
#[cfg(feature = "time")]
79+
mod time;
5580
mod uint;
5681
#[cfg(feature = "uuid")]
5782
mod uuid;

sqlx-core/src/sqlite/types/time.rs

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
use crate::value::ValueRef;
2+
use crate::{
3+
decode::Decode,
4+
encode::{Encode, IsNull},
5+
error::BoxDynError,
6+
sqlite::{type_info::DataType, Sqlite, SqliteArgumentValue, SqliteTypeInfo, SqliteValueRef},
7+
types::Type,
8+
};
9+
use time::format_description::{well_known::Rfc3339, FormatItem};
10+
use time::macros::format_description as fd;
11+
use time::{Date, OffsetDateTime, PrimitiveDateTime, Time};
12+
13+
impl Type<Sqlite> for OffsetDateTime {
14+
fn type_info() -> SqliteTypeInfo {
15+
SqliteTypeInfo(DataType::Datetime)
16+
}
17+
18+
fn compatible(ty: &SqliteTypeInfo) -> bool {
19+
<PrimitiveDateTime as Type<Sqlite>>::compatible(ty)
20+
}
21+
}
22+
23+
impl Type<Sqlite> for PrimitiveDateTime {
24+
fn type_info() -> SqliteTypeInfo {
25+
SqliteTypeInfo(DataType::Datetime)
26+
}
27+
28+
fn compatible(ty: &SqliteTypeInfo) -> bool {
29+
matches!(
30+
ty.0,
31+
DataType::Datetime | DataType::Text | DataType::Int64 | DataType::Int
32+
)
33+
}
34+
}
35+
36+
impl Type<Sqlite> for Date {
37+
fn type_info() -> SqliteTypeInfo {
38+
SqliteTypeInfo(DataType::Date)
39+
}
40+
41+
fn compatible(ty: &SqliteTypeInfo) -> bool {
42+
matches!(ty.0, DataType::Date | DataType::Text)
43+
}
44+
}
45+
46+
impl Type<Sqlite> for Time {
47+
fn type_info() -> SqliteTypeInfo {
48+
SqliteTypeInfo(DataType::Time)
49+
}
50+
51+
fn compatible(ty: &SqliteTypeInfo) -> bool {
52+
matches!(ty.0, DataType::Time | DataType::Text)
53+
}
54+
}
55+
56+
impl Encode<'_, Sqlite> for OffsetDateTime {
57+
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> IsNull {
58+
Encode::<Sqlite>::encode(self.format(&Rfc3339).unwrap(), buf)
59+
}
60+
}
61+
62+
impl Encode<'_, Sqlite> for PrimitiveDateTime {
63+
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> IsNull {
64+
let format = fd!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]");
65+
Encode::<Sqlite>::encode(self.format(&format).unwrap(), buf)
66+
}
67+
}
68+
69+
impl Encode<'_, Sqlite> for Date {
70+
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> IsNull {
71+
let format = fd!("[year]-[month]-[day]");
72+
Encode::<Sqlite>::encode(self.format(&format).unwrap(), buf)
73+
}
74+
}
75+
76+
impl Encode<'_, Sqlite> for Time {
77+
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> IsNull {
78+
let format = fd!("[hour]:[minute]:[second].[subsecond]");
79+
Encode::<Sqlite>::encode(self.format(&format).unwrap(), buf)
80+
}
81+
}
82+
83+
impl<'r> Decode<'r, Sqlite> for OffsetDateTime {
84+
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
85+
decode_offset_datetime(value)
86+
}
87+
}
88+
89+
impl<'r> Decode<'r, Sqlite> for PrimitiveDateTime {
90+
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
91+
decode_datetime(value)
92+
}
93+
}
94+
95+
impl<'r> Decode<'r, Sqlite> for Date {
96+
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
97+
Ok(Date::parse(value.text()?, &fd!("[year]-[month]-[day]"))?)
98+
}
99+
}
100+
101+
impl<'r> Decode<'r, Sqlite> for Time {
102+
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
103+
let value = value.text()?;
104+
105+
let sqlite_time_formats = &[
106+
fd!("[hour]:[minute]:[second].[subsecond]"),
107+
fd!("[hour]:[minute]:[second]"),
108+
fd!("[hour]:[minute]"),
109+
];
110+
111+
for format in sqlite_time_formats {
112+
if let Ok(dt) = Time::parse(value, &format) {
113+
return Ok(dt);
114+
}
115+
}
116+
117+
Err(format!("invalid time: {}", value).into())
118+
}
119+
}
120+
121+
fn decode_offset_datetime(value: SqliteValueRef<'_>) -> Result<OffsetDateTime, BoxDynError> {
122+
let dt = match value.type_info().0 {
123+
DataType::Text => decode_offset_datetime_from_text(value.text()?),
124+
DataType::Int | DataType::Int64 => {
125+
Some(OffsetDateTime::from_unix_timestamp(value.int64())?)
126+
}
127+
128+
_ => None,
129+
};
130+
131+
if let Some(dt) = dt {
132+
Ok(dt)
133+
} else {
134+
Err(format!("invalid offset datetime: {}", value.text()?).into())
135+
}
136+
}
137+
138+
fn decode_offset_datetime_from_text(value: &str) -> Option<OffsetDateTime> {
139+
if let Ok(dt) = OffsetDateTime::parse(value, &Rfc3339) {
140+
return Some(dt);
141+
}
142+
143+
if let Ok(dt) = OffsetDateTime::parse(value, formats::OFFSET_DATE_TIME) {
144+
return Some(dt);
145+
}
146+
147+
None
148+
}
149+
150+
fn decode_datetime(value: SqliteValueRef<'_>) -> Result<PrimitiveDateTime, BoxDynError> {
151+
let dt = match value.type_info().0 {
152+
DataType::Text => decode_datetime_from_text(value.text()?),
153+
DataType::Int | DataType::Int64 => {
154+
let parsed = OffsetDateTime::from_unix_timestamp(value.int64()).unwrap();
155+
Some(PrimitiveDateTime::new(parsed.date(), parsed.time()))
156+
}
157+
158+
_ => None,
159+
};
160+
161+
if let Some(dt) = dt {
162+
Ok(dt)
163+
} else {
164+
Err(format!("invalid datetime: {}", value.text()?).into())
165+
}
166+
}
167+
168+
fn decode_datetime_from_text(value: &str) -> Option<PrimitiveDateTime> {
169+
let default_format = fd!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]");
170+
if let Ok(dt) = PrimitiveDateTime::parse(value, &default_format) {
171+
return Some(dt);
172+
}
173+
174+
let formats = [
175+
FormatItem::Compound(formats::PRIMITIVE_DATE_TIME_SPACE_SEPARATED),
176+
FormatItem::Compound(formats::PRIMITIVE_DATE_TIME_T_SEPARATED),
177+
];
178+
179+
if let Ok(dt) = PrimitiveDateTime::parse(value, &FormatItem::First(&formats)) {
180+
return Some(dt);
181+
}
182+
183+
None
184+
}
185+
186+
mod formats {
187+
use time::format_description::{modifier, Component::*, FormatItem, FormatItem::*};
188+
189+
const YEAR: FormatItem<'_> = Component(Year({
190+
let mut value = modifier::Year::default();
191+
value.padding = modifier::Padding::Zero;
192+
value.repr = modifier::YearRepr::Full;
193+
value.iso_week_based = false;
194+
value.sign_is_mandatory = false;
195+
value
196+
}));
197+
198+
const MONTH: FormatItem<'_> = Component(Month({
199+
let mut value = modifier::Month::default();
200+
value.padding = modifier::Padding::Zero;
201+
value.repr = modifier::MonthRepr::Numerical;
202+
value.case_sensitive = true;
203+
value
204+
}));
205+
206+
const DAY: FormatItem<'_> = Component(Day({
207+
let mut value = modifier::Day::default();
208+
value.padding = modifier::Padding::Zero;
209+
value
210+
}));
211+
212+
const HOUR: FormatItem<'_> = Component(Hour({
213+
let mut value = modifier::Hour::default();
214+
value.padding = modifier::Padding::Zero;
215+
value.is_12_hour_clock = false;
216+
value
217+
}));
218+
219+
const MINUTE: FormatItem<'_> = Component(Minute({
220+
let mut value = modifier::Minute::default();
221+
value.padding = modifier::Padding::Zero;
222+
value
223+
}));
224+
225+
const SECOND: FormatItem<'_> = Component(Second({
226+
let mut value = modifier::Second::default();
227+
value.padding = modifier::Padding::Zero;
228+
value
229+
}));
230+
231+
const SUBSECOND: FormatItem<'_> = Component(Subsecond({
232+
let mut value = modifier::Subsecond::default();
233+
value.digits = modifier::SubsecondDigits::OneOrMore;
234+
value
235+
}));
236+
237+
const OFFSET_HOUR: FormatItem<'_> = Component(OffsetHour({
238+
let mut value = modifier::OffsetHour::default();
239+
value.sign_is_mandatory = true;
240+
value.padding = modifier::Padding::Zero;
241+
value
242+
}));
243+
244+
const OFFSET_MINUTE: FormatItem<'_> = Component(OffsetMinute({
245+
let mut value = modifier::OffsetMinute::default();
246+
value.padding = modifier::Padding::Zero;
247+
value
248+
}));
249+
250+
pub(super) const OFFSET_DATE_TIME: &[FormatItem<'_>] = {
251+
&[
252+
YEAR,
253+
Literal(b"-"),
254+
MONTH,
255+
Literal(b"-"),
256+
DAY,
257+
Optional(&Literal(b" ")),
258+
Optional(&Literal(b"T")),
259+
HOUR,
260+
Literal(b":"),
261+
MINUTE,
262+
Optional(&Literal(b":")),
263+
Optional(&SECOND),
264+
Optional(&Literal(b".")),
265+
Optional(&SUBSECOND),
266+
Optional(&OFFSET_HOUR),
267+
Optional(&Literal(b":")),
268+
Optional(&OFFSET_MINUTE),
269+
]
270+
};
271+
272+
pub(super) const PRIMITIVE_DATE_TIME_SPACE_SEPARATED: &[FormatItem<'_>] = {
273+
&[
274+
YEAR,
275+
Literal(b"-"),
276+
MONTH,
277+
Literal(b"-"),
278+
DAY,
279+
Literal(b" "),
280+
HOUR,
281+
Literal(b":"),
282+
MINUTE,
283+
Optional(&Literal(b":")),
284+
Optional(&SECOND),
285+
Optional(&Literal(b".")),
286+
Optional(&SUBSECOND),
287+
Optional(&Literal(b"Z")),
288+
]
289+
};
290+
291+
pub(super) const PRIMITIVE_DATE_TIME_T_SEPARATED: &[FormatItem<'_>] = {
292+
&[
293+
YEAR,
294+
Literal(b"-"),
295+
MONTH,
296+
Literal(b"-"),
297+
DAY,
298+
Literal(b"T"),
299+
HOUR,
300+
Literal(b":"),
301+
MINUTE,
302+
Optional(&Literal(b":")),
303+
Optional(&SECOND),
304+
Optional(&Literal(b".")),
305+
Optional(&SUBSECOND),
306+
Optional(&Literal(b"Z")),
307+
]
308+
};
309+
}

0 commit comments

Comments
 (0)