diff --git a/src/types/from_sql.rs b/src/types/from_sql.rs index 3fe74b4..4890a90 100644 --- a/src/types/from_sql.rs +++ b/src/types/from_sql.rs @@ -1,6 +1,7 @@ use super::{Value, ValueRef}; use std::error::Error; use std::fmt; +use std::convert::TryInto; /// Enum listing possible errors from `FromSql` trait. #[derive(Debug)] @@ -90,29 +91,12 @@ pub trait FromSql: Sized { fn column_result(value: ValueRef<'_>) -> FromSqlResult; } -impl FromSql for isize { - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - 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 { - 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) - } - }) + let i = i64::column_result(value)?; + i.try_into().map_err(|_| FromSqlError::OutOfRange(i)) } } ) @@ -121,9 +105,13 @@ macro_rules! from_sql_integral( from_sql_integral!(i8); from_sql_integral!(i16); from_sql_integral!(i32); +// from_sql_integral!(i64); // Not needed because the native type is i64. +from_sql_integral!(isize); from_sql_integral!(u8); from_sql_integral!(u16); from_sql_integral!(u32); +from_sql_integral!(u64); +from_sql_integral!(usize); impl FromSql for i64 { fn column_result(value: ValueRef<'_>) -> FromSqlResult { @@ -131,6 +119,16 @@ impl FromSql for i64 { } } +impl FromSql for f32 { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value { + ValueRef::Integer(i) => Ok(i as f32), + ValueRef::Real(f) => Ok(f as f32), + _ => Err(FromSqlError::InvalidType), + } + } +} + impl FromSql for f64 { fn column_result(value: ValueRef<'_>) -> FromSqlResult { match value { diff --git a/src/types/mod.rs b/src/types/mod.rs index 85d8ef2..8d0e1e7 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -4,11 +4,27 @@ //! 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` and `&[u8]`) +//! * Numbers +//! +//! The number situation is a little complicated due to the fact that all +//! numbers in SQLite are stored as `INTEGER` (`i64`) or `REAL` (`f64`). +//! +//! `ToSql` cannot fail and is therefore implemented for all number types that +//! can be losslessly converted to one of these types, i.e. `u8`, `u16`, `u32`, +//! `i8`, `i16`, `i32`, `i64`, `isize`, `f32` and `f64`. It is *not* implemented +//! for `u64` or `usize`. +//! +//! `FromSql` can fail, and is implemented for all primitive number types, +//! however you may get a runtime error or rounding depending on the types +//! and values. +//! +//! * `INTEGER` to integer: returns an `Error::IntegralValueOutOfRange` error +//! if the value does not fit. +//! * `REAL` to integer: always returns an `Error::InvalidColumnType` error. +//! * `INTEGER` to float: casts using `as` operator. Never fails. +//! * `REAL` to float: casts using `as` operator. Never fails. //! //! Additionally, if the `time` feature is enabled, implementations are //! provided for `time::OffsetDateTime` that use the RFC 3339 date/time format, @@ -116,7 +132,7 @@ impl fmt::Display for Type { #[cfg(test)] mod test { use super::Value; - use crate::{Connection, Error, NO_PARAMS}; + use crate::{Connection, Error, NO_PARAMS, params, Statement}; use std::f64::EPSILON; use std::os::raw::{c_double, c_int}; @@ -369,4 +385,76 @@ mod test { } assert_eq!(Value::Null, row.get::<_, Value>(4).unwrap()); } + + macro_rules! test_conversion { + ($db_etc:ident, $insert_value:expr, $get_type:ty, expect $expected_value:expr) => { + $db_etc.insert_statement.execute(params![$insert_value]).unwrap(); + let res = $db_etc.query_statement.query_row(NO_PARAMS, |row| { + row.get::<_, $get_type>(0) + }); + assert_eq!(res.unwrap(), $expected_value); + $db_etc.delete_statement.execute(NO_PARAMS).unwrap(); + }; + ($db_etc:ident, $insert_value:expr, $get_type:ty, expect_error) => { + $db_etc.insert_statement.execute(params![$insert_value]).unwrap(); + let res = $db_etc.query_statement.query_row(NO_PARAMS, |row| { + row.get::<_, $get_type>(0) + }); + res.unwrap_err(); + $db_etc.delete_statement.execute(NO_PARAMS).unwrap(); + }; + } + + #[test] + fn test_numeric_conversions() { + // Test what happens when we store an f32 and retrieve an i32 etc. + let db = Connection::open_in_memory().unwrap(); + db.execute_batch("CREATE TABLE foo (x)").unwrap(); + + // SQLite actually ignores the column types, so we just need to test + // different numeric values. + + struct DbEtc<'conn> { + insert_statement: Statement<'conn>, + query_statement: Statement<'conn>, + delete_statement: Statement<'conn>, + } + + let mut db_etc = DbEtc { + insert_statement: db.prepare("INSERT INTO foo VALUES (?1)").unwrap(), + query_statement: db.prepare("SELECT x FROM foo").unwrap(), + delete_statement: db.prepare("DELETE FROM foo").unwrap(), + }; + + // Basic non-converting test. + test_conversion!(db_etc, 0u8, u8, expect 0u8); + + // In-range integral conversions. + test_conversion!(db_etc, 100u8, i8, expect 100i8); + test_conversion!(db_etc, 200u8, u8, expect 200u8); + test_conversion!(db_etc, 100u16, i8, expect 100i8); + test_conversion!(db_etc, 200u16, u8, expect 200u8); + test_conversion!(db_etc, u32::MAX, u64, expect u32::MAX as u64); + test_conversion!(db_etc, i64::MIN, i64, expect i64::MIN); + test_conversion!(db_etc, i64::MAX, i64, expect i64::MAX); + test_conversion!(db_etc, i64::MAX, u64, expect i64::MAX as u64); + + // Out-of-range integral conversions. + test_conversion!(db_etc, 200u8, i8, expect_error); + test_conversion!(db_etc, 400u16, i8, expect_error); + test_conversion!(db_etc, 400u16, u8, expect_error); + test_conversion!(db_etc, -1i8, u8, expect_error); + test_conversion!(db_etc, i64::MIN, u64, expect_error); + + // Integer to float, always works. + test_conversion!(db_etc, i64::MIN, f32, expect i64::MIN as f32); + test_conversion!(db_etc, i64::MAX, f32, expect i64::MAX as f32); + test_conversion!(db_etc, i64::MIN, f64, expect i64::MIN as f64); + test_conversion!(db_etc, i64::MAX, f64, expect i64::MAX as f64); + + // Float to int conversion, never works even if the actual value is an + // integer. + test_conversion!(db_etc, 0f64, i64, expect_error); + + } } diff --git a/src/types/to_sql.rs b/src/types/to_sql.rs index 937c0f8..94e2c4f 100644 --- a/src/types/to_sql.rs +++ b/src/types/to_sql.rs @@ -59,6 +59,7 @@ from_value!(isize); from_value!(u8); from_value!(u16); from_value!(u32); +from_value!(f32); from_value!(f64); from_value!(Vec); @@ -147,6 +148,7 @@ to_sql_self!(isize); to_sql_self!(u8); to_sql_self!(u16); to_sql_self!(u32); +to_sql_self!(f32); to_sql_self!(f64); #[cfg(feature = "i128_blob")] diff --git a/src/types/value.rs b/src/types/value.rs index 64dc203..3facd10 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -78,6 +78,12 @@ impl From for Value { } } +impl From for Value { + fn from(f: f32) -> Value { + Value::Real(f.into()) + } +} + impl From for Value { fn from(f: f64) -> Value { Value::Real(f)