Implement From/ToSql for more types

This implements `FromSql` for `u64`, `usize` and `f32`, and `ToSql` for `f32`.

I also updated the documentation to describe how it currently works, and changed the implementation to use `try_from` for integral casts rather rather than custom code.

Test added.
This commit is contained in:
Tim Hutt 2020-10-22 21:51:30 +01:00 committed by Thom Chiovoloni
parent 2461540306
commit ebcec59969
4 changed files with 117 additions and 23 deletions

View File

@ -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<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)
}
})
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<Self> {
@ -131,6 +119,16 @@ impl FromSql for i64 {
}
}
impl FromSql for f32 {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
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<Self> {
match value {

View File

@ -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<u8>` 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);
}
}

View File

@ -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<u8>);
@ -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")]

View File

@ -78,6 +78,12 @@ impl From<i64> for Value {
}
}
impl From<f32> for Value {
fn from(f: f32) -> Value {
Value::Real(f.into())
}
}
impl From<f64> for Value {
fn from(f: f64) -> Value {
Value::Real(f)