From b83d22e2b7a99dc80c0238754f4b0de3369d77e0 Mon Sep 17 00:00:00 2001 From: Nikhil Benesch Date: Sat, 11 Jul 2020 11:59:29 -0400 Subject: [PATCH] Upgrade to time v0.2 and put it behind a feature flag This also removes the usage of time in the crate's top-level documentation example, as was done for the README in #625. Fix #653. --- Cargo.toml | 7 +++-- README.md | 7 +++-- src/lib.rs | 14 +++------- src/types/mod.rs | 66 ++++++++++++++++++++++++----------------------- src/types/time.rs | 59 ++++++++++++++++++++++-------------------- 5 files changed, 79 insertions(+), 74 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 09a3589..1321ad2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,6 +83,9 @@ bundled-full = [ "load_extension", "serde_json", "series", + # time v0.2 does not work with tarpaulin v0.14.0. See time-rs/time#265. + # Re-enable when time v0.3 is released with the fix. + # "time", "trace", "unlock_notify", "url", @@ -92,7 +95,7 @@ bundled-full = [ ] [dependencies] -time = "0.1.0" +time = { version = "0.2", optional = true } bitflags = "1.0" lru-cache = "0.1" chrono = { version = "0.4", optional = true } @@ -141,7 +144,7 @@ name = "exec" harness = false [package.metadata.docs.rs] -features = [ "backup", "blob", "chrono", "collation", "functions", "limits", "load_extension", "serde_json", "trace", "url", "vtab", "window", "modern_sqlite", "column_decltype" ] +features = [ "backup", "blob", "chrono", "collation", "functions", "limits", "load_extension", "serde_json", "time", "trace", "url", "vtab", "window", "modern_sqlite", "column_decltype" ] all-features = false no-default-features = true default-target = "x86_64-unknown-linux-gnu" diff --git a/README.md b/README.md index 7e55019..97f7177 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,9 @@ features](https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-s * `serde_json` implements [`FromSql`](https://docs.rs/rusqlite/~0/rusqlite/types/trait.FromSql.html) and [`ToSql`](https://docs.rs/rusqlite/~0/rusqlite/types/trait.ToSql.html) for the `Value` type from the [`serde_json` crate](https://crates.io/crates/serde_json). +* `time` implements [`FromSql`](https://docs.rs/rusqlite/~0/rusqlite/types/trait.FromSql.html) + and [`ToSql`](https://docs.rs/rusqlite/~0/rusqlite/types/trait.ToSql.html) for the + `time::OffsetDateTime` type from the [`time` crate](https://crates.io/crates/time). * `url` implements [`FromSql`](https://docs.rs/rusqlite/~0/rusqlite/types/trait.FromSql.html) and [`ToSql`](https://docs.rs/rusqlite/~0/rusqlite/types/trait.ToSql.html) for the `Url` type from the [`url` crate](https://crates.io/crates/url). @@ -109,7 +112,7 @@ features](https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-s `libsqlite3-sys` is a separate crate from `rusqlite` that provides the Rust declarations for SQLite's C API. By default, `libsqlite3-sys` attempts to find a SQLite library that already exists on your system using pkg-config, or a -[Vcpkg](https://github.com/Microsoft/vcpkg) installation for MSVC ABI builds. +[Vcpkg](https://github.com/Microsoft/vcpkg) installation for MSVC ABI builds. You can adjust this behavior in a number of ways: @@ -131,7 +134,7 @@ You can adjust this behavior in a number of ways: options. The default when using vcpkg is to dynamically link, which must be enabled by setting `VCPKGRS_DYNAMIC=1` environment variable before build. `vcpkg install sqlite3:x64-windows` will install the required library. - + ### Binding generation We use [bindgen](https://crates.io/crates/bindgen) to generate the Rust diff --git a/src/lib.rs b/src/lib.rs index e2bfa94..a16106c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,13 +3,11 @@ //! //! ```rust //! use rusqlite::{params, Connection, Result}; -//! use time::Timespec; //! //! #[derive(Debug)] //! struct Person { //! id: i32, //! name: String, -//! time_created: Timespec, //! data: Option>, //! } //! @@ -20,7 +18,6 @@ //! "CREATE TABLE person ( //! id INTEGER PRIMARY KEY, //! name TEXT NOT NULL, -//! time_created TEXT NOT NULL, //! data BLOB //! )", //! params![], @@ -28,22 +25,19 @@ //! let me = Person { //! id: 0, //! name: "Steven".to_string(), -//! time_created: time::get_time(), //! data: None, //! }; //! conn.execute( -//! "INSERT INTO person (name, time_created, data) -//! VALUES (?1, ?2, ?3)", -//! params![me.name, me.time_created, me.data], +//! "INSERT INTO person (name, data) VALUES (?1, ?2)", +//! params![me.name, me.data], //! )?; //! -//! let mut stmt = conn.prepare("SELECT id, name, time_created, data FROM person")?; +//! let mut stmt = conn.prepare("SELECT id, name, data FROM person")?; //! let person_iter = stmt.query_map(params![], |row| { //! Ok(Person { //! id: row.get(0)?, //! name: row.get(1)?, -//! time_created: row.get(2)?, -//! data: row.get(3)?, +//! data: row.get(2)?, //! }) //! })?; //! diff --git a/src/types/mod.rs b/src/types/mod.rs index 92f5876..2d163cf 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -10,40 +10,39 @@ //! * 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 the RFC 3339 date/time format, +//! Additionally, if the `time` feature is enabled, implementations are +//! provided for `time::OffsetDateTime` 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 -//! use rusqlite::types::{FromSql, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; -//! use rusqlite::Result; -//! -//! pub struct TimespecSql(pub time::Timespec); -//! -//! impl FromSql for TimespecSql { -//! fn column_result(value: ValueRef) -> FromSqlResult { -//! f64::column_result(value).map(|as_f64| { -//! TimespecSql(time::Timespec { -//! sec: as_f64.trunc() as i64, -//! nsec: (as_f64.fract() * 1.0e9) as i32, -//! }) -//! }) -//! } -//! } -//! -//! impl ToSql for TimespecSql { -//! fn to_sql(&self) -> Result { -//! let TimespecSql(ts) = *self; -//! let as_f64 = ts.sec as f64 + (ts.nsec as f64) / 1.0e9; -//! Ok(as_f64.into()) -//! } -//! } -//! ``` +//! want different storage for datetimes, you can use a newtype. //! +#![cfg_attr(feature = "time", doc = r##" +For example, to store datetimes as `i64`s counting the number of seconds since +the Unix epoch: + +``` +use rusqlite::types::{FromSql, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; +use rusqlite::Result; + +pub struct DateTimeSql(pub time::OffsetDateTime); + +impl FromSql for DateTimeSql { + fn column_result(value: ValueRef) -> FromSqlResult { + i64::column_result(value).map(|as_i64| { + DateTimeSql(time::OffsetDateTime::from_unix_timestamp(as_i64)) + }) + } +} + +impl ToSql for DateTimeSql { + fn to_sql(&self) -> Result { + Ok(self.0.timestamp().into()) + } +} +``` + +"##)] //! `ToSql` and `FromSql` are also implemented for `Option` where `T` //! implements `ToSql` or `FromSql` for the cases where you want to know if a //! value was NULL (which gets translated to `None`). @@ -60,6 +59,7 @@ mod chrono; mod from_sql; #[cfg(feature = "serde_json")] mod serde_json; +#[cfg(feature = "time")] mod time; mod to_sql; #[cfg(feature = "url")] @@ -273,8 +273,9 @@ mod test { assert!(is_invalid_column_type( row.get::<_, String>(0).err().unwrap() )); + #[cfg(feature = "time")] assert!(is_invalid_column_type( - row.get::<_, time::Timespec>(0).err().unwrap() + row.get::<_, time::OffsetDateTime>(0).err().unwrap() )); assert!(is_invalid_column_type( row.get::<_, Option>(0).err().unwrap() @@ -335,8 +336,9 @@ mod test { assert!(is_invalid_column_type( row.get::<_, Vec>(4).err().unwrap() )); + #[cfg(feature = "time")] assert!(is_invalid_column_type( - row.get::<_, time::Timespec>(4).err().unwrap() + row.get::<_, time::OffsetDateTime>(4).err().unwrap() )); } diff --git a/src/types/time.rs b/src/types/time.rs index aa3e38d..8589167 100644 --- a/src/types/time.rs +++ b/src/types/time.rs @@ -1,40 +1,40 @@ +//! `ToSql` and `FromSql` implementation for [`time::OffsetDateTime`]. use crate::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; use crate::Result; +use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset}; const CURRENT_TIMESTAMP_FMT: &str = "%Y-%m-%d %H:%M:%S"; -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"; +const SQLITE_DATETIME_FMT: &str = "%Y-%m-%dT%H:%M:%S.%NZ"; +const SQLITE_DATETIME_FMT_LEGACY: &str = "%Y-%m-%d %H:%M:%S:%N %z"; -impl ToSql for time::Timespec { +impl ToSql for OffsetDateTime { fn to_sql(&self) -> Result> { - let time_string = time::at_utc(*self) - .strftime(SQLITE_DATETIME_FMT) - .unwrap() - .to_string(); + let time_string = self.to_offset(UtcOffset::UTC).format(SQLITE_DATETIME_FMT); Ok(ToSqlOutput::from(time_string)) } } -impl FromSql for time::Timespec { +impl FromSql for OffsetDateTime { fn column_result(value: ValueRef<'_>) -> FromSqlResult { - value - .as_str() - .and_then(|s| { - match s.len() { - 19 => time::strptime(s, CURRENT_TIMESTAMP_FMT), - _ => time::strptime(s, SQLITE_DATETIME_FMT).or_else(|err| { - time::strptime(s, SQLITE_DATETIME_FMT_LEGACY).map_err(|_| err) + value.as_str().and_then(|s| { + match s.len() { + 19 => PrimitiveDateTime::parse(s, CURRENT_TIMESTAMP_FMT).map(|d| d.assume_utc()), + _ => PrimitiveDateTime::parse(s, SQLITE_DATETIME_FMT) + .map(|d| d.assume_utc()) + .or_else(|err| { + OffsetDateTime::parse(s, SQLITE_DATETIME_FMT_LEGACY).map_err(|_| err) }), - } - .map_err(|err| FromSqlError::Other(Box::new(err))) - }) - .map(|tm| tm.to_timespec()) + } + .map_err(|err| FromSqlError::Other(Box::new(err))) + }) } } #[cfg(test)] mod test { use crate::{Connection, Result, NO_PARAMS}; + use std::time::Duration; + use time::OffsetDateTime; fn checked_memory_handle() -> Connection { let db = Connection::open_in_memory().unwrap(); @@ -44,22 +44,25 @@ mod test { } #[test] - fn test_timespec() { + fn test_offset_date_time() { let db = checked_memory_handle(); let mut ts_vec = vec![]; - ts_vec.push(time::Timespec::new(10_000, 0)); //January 1, 1970 2:46:40 AM - ts_vec.push(time::Timespec::new(10_000, 1000)); //January 1, 1970 2:46:40 AM (and one microsecond) - ts_vec.push(time::Timespec::new(1_500_391_124, 1_000_000)); //July 18, 2017 - ts_vec.push(time::Timespec::new(2_000_000_000, 2_000_000)); //May 18, 2033 - ts_vec.push(time::Timespec::new(3_000_000_000, 999_999_999)); //January 24, 2065 - ts_vec.push(time::Timespec::new(10_000_000_000, 0)); //November 20, 2286 + let make_datetime = + |secs, nanos| OffsetDateTime::from_unix_timestamp(secs) + Duration::from_nanos(nanos); + + ts_vec.push(make_datetime(10_000, 0)); //January 1, 1970 2:46:40 AM + ts_vec.push(make_datetime(10_000, 1000)); //January 1, 1970 2:46:40 AM (and one microsecond) + ts_vec.push(make_datetime(1_500_391_124, 1_000_000)); //July 18, 2017 + ts_vec.push(make_datetime(2_000_000_000, 2_000_000)); //May 18, 2033 + ts_vec.push(make_datetime(3_000_000_000, 999_999_999)); //January 24, 2065 + ts_vec.push(make_datetime(10_000_000_000, 0)); //November 20, 2286 for ts in ts_vec { db.execute("INSERT INTO foo(t) VALUES (?)", &[&ts]).unwrap(); - let from: time::Timespec = db + let from: OffsetDateTime = db .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) .unwrap(); @@ -72,7 +75,7 @@ mod test { #[test] fn test_sqlite_functions() { let db = checked_memory_handle(); - let result: Result = + let result: Result = db.query_row("SELECT CURRENT_TIMESTAMP", NO_PARAMS, |r| r.get(0)); assert!(result.is_ok()); }