From 5d8a840b5dc9114030bcfe44c29eb0b0e64697cb Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Sun, 24 Dec 2017 09:02:40 +0000 Subject: [PATCH] Fix date/time format for SQLite, use RFC 3339 We implement `ToSql` and `FromSql` for `time::Timespec` values. Our documentation indicates that we store the value in the same format used by SQLite's built-in date/time functions, but this was not correct. We were using the format: %Y-%m-%d %H:%M:%S:%f %Z This format cannot be interpreted at all by SQLite's built-in date/time functions. There are three reasons for this: - SQLite supports only two timezone formats: `[+-]HH:MM` and the literal character `Z` (indicating UTC) - SQLite does not support a space before the timezone indicator - SQLite supports a period (`.`) between the seconds field and the fractional seconds field, but not a colon (`:`) SQLite does support the RFC 3339 date/time format, which is standard in many other places. As we're always storing a UTC value, we'll simply use a trailing `Z` to indicate the timezone, as allowed by RFC 3339. The new format is: %Y-%m-%dT%H:%M:%S.%fZ To avoid breaking applications using databases with values in the old format, we'll continue to support it as a fallback for `FromSql`. [1] https://www.sqlite.org/lang_datefunc.html [2] https://tools.ietf.org/html/rfc3339 --- src/types/mod.rs | 13 +++++++------ src/types/time.rs | 13 ++++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index c998d89..74757cb 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -10,12 +10,13 @@ //! * Strings (`String` and `&str`) //! * Blobs (`Vec` and `&[u8]`) //! -//! Additionally, because it is such a common data type, implementations are provided for -//! `time::Timespec` that use a string for storage (using the same format string, -//! `"%Y-%m-%d %H:%M:%S"`, as SQLite's builtin -//! [datetime](https://www.sqlite.org/lang_datefunc.html) function. Note that this storage -//! truncates timespecs to the nearest second. If you want different storage for timespecs, you can -//! use a newtype. For example, to store timespecs as `f64`s: +//! Additionally, because it is such a common data type, implementations are +//! provided for `time::Timespec` that use the RFC 3339 date/time format, +//! `"%Y-%m-%dT%H:%M:%S.%fZ"`, to store time values as strings. These values +//! can be parsed by SQLite's builtin +//! [datetime](https://www.sqlite.org/lang_datefunc.html) functions. If you +//! want different storage for timespecs, you can use a newtype. For example, to +//! store timespecs as `f64`s: //! //! ```rust //! extern crate rusqlite; diff --git a/src/types/time.rs b/src/types/time.rs index f0dd46c..458435c 100644 --- a/src/types/time.rs +++ b/src/types/time.rs @@ -3,7 +3,8 @@ extern crate time; use Result; use types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; -const SQLITE_DATETIME_FMT: &str = "%Y-%m-%d %H:%M:%S:%f %Z"; +const SQLITE_DATETIME_FMT: &str = "%Y-%m-%dT%H:%M:%S.%fZ"; +const SQLITE_DATETIME_FMT_LEGACY: &str = "%Y-%m-%d %H:%M:%S:%f %Z"; impl ToSql for time::Timespec { fn to_sql(&self) -> Result { @@ -19,10 +20,12 @@ impl FromSql for time::Timespec { fn column_result(value: ValueRef) -> FromSqlResult { value .as_str() - .and_then(|s| match time::strptime(s, SQLITE_DATETIME_FMT) { - Ok(tm) => Ok(tm.to_timespec()), - Err(err) => Err(FromSqlError::Other(Box::new(err))), - }) + .and_then(|s| { + time::strptime(s, SQLITE_DATETIME_FMT) + .or_else(|err| { + time::strptime(s, SQLITE_DATETIME_FMT_LEGACY) + .or(Err(FromSqlError::Other(Box::new(err))))})}) + .map(|tm| tm.to_timespec()) } }