diff --git a/src/types/mod.rs b/src/types/mod.rs index 74f2391..fa0d2c8 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -11,22 +11,22 @@ //! 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. +//! `ToSql` and `FromSql` are implemented for all primitive number types. +//! `FromSql` has different behaviour depending on the SQL and Rust types, and +//! the value. //! //! * `INTEGER` to integer: returns an `Error::IntegralValueOutOfRange` error -//! if the value does not fit. +//! if the value does not fit in the Rust type. //! * `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 +//! `ToSql` always succeeds except when storing a `u64` or `usize` value that +//! cannot fit in an `INTEGER` (`i64`). Also note that SQLite ignores column +//! types, so if you store an `i64` in a column with type `REAL` it will be +//! stored as an `INTEGER`, not a `REAL`. +//! +//! 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 @@ -398,7 +398,7 @@ mod test { 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:ident, $insert_value:expr, $get_type:ty, expect_from_sql_error) => { $db_etc .insert_statement .execute(params![$insert_value]) @@ -409,6 +409,12 @@ mod test { res.unwrap_err(); $db_etc.delete_statement.execute(NO_PARAMS).unwrap(); }; + ($db_etc:ident, $insert_value:expr, $get_type:ty, expect_to_sql_error) => { + $db_etc + .insert_statement + .execute(params![$insert_value]) + .unwrap_err(); + }; } #[test] @@ -446,22 +452,28 @@ mod test { 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); + test_conversion!(db_etc, 100usize, usize, expect 100usize); + test_conversion!(db_etc, 100u64, u64, expect 100u64); + test_conversion!(db_etc, i64::MAX as u64, 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); + test_conversion!(db_etc, 200u8, i8, expect_from_sql_error); + test_conversion!(db_etc, 400u16, i8, expect_from_sql_error); + test_conversion!(db_etc, 400u16, u8, expect_from_sql_error); + test_conversion!(db_etc, -1i8, u8, expect_from_sql_error); + test_conversion!(db_etc, i64::MIN, u64, expect_from_sql_error); + test_conversion!(db_etc, u64::MAX, i64, expect_to_sql_error); + test_conversion!(db_etc, u64::MAX, u64, expect_to_sql_error); + test_conversion!(db_etc, i64::MAX as u64 + 1, u64, expect_to_sql_error); - // Integer to float, always works. + // FromSql 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); + // FromSql float to int conversion, never works even if the actual value + // is an integer. + test_conversion!(db_etc, 0f64, i64, expect_from_sql_error); } } diff --git a/src/types/to_sql.rs b/src/types/to_sql.rs index 94e2c4f..8f90422 100644 --- a/src/types/to_sql.rs +++ b/src/types/to_sql.rs @@ -1,10 +1,11 @@ use super::{Null, Value, ValueRef}; #[cfg(feature = "array")] use crate::vtab::array::Array; -use crate::Result; +use crate::{Error, Result}; use std::borrow::Cow; +use std::convert::TryFrom; -/// `ToSqlOutput` represents the possible output types for implementors of the +/// `ToSqlOutput` represents the possible output types for implementers of the /// `ToSql` trait. #[derive(Clone, Debug, PartialEq)] #[non_exhaustive] @@ -86,7 +87,8 @@ impl ToSql for ToSqlOutput<'_> { } } -/// A trait for types that can be converted into SQLite values. +/// A trait for types that can be converted into SQLite values. Returns +/// `Error::ToSqlConversionFailure` if the conversion fails. pub trait ToSql { /// Converts Rust value to SQLite value fn to_sql(&self) -> Result>; @@ -157,6 +159,25 @@ to_sql_self!(i128); #[cfg(feature = "uuid")] to_sql_self!(uuid::Uuid); +macro_rules! to_sql_self_fallible( + ($t:ty) => ( + impl ToSql for $t { + fn to_sql(&self) -> Result> { + Ok(ToSqlOutput::Owned(Value::Integer( + i64::try_from(*self).map_err( + // TODO: Include the values in the error message. + |err| Error::ToSqlConversionFailure(err.into()) + )? + ))) + } + } + ) +); + +// Special implementations for usize and u64 because these conversions can fail. +to_sql_self_fallible!(u64); +to_sql_self_fallible!(usize); + impl ToSql for &'_ T where T: ToSql,