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
This commit is contained in:
Travis Cross 2017-12-24 09:02:40 +00:00
parent 402d5340d5
commit 5d8a840b5d
2 changed files with 15 additions and 11 deletions

View File

@ -10,12 +10,13 @@
//! * Strings (`String` and `&str`) //! * Strings (`String` and `&str`)
//! * Blobs (`Vec<u8>` and `&[u8]`) //! * Blobs (`Vec<u8>` and `&[u8]`)
//! //!
//! Additionally, because it is such a common data type, implementations are provided for //! Additionally, because it is such a common data type, implementations are
//! `time::Timespec` that use a string for storage (using the same format string, //! provided for `time::Timespec` that use the RFC 3339 date/time format,
//! `"%Y-%m-%d %H:%M:%S"`, as SQLite's builtin //! `"%Y-%m-%dT%H:%M:%S.%fZ"`, to store time values as strings. These values
//! [datetime](https://www.sqlite.org/lang_datefunc.html) function. Note that this storage //! can be parsed by SQLite's builtin
//! truncates timespecs to the nearest second. If you want different storage for timespecs, you can //! [datetime](https://www.sqlite.org/lang_datefunc.html) functions. If you
//! use a newtype. For example, to store timespecs as `f64`s: //! want different storage for timespecs, you can use a newtype. For example, to
//! store timespecs as `f64`s:
//! //!
//! ```rust //! ```rust
//! extern crate rusqlite; //! extern crate rusqlite;

View File

@ -3,7 +3,8 @@ extern crate time;
use Result; use Result;
use types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; 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 { impl ToSql for time::Timespec {
fn to_sql(&self) -> Result<ToSqlOutput> { fn to_sql(&self) -> Result<ToSqlOutput> {
@ -19,10 +20,12 @@ impl FromSql for time::Timespec {
fn column_result(value: ValueRef) -> FromSqlResult<Self> { fn column_result(value: ValueRef) -> FromSqlResult<Self> {
value value
.as_str() .as_str()
.and_then(|s| match time::strptime(s, SQLITE_DATETIME_FMT) { .and_then(|s| {
Ok(tm) => Ok(tm.to_timespec()), time::strptime(s, SQLITE_DATETIME_FMT)
Err(err) => Err(FromSqlError::Other(Box::new(err))), .or_else(|err| {
}) time::strptime(s, SQLITE_DATETIME_FMT_LEGACY)
.or(Err(FromSqlError::Other(Box::new(err))))})})
.map(|tm| tm.to_timespec())
} }
} }