diff options
author | Joel Galenson <jgalenson@google.com> | 2020-07-28 13:34:02 -0700 |
---|---|---|
committer | Joel Galenson <jgalenson@google.com> | 2020-07-28 13:34:02 -0700 |
commit | 8b0df7ff3a4139db9f9ed71ea9d0bc82eca3eb30 (patch) | |
tree | ba5a264b0979e0fa15b1912cde8b9c04bd0e109f /src/types | |
parent | 26442fd4c4f3e1f469c323e695c4fcd972b150f5 (diff) | |
download | rusqlite-8b0df7ff3a4139db9f9ed71ea9d0bc82eca3eb30.tar.gz |
Import rusqlite-0.23.1
Change-Id: Id1ca7bcaad7820f463bfcce6945d80fc1d6918f5
Diffstat (limited to 'src/types')
-rw-r--r-- | src/types/chrono.rs | 280 | ||||
-rw-r--r-- | src/types/from_sql.rs | 272 | ||||
-rw-r--r-- | src/types/mod.rs | 363 | ||||
-rw-r--r-- | src/types/serde_json.rs | 60 | ||||
-rw-r--r-- | src/types/time.rs | 79 | ||||
-rw-r--r-- | src/types/to_sql.rs | 376 | ||||
-rw-r--r-- | src/types/url.rs | 81 | ||||
-rw-r--r-- | src/types/value.rs | 121 | ||||
-rw-r--r-- | src/types/value_ref.rs | 170 |
9 files changed, 1802 insertions, 0 deletions
diff --git a/src/types/chrono.rs b/src/types/chrono.rs new file mode 100644 index 0000000..3cba1e9 --- /dev/null +++ b/src/types/chrono.rs @@ -0,0 +1,280 @@ +//! Convert most of the [Time Strings](http://sqlite.org/lang_datefunc.html) to chrono types. + +use std::borrow::Cow; + +use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; + +use crate::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; +use crate::Result; + +/// ISO 8601 calendar date without timezone => "YYYY-MM-DD" +impl ToSql for NaiveDate { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + let date_str = self.format("%Y-%m-%d").to_string(); + Ok(ToSqlOutput::from(date_str)) + } +} + +/// "YYYY-MM-DD" => ISO 8601 calendar date without timezone. +impl FromSql for NaiveDate { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + value + .as_str() + .and_then(|s| match NaiveDate::parse_from_str(s, "%Y-%m-%d") { + Ok(dt) => Ok(dt), + Err(err) => Err(FromSqlError::Other(Box::new(err))), + }) + } +} + +/// ISO 8601 time without timezone => "HH:MM:SS.SSS" +impl ToSql for NaiveTime { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + let date_str = self.format("%H:%M:%S%.f").to_string(); + Ok(ToSqlOutput::from(date_str)) + } +} + +/// "HH:MM"/"HH:MM:SS"/"HH:MM:SS.SSS" => ISO 8601 time without timezone. +impl FromSql for NaiveTime { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + value.as_str().and_then(|s| { + let fmt = match s.len() { + 5 => "%H:%M", + 8 => "%H:%M:%S", + _ => "%H:%M:%S%.f", + }; + match NaiveTime::parse_from_str(s, fmt) { + Ok(dt) => Ok(dt), + Err(err) => Err(FromSqlError::Other(Box::new(err))), + } + }) + } +} + +/// ISO 8601 combined date and time without timezone => +/// "YYYY-MM-DDTHH:MM:SS.SSS" +impl ToSql for NaiveDateTime { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + let date_str = self.format("%Y-%m-%dT%H:%M:%S%.f").to_string(); + Ok(ToSqlOutput::from(date_str)) + } +} + +/// "YYYY-MM-DD HH:MM:SS"/"YYYY-MM-DD HH:MM:SS.SSS" => ISO 8601 combined date +/// and time without timezone. ("YYYY-MM-DDTHH:MM:SS"/"YYYY-MM-DDTHH:MM:SS.SSS" +/// also supported) +impl FromSql for NaiveDateTime { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + value.as_str().and_then(|s| { + let fmt = if s.len() >= 11 && s.as_bytes()[10] == b'T' { + "%Y-%m-%dT%H:%M:%S%.f" + } else { + "%Y-%m-%d %H:%M:%S%.f" + }; + + match NaiveDateTime::parse_from_str(s, fmt) { + Ok(dt) => Ok(dt), + Err(err) => Err(FromSqlError::Other(Box::new(err))), + } + }) + } +} + +/// Date and time with time zone => UTC RFC3339 timestamp +/// ("YYYY-MM-DDTHH:MM:SS.SSS+00:00"). +impl<Tz: TimeZone> ToSql for DateTime<Tz> { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + Ok(ToSqlOutput::from(self.with_timezone(&Utc).to_rfc3339())) + } +} + +/// RFC3339 ("YYYY-MM-DDTHH:MM:SS.SSS[+-]HH:MM") into `DateTime<Utc>`. +impl FromSql for DateTime<Utc> { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + { + // Try to parse value as rfc3339 first. + let s = value.as_str()?; + + // If timestamp looks space-separated, make a copy and replace it with 'T'. + let s = if s.len() >= 11 && s.as_bytes()[10] == b' ' { + let mut s = s.to_string(); + unsafe { + let sbytes = s.as_mut_vec(); + sbytes[10] = b'T'; + } + Cow::Owned(s) + } else { + Cow::Borrowed(s) + }; + + if let Ok(dt) = DateTime::parse_from_rfc3339(&s) { + return Ok(dt.with_timezone(&Utc)); + } + } + + // Couldn't parse as rfc3339 - fall back to NaiveDateTime. + NaiveDateTime::column_result(value).map(|dt| Utc.from_utc_datetime(&dt)) + } +} + +/// RFC3339 ("YYYY-MM-DDTHH:MM:SS.SSS[+-]HH:MM") into `DateTime<Local>`. +impl FromSql for DateTime<Local> { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + let utc_dt = DateTime::<Utc>::column_result(value)?; + Ok(utc_dt.with_timezone(&Local)) + } +} + +#[cfg(test)] +mod test { + use crate::{Connection, Result, NO_PARAMS}; + use chrono::{DateTime, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; + + fn checked_memory_handle() -> Connection { + let db = Connection::open_in_memory().unwrap(); + db.execute_batch("CREATE TABLE foo (t TEXT, i INTEGER, f FLOAT, b BLOB)") + .unwrap(); + db + } + + #[test] + fn test_naive_date() { + let db = checked_memory_handle(); + let date = NaiveDate::from_ymd(2016, 2, 23); + db.execute("INSERT INTO foo (t) VALUES (?)", &[&date]) + .unwrap(); + + let s: String = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!("2016-02-23", s); + let t: NaiveDate = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(date, t); + } + + #[test] + fn test_naive_time() { + let db = checked_memory_handle(); + let time = NaiveTime::from_hms(23, 56, 4); + db.execute("INSERT INTO foo (t) VALUES (?)", &[&time]) + .unwrap(); + + let s: String = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!("23:56:04", s); + let v: NaiveTime = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(time, v); + } + + #[test] + fn test_naive_date_time() { + let db = checked_memory_handle(); + let date = NaiveDate::from_ymd(2016, 2, 23); + let time = NaiveTime::from_hms(23, 56, 4); + let dt = NaiveDateTime::new(date, time); + + db.execute("INSERT INTO foo (t) VALUES (?)", &[&dt]) + .unwrap(); + + let s: String = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!("2016-02-23T23:56:04", s); + let v: NaiveDateTime = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(dt, v); + + db.execute("UPDATE foo set b = datetime(t)", NO_PARAMS) + .unwrap(); // "YYYY-MM-DD HH:MM:SS" + let hms: NaiveDateTime = db + .query_row("SELECT b FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(dt, hms); + } + + #[test] + fn test_date_time_utc() { + let db = checked_memory_handle(); + let date = NaiveDate::from_ymd(2016, 2, 23); + let time = NaiveTime::from_hms_milli(23, 56, 4, 789); + let dt = NaiveDateTime::new(date, time); + let utc = Utc.from_utc_datetime(&dt); + + db.execute("INSERT INTO foo (t) VALUES (?)", &[&utc]) + .unwrap(); + + let s: String = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!("2016-02-23T23:56:04.789+00:00", s); + + let v1: DateTime<Utc> = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(utc, v1); + + let v2: DateTime<Utc> = db + .query_row("SELECT '2016-02-23 23:56:04.789'", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(utc, v2); + + let v3: DateTime<Utc> = db + .query_row("SELECT '2016-02-23 23:56:04'", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(utc - Duration::milliseconds(789), v3); + + let v4: DateTime<Utc> = db + .query_row("SELECT '2016-02-23 23:56:04.789+00:00'", NO_PARAMS, |r| { + r.get(0) + }) + .unwrap(); + assert_eq!(utc, v4); + } + + #[test] + fn test_date_time_local() { + let db = checked_memory_handle(); + let date = NaiveDate::from_ymd(2016, 2, 23); + let time = NaiveTime::from_hms_milli(23, 56, 4, 789); + let dt = NaiveDateTime::new(date, time); + let local = Local.from_local_datetime(&dt).single().unwrap(); + + db.execute("INSERT INTO foo (t) VALUES (?)", &[&local]) + .unwrap(); + + // Stored string should be in UTC + let s: String = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert!(s.ends_with("+00:00")); + + let v: DateTime<Local> = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(local, v); + } + + #[test] + fn test_sqlite_functions() { + let db = checked_memory_handle(); + let result: Result<NaiveTime> = + db.query_row("SELECT CURRENT_TIME", NO_PARAMS, |r| r.get(0)); + assert!(result.is_ok()); + let result: Result<NaiveDate> = + db.query_row("SELECT CURRENT_DATE", NO_PARAMS, |r| r.get(0)); + assert!(result.is_ok()); + let result: Result<NaiveDateTime> = + db.query_row("SELECT CURRENT_TIMESTAMP", NO_PARAMS, |r| r.get(0)); + assert!(result.is_ok()); + let result: Result<DateTime<Utc>> = + db.query_row("SELECT CURRENT_TIMESTAMP", NO_PARAMS, |r| r.get(0)); + assert!(result.is_ok()); + } +} diff --git a/src/types/from_sql.rs b/src/types/from_sql.rs new file mode 100644 index 0000000..e8eadc5 --- /dev/null +++ b/src/types/from_sql.rs @@ -0,0 +1,272 @@ +use super::{Value, ValueRef}; +use std::error::Error; +use std::fmt; + +/// Enum listing possible errors from `FromSql` trait. +#[derive(Debug)] +#[non_exhaustive] +pub enum FromSqlError { + /// Error when an SQLite value is requested, but the type of the result + /// cannot be converted to the requested Rust type. + InvalidType, + + /// Error when the i64 value returned by SQLite cannot be stored into the + /// requested type. + OutOfRange(i64), + + /// `feature = "i128_blob"` Error returned when reading an `i128` from a + /// blob with a size other than 16. Only available when the `i128_blob` + /// feature is enabled. + #[cfg(feature = "i128_blob")] + InvalidI128Size(usize), + + /// `feature = "uuid"` Error returned when reading a `uuid` from a blob with + /// a size other than 16. Only available when the `uuid` feature is enabled. + #[cfg(feature = "uuid")] + InvalidUuidSize(usize), + + /// An error case available for implementors of the `FromSql` trait. + Other(Box<dyn Error + Send + Sync + 'static>), +} + +impl PartialEq for FromSqlError { + fn eq(&self, other: &FromSqlError) -> bool { + match (self, other) { + (FromSqlError::InvalidType, FromSqlError::InvalidType) => true, + (FromSqlError::OutOfRange(n1), FromSqlError::OutOfRange(n2)) => n1 == n2, + #[cfg(feature = "i128_blob")] + (FromSqlError::InvalidI128Size(s1), FromSqlError::InvalidI128Size(s2)) => s1 == s2, + #[cfg(feature = "uuid")] + (FromSqlError::InvalidUuidSize(s1), FromSqlError::InvalidUuidSize(s2)) => s1 == s2, + (..) => false, + } + } +} + +impl fmt::Display for FromSqlError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + FromSqlError::InvalidType => write!(f, "Invalid type"), + FromSqlError::OutOfRange(i) => write!(f, "Value {} out of range", i), + #[cfg(feature = "i128_blob")] + FromSqlError::InvalidI128Size(s) => { + write!(f, "Cannot read 128bit value out of {} byte blob", s) + } + #[cfg(feature = "uuid")] + FromSqlError::InvalidUuidSize(s) => { + write!(f, "Cannot read UUID value out of {} byte blob", s) + } + FromSqlError::Other(ref err) => err.fmt(f), + } + } +} + +impl Error for FromSqlError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + if let FromSqlError::Other(ref err) = self { + Some(&**err) + } else { + None + } + } +} + +/// Result type for implementors of the `FromSql` trait. +pub type FromSqlResult<T> = Result<T, FromSqlError>; + +/// A trait for types that can be created from a SQLite value. +/// +/// Note that `FromSql` and `ToSql` are defined for most integral types, but +/// not `u64` or `usize`. This is intentional; SQLite returns integers as +/// signed 64-bit values, which cannot fully represent the range of these +/// types. Rusqlite would have to +/// decide how to handle negative values: return an error or reinterpret as a +/// very large postive numbers, neither of which +/// is guaranteed to be correct for everyone. Callers can work around this by +/// fetching values as i64 and then doing the interpretation themselves or by +/// defining a newtype and implementing `FromSql`/`ToSql` for it. +pub trait FromSql: Sized { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self>; +} + +impl FromSql for isize { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + i64::column_result(value).and_then(|i| { + if i < isize::min_value() as i64 || i > isize::max_value() as i64 { + Err(FromSqlError::OutOfRange(i)) + } else { + Ok(i as isize) + } + }) + } +} + +macro_rules! from_sql_integral( + ($t:ident) => ( + impl FromSql for $t { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + i64::column_result(value).and_then(|i| { + if i < i64::from($t::min_value()) || i > i64::from($t::max_value()) { + Err(FromSqlError::OutOfRange(i)) + } else { + Ok(i as $t) + } + }) + } + } + ) +); + +from_sql_integral!(i8); +from_sql_integral!(i16); +from_sql_integral!(i32); +from_sql_integral!(u8); +from_sql_integral!(u16); +from_sql_integral!(u32); + +impl FromSql for i64 { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + value.as_i64() + } +} + +impl FromSql for f64 { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + match value { + ValueRef::Integer(i) => Ok(i as f64), + ValueRef::Real(f) => Ok(f), + _ => Err(FromSqlError::InvalidType), + } + } +} + +impl FromSql for bool { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + i64::column_result(value).map(|i| match i { + 0 => false, + _ => true, + }) + } +} + +impl FromSql for String { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + value.as_str().map(ToString::to_string) + } +} + +impl FromSql for Box<str> { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + value.as_str().map(Into::into) + } +} + +impl FromSql for std::rc::Rc<str> { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + value.as_str().map(Into::into) + } +} + +impl FromSql for std::sync::Arc<str> { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + value.as_str().map(Into::into) + } +} + +impl FromSql for Vec<u8> { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + value.as_blob().map(|b| b.to_vec()) + } +} + +#[cfg(feature = "i128_blob")] +impl FromSql for i128 { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + use byteorder::{BigEndian, ByteOrder}; + + value.as_blob().and_then(|bytes| { + if bytes.len() == 16 { + Ok(BigEndian::read_i128(bytes) ^ (1i128 << 127)) + } else { + Err(FromSqlError::InvalidI128Size(bytes.len())) + } + }) + } +} + +#[cfg(feature = "uuid")] +impl FromSql for uuid::Uuid { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + value + .as_blob() + .and_then(|bytes| { + uuid::Builder::from_slice(bytes) + .map_err(|_| FromSqlError::InvalidUuidSize(bytes.len())) + }) + .map(|mut builder| builder.build()) + } +} + +impl<T: FromSql> FromSql for Option<T> { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + match value { + ValueRef::Null => Ok(None), + _ => FromSql::column_result(value).map(Some), + } + } +} + +impl FromSql for Value { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + Ok(value.into()) + } +} + +#[cfg(test)] +mod test { + use super::FromSql; + use crate::{Connection, Error}; + + fn checked_memory_handle() -> Connection { + Connection::open_in_memory().unwrap() + } + + #[test] + fn test_integral_ranges() { + let db = checked_memory_handle(); + + fn check_ranges<T>(db: &Connection, out_of_range: &[i64], in_range: &[i64]) + where + T: Into<i64> + FromSql + ::std::fmt::Debug, + { + for n in out_of_range { + let err = db + .query_row("SELECT ?", &[n], |r| r.get::<_, T>(0)) + .unwrap_err(); + match err { + Error::IntegralValueOutOfRange(_, value) => assert_eq!(*n, value), + _ => panic!("unexpected error: {}", err), + } + } + for n in in_range { + assert_eq!( + *n, + db.query_row("SELECT ?", &[n], |r| r.get::<_, T>(0)) + .unwrap() + .into() + ); + } + } + + check_ranges::<i8>(&db, &[-129, 128], &[-128, 0, 1, 127]); + check_ranges::<i16>(&db, &[-32769, 32768], &[-32768, -1, 0, 1, 32767]); + check_ranges::<i32>( + &db, + &[-2_147_483_649, 2_147_483_648], + &[-2_147_483_648, -1, 0, 1, 2_147_483_647], + ); + check_ranges::<u8>(&db, &[-2, -1, 256], &[0, 1, 255]); + check_ranges::<u16>(&db, &[-2, -1, 65536], &[0, 1, 65535]); + check_ranges::<u32>(&db, &[-2, -1, 4_294_967_296], &[0, 1, 4_294_967_295]); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..d79ff82 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,363 @@ +//! Traits dealing with SQLite data types. +//! +//! SQLite uses a [dynamic type system](https://www.sqlite.org/datatype3.html). Implementations of +//! the `ToSql` and `FromSql` traits are provided for the basic types that +//! SQLite provides methods for: +//! +//! * Integers (`i32` and `i64`; SQLite uses `i64` internally, so getting an +//! `i32` will truncate if the value is too large or too small). +//! * Reals (`f64`) +//! * Strings (`String` and `&str`) +//! * Blobs (`Vec<u8>` 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, +//! `"%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<Self> { +//! 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<ToSqlOutput> { +//! let TimespecSql(ts) = *self; +//! let as_f64 = ts.sec as f64 + (ts.nsec as f64) / 1.0e9; +//! Ok(as_f64.into()) +//! } +//! } +//! ``` +//! +//! `ToSql` and `FromSql` are also implemented for `Option<T>` where `T` +//! implements `ToSql` or `FromSql` for the cases where you want to know if a +//! value was NULL (which gets translated to `None`). + +pub use self::from_sql::{FromSql, FromSqlError, FromSqlResult}; +pub use self::to_sql::{ToSql, ToSqlOutput}; +pub use self::value::Value; +pub use self::value_ref::ValueRef; + +use std::fmt; + +#[cfg(feature = "chrono")] +mod chrono; +mod from_sql; +#[cfg(feature = "serde_json")] +mod serde_json; +mod time; +mod to_sql; +#[cfg(feature = "url")] +mod url; +mod value; +mod value_ref; + +/// Empty struct that can be used to fill in a query parameter as `NULL`. +/// +/// ## Example +/// +/// ```rust,no_run +/// # use rusqlite::{Connection, Result}; +/// # use rusqlite::types::{Null}; +/// +/// fn insert_null(conn: &Connection) -> Result<usize> { +/// conn.execute("INSERT INTO people (name) VALUES (?)", &[Null]) +/// } +/// ``` +#[derive(Copy, Clone)] +pub struct Null; + +#[derive(Clone, Debug, PartialEq)] +pub enum Type { + Null, + Integer, + Real, + Text, + Blob, +} + +impl fmt::Display for Type { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Type::Null => write!(f, "Null"), + Type::Integer => write!(f, "Integer"), + Type::Real => write!(f, "Real"), + Type::Text => write!(f, "Text"), + Type::Blob => write!(f, "Blob"), + } + } +} + +#[cfg(test)] +mod test { + use super::Value; + use crate::{Connection, Error, NO_PARAMS}; + use std::f64::EPSILON; + use std::os::raw::{c_double, c_int}; + + fn checked_memory_handle() -> Connection { + let db = Connection::open_in_memory().unwrap(); + db.execute_batch("CREATE TABLE foo (b BLOB, t TEXT, i INTEGER, f FLOAT, n)") + .unwrap(); + db + } + + #[test] + fn test_blob() { + let db = checked_memory_handle(); + + let v1234 = vec![1u8, 2, 3, 4]; + db.execute("INSERT INTO foo(b) VALUES (?)", &[&v1234]) + .unwrap(); + + let v: Vec<u8> = db + .query_row("SELECT b FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(v, v1234); + } + + #[test] + fn test_empty_blob() { + let db = checked_memory_handle(); + + let empty = vec![]; + db.execute("INSERT INTO foo(b) VALUES (?)", &[&empty]) + .unwrap(); + + let v: Vec<u8> = db + .query_row("SELECT b FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(v, empty); + } + + #[test] + fn test_str() { + let db = checked_memory_handle(); + + let s = "hello, world!"; + db.execute("INSERT INTO foo(t) VALUES (?)", &[&s]).unwrap(); + + let from: String = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(from, s); + } + + #[test] + fn test_string() { + let db = checked_memory_handle(); + + let s = "hello, world!"; + db.execute("INSERT INTO foo(t) VALUES (?)", &[s.to_owned()]) + .unwrap(); + + let from: String = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(from, s); + } + + #[test] + fn test_value() { + let db = checked_memory_handle(); + + db.execute("INSERT INTO foo(i) VALUES (?)", &[Value::Integer(10)]) + .unwrap(); + + assert_eq!( + 10i64, + db.query_row::<i64, _, _>("SELECT i FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap() + ); + } + + #[test] + fn test_option() { + let db = checked_memory_handle(); + + let s = Some("hello, world!"); + let b = Some(vec![1u8, 2, 3, 4]); + + db.execute("INSERT INTO foo(t) VALUES (?)", &[&s]).unwrap(); + db.execute("INSERT INTO foo(b) VALUES (?)", &[&b]).unwrap(); + + let mut stmt = db + .prepare("SELECT t, b FROM foo ORDER BY ROWID ASC") + .unwrap(); + let mut rows = stmt.query(NO_PARAMS).unwrap(); + + { + let row1 = rows.next().unwrap().unwrap(); + let s1: Option<String> = row1.get_unwrap(0); + let b1: Option<Vec<u8>> = row1.get_unwrap(1); + assert_eq!(s.unwrap(), s1.unwrap()); + assert!(b1.is_none()); + } + + { + let row2 = rows.next().unwrap().unwrap(); + let s2: Option<String> = row2.get_unwrap(0); + let b2: Option<Vec<u8>> = row2.get_unwrap(1); + assert!(s2.is_none()); + assert_eq!(b, b2); + } + } + + #[test] + #[allow(clippy::cognitive_complexity)] + fn test_mismatched_types() { + fn is_invalid_column_type(err: Error) -> bool { + match err { + Error::InvalidColumnType(..) => true, + _ => false, + } + } + + let db = checked_memory_handle(); + + db.execute( + "INSERT INTO foo(b, t, i, f) VALUES (X'0102', 'text', 1, 1.5)", + NO_PARAMS, + ) + .unwrap(); + + let mut stmt = db.prepare("SELECT b, t, i, f, n FROM foo").unwrap(); + let mut rows = stmt.query(NO_PARAMS).unwrap(); + + let row = rows.next().unwrap().unwrap(); + + // check the correct types come back as expected + assert_eq!(vec![1, 2], row.get::<_, Vec<u8>>(0).unwrap()); + assert_eq!("text", row.get::<_, String>(1).unwrap()); + assert_eq!(1, row.get::<_, c_int>(2).unwrap()); + assert!((1.5 - row.get::<_, c_double>(3).unwrap()).abs() < EPSILON); + assert!(row.get::<_, Option<c_int>>(4).unwrap().is_none()); + assert!(row.get::<_, Option<c_double>>(4).unwrap().is_none()); + assert!(row.get::<_, Option<String>>(4).unwrap().is_none()); + + // check some invalid types + + // 0 is actually a blob (Vec<u8>) + assert!(is_invalid_column_type( + row.get::<_, c_int>(0).err().unwrap() + )); + assert!(is_invalid_column_type( + row.get::<_, c_int>(0).err().unwrap() + )); + assert!(is_invalid_column_type(row.get::<_, i64>(0).err().unwrap())); + assert!(is_invalid_column_type( + row.get::<_, c_double>(0).err().unwrap() + )); + assert!(is_invalid_column_type( + row.get::<_, String>(0).err().unwrap() + )); + assert!(is_invalid_column_type( + row.get::<_, time::Timespec>(0).err().unwrap() + )); + assert!(is_invalid_column_type( + row.get::<_, Option<c_int>>(0).err().unwrap() + )); + + // 1 is actually a text (String) + assert!(is_invalid_column_type( + row.get::<_, c_int>(1).err().unwrap() + )); + assert!(is_invalid_column_type(row.get::<_, i64>(1).err().unwrap())); + assert!(is_invalid_column_type( + row.get::<_, c_double>(1).err().unwrap() + )); + assert!(is_invalid_column_type( + row.get::<_, Vec<u8>>(1).err().unwrap() + )); + assert!(is_invalid_column_type( + row.get::<_, Option<c_int>>(1).err().unwrap() + )); + + // 2 is actually an integer + assert!(is_invalid_column_type( + row.get::<_, String>(2).err().unwrap() + )); + assert!(is_invalid_column_type( + row.get::<_, Vec<u8>>(2).err().unwrap() + )); + assert!(is_invalid_column_type( + row.get::<_, Option<String>>(2).err().unwrap() + )); + + // 3 is actually a float (c_double) + assert!(is_invalid_column_type( + row.get::<_, c_int>(3).err().unwrap() + )); + assert!(is_invalid_column_type(row.get::<_, i64>(3).err().unwrap())); + assert!(is_invalid_column_type( + row.get::<_, String>(3).err().unwrap() + )); + assert!(is_invalid_column_type( + row.get::<_, Vec<u8>>(3).err().unwrap() + )); + assert!(is_invalid_column_type( + row.get::<_, Option<c_int>>(3).err().unwrap() + )); + + // 4 is actually NULL + assert!(is_invalid_column_type( + row.get::<_, c_int>(4).err().unwrap() + )); + assert!(is_invalid_column_type(row.get::<_, i64>(4).err().unwrap())); + assert!(is_invalid_column_type( + row.get::<_, c_double>(4).err().unwrap() + )); + assert!(is_invalid_column_type( + row.get::<_, String>(4).err().unwrap() + )); + assert!(is_invalid_column_type( + row.get::<_, Vec<u8>>(4).err().unwrap() + )); + assert!(is_invalid_column_type( + row.get::<_, time::Timespec>(4).err().unwrap() + )); + } + + #[test] + fn test_dynamic_type() { + use super::Value; + let db = checked_memory_handle(); + + db.execute( + "INSERT INTO foo(b, t, i, f) VALUES (X'0102', 'text', 1, 1.5)", + NO_PARAMS, + ) + .unwrap(); + + let mut stmt = db.prepare("SELECT b, t, i, f, n FROM foo").unwrap(); + let mut rows = stmt.query(NO_PARAMS).unwrap(); + + let row = rows.next().unwrap().unwrap(); + assert_eq!(Value::Blob(vec![1, 2]), row.get::<_, Value>(0).unwrap()); + assert_eq!( + Value::Text(String::from("text")), + row.get::<_, Value>(1).unwrap() + ); + assert_eq!(Value::Integer(1), row.get::<_, Value>(2).unwrap()); + match row.get::<_, Value>(3).unwrap() { + Value::Real(val) => assert!((1.5 - val).abs() < EPSILON), + x => panic!("Invalid Value {:?}", x), + } + assert_eq!(Value::Null, row.get::<_, Value>(4).unwrap()); + } +} diff --git a/src/types/serde_json.rs b/src/types/serde_json.rs new file mode 100644 index 0000000..abaecda --- /dev/null +++ b/src/types/serde_json.rs @@ -0,0 +1,60 @@ +//! `ToSql` and `FromSql` implementation for JSON `Value`. + +use serde_json::Value; + +use crate::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; +use crate::Result; + +/// Serialize JSON `Value` to text. +impl ToSql for Value { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + Ok(ToSqlOutput::from(serde_json::to_string(self).unwrap())) + } +} + +/// Deserialize text/blob to JSON `Value`. +impl FromSql for Value { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + match value { + ValueRef::Text(s) => serde_json::from_slice(s), + ValueRef::Blob(b) => serde_json::from_slice(b), + _ => return Err(FromSqlError::InvalidType), + } + .map_err(|err| FromSqlError::Other(Box::new(err))) + } +} + +#[cfg(test)] +mod test { + use crate::types::ToSql; + use crate::{Connection, NO_PARAMS}; + + fn checked_memory_handle() -> Connection { + let db = Connection::open_in_memory().unwrap(); + db.execute_batch("CREATE TABLE foo (t TEXT, b BLOB)") + .unwrap(); + db + } + + #[test] + fn test_json_value() { + let db = checked_memory_handle(); + + let json = r#"{"foo": 13, "bar": "baz"}"#; + let data: serde_json::Value = serde_json::from_str(json).unwrap(); + db.execute( + "INSERT INTO foo (t, b) VALUES (?, ?)", + &[&data as &dyn ToSql, &json.as_bytes()], + ) + .unwrap(); + + let t: serde_json::Value = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(data, t); + let b: serde_json::Value = db + .query_row("SELECT b FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + assert_eq!(data, b); + } +} diff --git a/src/types/time.rs b/src/types/time.rs new file mode 100644 index 0000000..097b22a --- /dev/null +++ b/src/types/time.rs @@ -0,0 +1,79 @@ +use crate::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; +use crate::Result; + +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"; + +impl ToSql for time::Timespec { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + let time_string = time::at_utc(*self) + .strftime(SQLITE_DATETIME_FMT) + .unwrap() + .to_string(); + Ok(ToSqlOutput::from(time_string)) + } +} + +impl FromSql for time::Timespec { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + 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).or_else(|_| Err(err)) + }), + } + .or_else(|err| Err(FromSqlError::Other(Box::new(err)))) + }) + .map(|tm| tm.to_timespec()) + } +} + +#[cfg(test)] +mod test { + use crate::{Connection, Result, NO_PARAMS}; + + fn checked_memory_handle() -> Connection { + let db = Connection::open_in_memory().unwrap(); + db.execute_batch("CREATE TABLE foo (t TEXT, i INTEGER, f FLOAT)") + .unwrap(); + db + } + + #[test] + fn test_timespec() { + 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 + + for ts in ts_vec { + db.execute("INSERT INTO foo(t) VALUES (?)", &[&ts]).unwrap(); + + let from: time::Timespec = db + .query_row("SELECT t FROM foo", NO_PARAMS, |r| r.get(0)) + .unwrap(); + + db.execute("DELETE FROM foo", NO_PARAMS).unwrap(); + + assert_eq!(from, ts); + } + } + + #[test] + fn test_sqlite_functions() { + let db = checked_memory_handle(); + let result: Result<time::Timespec> = + db.query_row("SELECT CURRENT_TIMESTAMP", NO_PARAMS, |r| r.get(0)); + assert!(result.is_ok()); + } +} diff --git a/src/types/to_sql.rs b/src/types/to_sql.rs new file mode 100644 index 0000000..bec53f8 --- /dev/null +++ b/src/types/to_sql.rs @@ -0,0 +1,376 @@ +use super::{Null, Value, ValueRef}; +#[cfg(feature = "array")] +use crate::vtab::array::Array; +use crate::Result; +use std::borrow::Cow; + +/// `ToSqlOutput` represents the possible output types for implementors of the +/// `ToSql` trait. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub enum ToSqlOutput<'a> { + /// A borrowed SQLite-representable value. + Borrowed(ValueRef<'a>), + + /// An owned SQLite-representable value. + Owned(Value), + + /// `feature = "blob"` A BLOB of the given length that is filled with + /// zeroes. + #[cfg(feature = "blob")] + ZeroBlob(i32), + + /// `feature = "array"` + #[cfg(feature = "array")] + Array(Array), +} + +// Generically allow any type that can be converted into a ValueRef +// to be converted into a ToSqlOutput as well. +impl<'a, T: ?Sized> From<&'a T> for ToSqlOutput<'a> +where + &'a T: Into<ValueRef<'a>>, +{ + fn from(t: &'a T) -> Self { + ToSqlOutput::Borrowed(t.into()) + } +} + +// We cannot also generically allow any type that can be converted +// into a Value to be converted into a ToSqlOutput because of +// coherence rules (https://github.com/rust-lang/rust/pull/46192), +// so we'll manually implement it for all the types we know can +// be converted into Values. +macro_rules! from_value( + ($t:ty) => ( + impl From<$t> for ToSqlOutput<'_> { + fn from(t: $t) -> Self { ToSqlOutput::Owned(t.into())} + } + ) +); +from_value!(String); +from_value!(Null); +from_value!(bool); +from_value!(i8); +from_value!(i16); +from_value!(i32); +from_value!(i64); +from_value!(isize); +from_value!(u8); +from_value!(u16); +from_value!(u32); +from_value!(f64); +from_value!(Vec<u8>); + +// It would be nice if we could avoid the heap allocation (of the `Vec`) that +// `i128` needs in `Into<Value>`, but it's probably fine for the moment, and not +// worth adding another case to Value. +#[cfg(feature = "i128_blob")] +from_value!(i128); + +#[cfg(feature = "uuid")] +from_value!(uuid::Uuid); + +impl ToSql for ToSqlOutput<'_> { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + Ok(match *self { + ToSqlOutput::Borrowed(v) => ToSqlOutput::Borrowed(v), + ToSqlOutput::Owned(ref v) => ToSqlOutput::Borrowed(ValueRef::from(v)), + + #[cfg(feature = "blob")] + ToSqlOutput::ZeroBlob(i) => ToSqlOutput::ZeroBlob(i), + #[cfg(feature = "array")] + ToSqlOutput::Array(ref a) => ToSqlOutput::Array(a.clone()), + }) + } +} + +/// A trait for types that can be converted into SQLite values. +pub trait ToSql { + fn to_sql(&self) -> Result<ToSqlOutput<'_>>; +} + +impl<T: ToSql + ToOwned + ?Sized> ToSql for Cow<'_, T> { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + self.as_ref().to_sql() + } +} + +impl<T: ToSql + ?Sized> ToSql for Box<T> { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + self.as_ref().to_sql() + } +} + +impl<T: ToSql + ?Sized> ToSql for std::rc::Rc<T> { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + self.as_ref().to_sql() + } +} + +impl<T: ToSql + ?Sized> ToSql for std::sync::Arc<T> { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + self.as_ref().to_sql() + } +} + +// We should be able to use a generic impl like this: +// +// impl<T: Copy> ToSql for T where T: Into<Value> { +// fn to_sql(&self) -> Result<ToSqlOutput> { +// Ok(ToSqlOutput::from((*self).into())) +// } +// } +// +// instead of the following macro, but this runs afoul of +// https://github.com/rust-lang/rust/issues/30191 and reports conflicting +// implementations even when there aren't any. + +macro_rules! to_sql_self( + ($t:ty) => ( + impl ToSql for $t { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + Ok(ToSqlOutput::from(*self)) + } + } + ) +); + +to_sql_self!(Null); +to_sql_self!(bool); +to_sql_self!(i8); +to_sql_self!(i16); +to_sql_self!(i32); +to_sql_self!(i64); +to_sql_self!(isize); +to_sql_self!(u8); +to_sql_self!(u16); +to_sql_self!(u32); +to_sql_self!(f64); + +#[cfg(feature = "i128_blob")] +to_sql_self!(i128); + +#[cfg(feature = "uuid")] +to_sql_self!(uuid::Uuid); + +impl<T: ?Sized> ToSql for &'_ T +where + T: ToSql, +{ + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + (*self).to_sql() + } +} + +impl ToSql for String { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + Ok(ToSqlOutput::from(self.as_str())) + } +} + +impl ToSql for str { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + Ok(ToSqlOutput::from(self)) + } +} + +impl ToSql for Vec<u8> { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + Ok(ToSqlOutput::from(self.as_slice())) + } +} + +impl ToSql for [u8] { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + Ok(ToSqlOutput::from(self)) + } +} + +impl ToSql for Value { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + Ok(ToSqlOutput::from(self)) + } +} + +impl<T: ToSql> ToSql for Option<T> { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + match *self { + None => Ok(ToSqlOutput::from(Null)), + Some(ref t) => t.to_sql(), + } + } +} + +#[cfg(test)] +mod test { + use super::ToSql; + + fn is_to_sql<T: ToSql>() {} + + #[test] + fn test_integral_types() { + is_to_sql::<i8>(); + is_to_sql::<i16>(); + is_to_sql::<i32>(); + is_to_sql::<i64>(); + is_to_sql::<u8>(); + is_to_sql::<u16>(); + is_to_sql::<u32>(); + } + + #[test] + fn test_cow_str() { + use std::borrow::Cow; + let s = "str"; + let cow: Cow<str> = Cow::Borrowed(s); + let r = cow.to_sql(); + assert!(r.is_ok()); + let cow: Cow<str> = Cow::Owned::<str>(String::from(s)); + let r = cow.to_sql(); + assert!(r.is_ok()); + // Ensure this compiles. + let _p: &[&dyn ToSql] = crate::params![cow]; + } + + #[test] + fn test_box_dyn() { + let s: Box<dyn ToSql> = Box::new("Hello world!"); + let _s: &[&dyn ToSql] = crate::params![s]; + let r = ToSql::to_sql(&s); + + assert!(r.is_ok()); + } + + #[test] + fn test_box_deref() { + let s: Box<str> = "Hello world!".into(); + let _s: &[&dyn ToSql] = crate::params![s]; + let r = s.to_sql(); + + assert!(r.is_ok()); + } + + #[test] + fn test_box_direct() { + let s: Box<str> = "Hello world!".into(); + let _s: &[&dyn ToSql] = crate::params![s]; + let r = ToSql::to_sql(&s); + + assert!(r.is_ok()); + } + + #[test] + fn test_cells() { + use std::{rc::Rc, sync::Arc}; + + let source_str: Box<str> = "Hello world!".into(); + + let s: Rc<Box<str>> = Rc::new(source_str.clone()); + let _s: &[&dyn ToSql] = crate::params![s]; + let r = s.to_sql(); + assert!(r.is_ok()); + + let s: Arc<Box<str>> = Arc::new(source_str.clone()); + let _s: &[&dyn ToSql] = crate::params![s]; + let r = s.to_sql(); + assert!(r.is_ok()); + + let s: Arc<str> = Arc::from(&*source_str); + let _s: &[&dyn ToSql] = crate::params![s]; + let r = s.to_sql(); + assert!(r.is_ok()); + + let s: Arc<dyn ToSql> = Arc::new(source_str.clone()); + let _s: &[&dyn ToSql] = crate::params![s]; + let r = s.to_sql(); + assert!(r.is_ok()); + + let s: Rc<str> = Rc::from(&*source_str); + let _s: &[&dyn ToSql] = crate::params![s]; + let r = s.to_sql(); + assert!(r.is_ok()); + + let s: Rc<dyn ToSql> = Rc::new(source_str); + let _s: &[&dyn ToSql] = crate::params![s]; + let r = s.to_sql(); + assert!(r.is_ok()); + } + + #[cfg(feature = "i128_blob")] + #[test] + fn test_i128() { + use crate::{Connection, NO_PARAMS}; + use std::i128; + let db = Connection::open_in_memory().unwrap(); + db.execute_batch("CREATE TABLE foo (i128 BLOB, desc TEXT)") + .unwrap(); + db.execute( + " + INSERT INTO foo(i128, desc) VALUES + (?, 'zero'), + (?, 'neg one'), (?, 'neg two'), + (?, 'pos one'), (?, 'pos two'), + (?, 'min'), (?, 'max')", + &[0i128, -1i128, -2i128, 1i128, 2i128, i128::MIN, i128::MAX], + ) + .unwrap(); + + let mut stmt = db + .prepare("SELECT i128, desc FROM foo ORDER BY i128 ASC") + .unwrap(); + + let res = stmt + .query_map(NO_PARAMS, |row| { + Ok((row.get::<_, i128>(0)?, row.get::<_, String>(1)?)) + }) + .unwrap() + .collect::<Result<Vec<_>, _>>() + .unwrap(); + + assert_eq!( + res, + &[ + (i128::MIN, "min".to_owned()), + (-2, "neg two".to_owned()), + (-1, "neg one".to_owned()), + (0, "zero".to_owned()), + (1, "pos one".to_owned()), + (2, "pos two".to_owned()), + (i128::MAX, "max".to_owned()), + ] + ); + } + + #[cfg(feature = "uuid")] + #[test] + fn test_uuid() { + use crate::{params, Connection}; + use uuid::Uuid; + + let db = Connection::open_in_memory().unwrap(); + db.execute_batch("CREATE TABLE foo (id BLOB CHECK(length(id) = 16), label TEXT);") + .unwrap(); + + let id = Uuid::new_v4(); + + db.execute( + "INSERT INTO foo (id, label) VALUES (?, ?)", + params![id, "target"], + ) + .unwrap(); + + let mut stmt = db + .prepare("SELECT id, label FROM foo WHERE id = ?") + .unwrap(); + + let mut rows = stmt.query(params![id]).unwrap(); + let row = rows.next().unwrap().unwrap(); + + let found_id: Uuid = row.get_unwrap(0); + let found_label: String = row.get_unwrap(1); + + assert_eq!(found_id, id); + assert_eq!(found_label, "target"); + } +} diff --git a/src/types/url.rs b/src/types/url.rs new file mode 100644 index 0000000..1c9c63a --- /dev/null +++ b/src/types/url.rs @@ -0,0 +1,81 @@ +//! `ToSql` and `FromSql` implementation for [`url::Url`]. +use crate::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; +use crate::Result; +use url::Url; + +/// Serialize `Url` to text. +impl ToSql for Url { + fn to_sql(&self) -> Result<ToSqlOutput<'_>> { + Ok(ToSqlOutput::from(self.as_str())) + } +} + +/// Deserialize text to `Url`. +impl FromSql for Url { + fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { + match value { + ValueRef::Text(s) => { + let s = std::str::from_utf8(s).map_err(|e| FromSqlError::Other(Box::new(e)))?; + Url::parse(s).map_err(|e| FromSqlError::Other(Box::new(e))) + } + _ => Err(FromSqlError::InvalidType), + } + } +} + +#[cfg(test)] +mod test { + use crate::{params, Connection, Error, Result}; + use url::{ParseError, Url}; + + fn checked_memory_handle() -> Connection { + let db = Connection::open_in_memory().unwrap(); + db.execute_batch("CREATE TABLE urls (i INTEGER, v TEXT)") + .unwrap(); + db + } + + fn get_url(db: &Connection, id: i64) -> Result<Url> { + db.query_row("SELECT v FROM urls WHERE i = ?", params![id], |r| r.get(0)) + } + + #[test] + fn test_sql_url() { + let db = &checked_memory_handle(); + + let url0 = Url::parse("http://www.example1.com").unwrap(); + let url1 = Url::parse("http://www.example1.com/👌").unwrap(); + let url2 = "http://www.example2.com/👌"; + + db.execute( + "INSERT INTO urls (i, v) VALUES (0, ?), (1, ?), (2, ?), (3, ?)", + // also insert a non-hex encoded url (which might be present if it was + // inserted separately) + params![url0, url1, url2, "illegal"], + ) + .unwrap(); + + assert_eq!(get_url(db, 0).unwrap(), url0); + + assert_eq!(get_url(db, 1).unwrap(), url1); + + // Should successfully read it, even though it wasn't inserted as an + // escaped url. + let out_url2: Url = get_url(db, 2).unwrap(); + assert_eq!(out_url2, Url::parse(url2).unwrap()); + + // Make sure the conversion error comes through correctly. + let err = get_url(db, 3).unwrap_err(); + match err { + Error::FromSqlConversionFailure(_, _, e) => { + assert_eq!( + *e.downcast::<ParseError>().unwrap(), + ParseError::RelativeUrlWithoutBase, + ); + } + e => { + panic!("Expected conversion failure, got {}", e); + } + } + } +} diff --git a/src/types/value.rs b/src/types/value.rs new file mode 100644 index 0000000..332d78b --- /dev/null +++ b/src/types/value.rs @@ -0,0 +1,121 @@ +use super::{Null, Type}; + +/// Owning [dynamic type value](http://sqlite.org/datatype3.html). Value's type is typically +/// dictated by SQLite (not by the caller). +/// +/// See [`ValueRef`](enum.ValueRef.html) for a non-owning dynamic type value. +#[derive(Clone, Debug, PartialEq)] +pub enum Value { + /// The value is a `NULL` value. + Null, + /// The value is a signed integer. + Integer(i64), + /// The value is a floating point number. + Real(f64), + /// The value is a text string. + Text(String), + /// The value is a blob of data + Blob(Vec<u8>), +} + +impl From<Null> for Value { + fn from(_: Null) -> Value { + Value::Null + } +} + +impl From<bool> for Value { + fn from(i: bool) -> Value { + Value::Integer(i as i64) + } +} + +impl From<isize> for Value { + fn from(i: isize) -> Value { + Value::Integer(i as i64) + } +} + +#[cfg(feature = "i128_blob")] +impl From<i128> for Value { + fn from(i: i128) -> Value { + use byteorder::{BigEndian, ByteOrder}; + let mut buf = vec![0u8; 16]; + // We store these biased (e.g. with the most significant bit flipped) + // so that comparisons with negative numbers work properly. + BigEndian::write_i128(&mut buf, i ^ (1i128 << 127)); + Value::Blob(buf) + } +} + +#[cfg(feature = "uuid")] +impl From<uuid::Uuid> for Value { + fn from(id: uuid::Uuid) -> Value { + Value::Blob(id.as_bytes().to_vec()) + } +} + +macro_rules! from_i64( + ($t:ty) => ( + impl From<$t> for Value { + fn from(i: $t) -> Value { + Value::Integer(i64::from(i)) + } + } + ) +); + +from_i64!(i8); +from_i64!(i16); +from_i64!(i32); +from_i64!(u8); +from_i64!(u16); +from_i64!(u32); + +impl From<i64> for Value { + fn from(i: i64) -> Value { + Value::Integer(i) + } +} + +impl From<f64> for Value { + fn from(f: f64) -> Value { + Value::Real(f) + } +} + +impl From<String> for Value { + fn from(s: String) -> Value { + Value::Text(s) + } +} + +impl From<Vec<u8>> for Value { + fn from(v: Vec<u8>) -> Value { + Value::Blob(v) + } +} + +impl<T> From<Option<T>> for Value +where + T: Into<Value>, +{ + fn from(v: Option<T>) -> Value { + match v { + Some(x) => x.into(), + None => Value::Null, + } + } +} + +impl Value { + pub fn data_type(&self) -> Type { + match *self { + Value::Null => Type::Null, + Value::Integer(_) => Type::Integer, + Value::Real(_) => Type::Real, + Value::Text(_) => Type::Text, + Value::Blob(_) => Type::Blob, + } + } +} diff --git a/src/types/value_ref.rs b/src/types/value_ref.rs new file mode 100644 index 0000000..80d2457 --- /dev/null +++ b/src/types/value_ref.rs @@ -0,0 +1,170 @@ +use super::{Type, Value}; +use crate::types::{FromSqlError, FromSqlResult}; + +/// A non-owning [dynamic type value](http://sqlite.org/datatype3.html). Typically the +/// memory backing this value is owned by SQLite. +/// +/// See [`Value`](enum.Value.html) for an owning dynamic type value. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ValueRef<'a> { + /// The value is a `NULL` value. + Null, + /// The value is a signed integer. + Integer(i64), + /// The value is a floating point number. + Real(f64), + /// The value is a text string. + Text(&'a [u8]), + /// The value is a blob of data + Blob(&'a [u8]), +} + +impl ValueRef<'_> { + pub fn data_type(&self) -> Type { + match *self { + ValueRef::Null => Type::Null, + ValueRef::Integer(_) => Type::Integer, + ValueRef::Real(_) => Type::Real, + ValueRef::Text(_) => Type::Text, + ValueRef::Blob(_) => Type::Blob, + } + } +} + +impl<'a> ValueRef<'a> { + /// If `self` is case `Integer`, returns the integral value. Otherwise, + /// returns `Err(Error::InvalidColumnType)`. + pub fn as_i64(&self) -> FromSqlResult<i64> { + match *self { + ValueRef::Integer(i) => Ok(i), + _ => Err(FromSqlError::InvalidType), + } + } + + /// If `self` is case `Real`, returns the floating point value. Otherwise, + /// returns `Err(Error::InvalidColumnType)`. + pub fn as_f64(&self) -> FromSqlResult<f64> { + match *self { + ValueRef::Real(f) => Ok(f), + _ => Err(FromSqlError::InvalidType), + } + } + + /// If `self` is case `Text`, returns the string value. Otherwise, returns + /// `Err(Error::InvalidColumnType)`. + pub fn as_str(&self) -> FromSqlResult<&'a str> { + match *self { + ValueRef::Text(t) => { + std::str::from_utf8(t).map_err(|e| FromSqlError::Other(Box::new(e))) + } + _ => Err(FromSqlError::InvalidType), + } + } + + /// If `self` is case `Blob`, returns the byte slice. Otherwise, returns + /// `Err(Error::InvalidColumnType)`. + pub fn as_blob(&self) -> FromSqlResult<&'a [u8]> { + match *self { + ValueRef::Blob(b) => Ok(b), + _ => Err(FromSqlError::InvalidType), + } + } +} + +impl From<ValueRef<'_>> for Value { + fn from(borrowed: ValueRef<'_>) -> Value { + match borrowed { + ValueRef::Null => Value::Null, + ValueRef::Integer(i) => Value::Integer(i), + ValueRef::Real(r) => Value::Real(r), + ValueRef::Text(s) => { + let s = std::str::from_utf8(s).expect("invalid UTF-8"); + Value::Text(s.to_string()) + } + ValueRef::Blob(b) => Value::Blob(b.to_vec()), + } + } +} + +impl<'a> From<&'a str> for ValueRef<'a> { + fn from(s: &str) -> ValueRef<'_> { + ValueRef::Text(s.as_bytes()) + } +} + +impl<'a> From<&'a [u8]> for ValueRef<'a> { + fn from(s: &[u8]) -> ValueRef<'_> { + ValueRef::Blob(s) + } +} + +impl<'a> From<&'a Value> for ValueRef<'a> { + fn from(value: &'a Value) -> ValueRef<'a> { + match *value { + Value::Null => ValueRef::Null, + Value::Integer(i) => ValueRef::Integer(i), + Value::Real(r) => ValueRef::Real(r), + Value::Text(ref s) => ValueRef::Text(s.as_bytes()), + Value::Blob(ref b) => ValueRef::Blob(b), + } + } +} + +impl<'a, T> From<Option<T>> for ValueRef<'a> +where + T: Into<ValueRef<'a>>, +{ + fn from(s: Option<T>) -> ValueRef<'a> { + match s { + Some(x) => x.into(), + None => ValueRef::Null, + } + } +} + +#[cfg(any(feature = "functions", feature = "session", feature = "vtab"))] +impl<'a> ValueRef<'a> { + pub(crate) unsafe fn from_value(value: *mut crate::ffi::sqlite3_value) -> ValueRef<'a> { + use crate::ffi; + use std::slice::from_raw_parts; + + match ffi::sqlite3_value_type(value) { + ffi::SQLITE_NULL => ValueRef::Null, + ffi::SQLITE_INTEGER => ValueRef::Integer(ffi::sqlite3_value_int64(value)), + ffi::SQLITE_FLOAT => ValueRef::Real(ffi::sqlite3_value_double(value)), + ffi::SQLITE_TEXT => { + let text = ffi::sqlite3_value_text(value); + let len = ffi::sqlite3_value_bytes(value); + assert!( + !text.is_null(), + "unexpected SQLITE_TEXT value type with NULL data" + ); + let s = from_raw_parts(text as *const u8, len as usize); + ValueRef::Text(s) + } + ffi::SQLITE_BLOB => { + let (blob, len) = ( + ffi::sqlite3_value_blob(value), + ffi::sqlite3_value_bytes(value), + ); + + assert!( + len >= 0, + "unexpected negative return from sqlite3_value_bytes" + ); + if len > 0 { + assert!( + !blob.is_null(), + "unexpected SQLITE_BLOB value type with NULL data" + ); + ValueRef::Blob(from_raw_parts(blob as *const u8, len as usize)) + } else { + // The return value from sqlite3_value_blob() for a zero-length BLOB + // is a NULL pointer. + ValueRef::Blob(&[]) + } + } + _ => unreachable!("sqlite3_value_type returned invalid value"), + } + } +} |