Add a feature for storing i128 as blobs.

This is behind the `i128_blob` feature.

Blobs are stored as 16 byte big-endian values, with their most significant bit
flipped. This is so that sorting, comparison, etc all work properly, even with
negative numbers. This also allows the representation to be stable across
different computers.

It's possible that the `FromSql` implementation should handle the case that the
real value is stored in an integer. I didn't do this, but would be willing to
make the change. I don't think we should store them this way though, since I
don't think users would be able to sort/compare them sanely.

Support for `u128` is not implemented, as comparison with i128 values would work
strangely. This also is consistent with `u64` not being allowed, not that I
think that would be reason enough on it's own.

The `byteorder` crate is used if this feature is flipped, as it's quite small
and implements things more or less optimally. If/when `i128::{to,from}_be_bytes`
gets stabilized (https://github.com/rust-lang/rust/issues/52963), we should
probably use that instead.
This commit is contained in:
Thom Chiovoloni 2018-10-08 12:04:07 -07:00
parent 7176be2d6d
commit 572471c40f
8 changed files with 98 additions and 0 deletions

View File

@ -37,6 +37,7 @@ script:
- cargo test --features serde_json - cargo test --features serde_json
- cargo test --features bundled - cargo test --features bundled
- cargo test --features sqlcipher - cargo test --features sqlcipher
- cargo test --features i128_blob
- cargo test --features "unlock_notify bundled" - cargo test --features "unlock_notify bundled"
- cargo test --features "array bundled csvtab vtab" - cargo test --features "array bundled csvtab vtab"
- cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace vtab" - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace vtab"

View File

@ -32,6 +32,7 @@ bundled = ["libsqlite3-sys/bundled"]
buildtime_bindgen = ["libsqlite3-sys/buildtime_bindgen"] buildtime_bindgen = ["libsqlite3-sys/buildtime_bindgen"]
limits = [] limits = []
hooks = [] hooks = []
i128_blob = ["byteorder"]
sqlcipher = ["libsqlite3-sys/sqlcipher"] sqlcipher = ["libsqlite3-sys/sqlcipher"]
unlock_notify = ["libsqlite3-sys/unlock_notify"] unlock_notify = ["libsqlite3-sys/unlock_notify"]
# xSavepoint, xRelease and xRollbackTo: 3.7.7 (2011-06-23) # xSavepoint, xRelease and xRollbackTo: 3.7.7 (2011-06-23)
@ -48,6 +49,7 @@ chrono = { version = "0.4", optional = true }
serde_json = { version = "1.0", optional = true } serde_json = { version = "1.0", optional = true }
csv = { version = "1.0", optional = true } csv = { version = "1.0", optional = true }
lazy_static = { version = "1.0", optional = true } lazy_static = { version = "1.0", optional = true }
byteorder = { version = "1.2", features = ["i128"], optional = true }
[dev-dependencies] [dev-dependencies]
tempdir = "0.3" tempdir = "0.3"

View File

@ -98,6 +98,7 @@ features](https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-s
* `vtab` for [virtual table](https://sqlite.org/vtab.html) support (allows you to write virtual table implemntations in Rust). Currently, only read-only virtual tables are supported. * `vtab` for [virtual table](https://sqlite.org/vtab.html) support (allows you to write virtual table implemntations in Rust). Currently, only read-only virtual tables are supported.
* [`csvtab`](https://sqlite.org/csv.html), CSV virtual table written in Rust. * [`csvtab`](https://sqlite.org/csv.html), CSV virtual table written in Rust.
* [`array`](https://sqlite.org/carray.html), The `rarray()` Table-Valued Function. * [`array`](https://sqlite.org/carray.html), The `rarray()` Table-Valued Function.
* `i128_blob` allows storing values of type `i128` type in SQLite databases. Internally, the data is stored as a 16 byte big-endian blob, with the most significant bit flipped, which allows ordering and comparison between different blobs storing i128s to work as expected.
## Notes on building rusqlite and libsqlite3-sys ## Notes on building rusqlite and libsqlite3-sys

View File

@ -67,6 +67,9 @@ extern crate bitflags;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
#[cfg(feature = "i128_blob")]
extern crate byteorder;
use std::cell::RefCell; use std::cell::RefCell;
use std::convert; use std::convert;
use std::default::Default; use std::default::Default;

View File

@ -161,6 +161,10 @@ impl<'a, 'stmt> Row<'a, 'stmt> {
/// ///
/// Returns an `Error::InvalidColumnName` if `idx` is not a valid column /// Returns an `Error::InvalidColumnName` if `idx` is not a valid column
/// name for this row. /// name for this row.
///
/// If the result type is i128 (which requires the `i128_blob` feature to be
/// enabled), and the underlying SQLite column is a blob whose size is not
/// 16 bytes, `Error::InvalidColumnType` will also be returned.
pub fn get_checked<I: RowIndex, T: FromSql>(&self, idx: I) -> Result<T> { pub fn get_checked<I: RowIndex, T: FromSql>(&self, idx: I) -> Result<T> {
let idx = try!(idx.idx(self.stmt)); let idx = try!(idx.idx(self.stmt));
let value = self.stmt.value_ref(idx); let value = self.stmt.value_ref(idx);
@ -170,6 +174,10 @@ impl<'a, 'stmt> Row<'a, 'stmt> {
FromSqlError::Other(err) => { FromSqlError::Other(err) => {
Error::FromSqlConversionFailure(idx as usize, value.data_type(), err) Error::FromSqlConversionFailure(idx as usize, value.data_type(), err)
} }
#[cfg(feature = "i128_blob")]
FromSqlError::InvalidI128Size(_) => {
Error::InvalidColumnType(idx, value.data_type())
}
}) })
} }

View File

@ -13,6 +13,11 @@ pub enum FromSqlError {
/// requested type. /// requested type.
OutOfRange(i64), OutOfRange(i64),
/// 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),
/// An error case available for implementors of the `FromSql` trait. /// An error case available for implementors of the `FromSql` trait.
Other(Box<Error + Send + Sync>), Other(Box<Error + Send + Sync>),
} }
@ -22,6 +27,9 @@ impl fmt::Display for FromSqlError {
match *self { match *self {
FromSqlError::InvalidType => write!(f, "Invalid type"), FromSqlError::InvalidType => write!(f, "Invalid type"),
FromSqlError::OutOfRange(i) => write!(f, "Value {} out of range", i), 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),
FromSqlError::Other(ref err) => err.fmt(f), FromSqlError::Other(ref err) => err.fmt(f),
} }
} }
@ -32,6 +40,9 @@ impl Error for FromSqlError {
match *self { match *self {
FromSqlError::InvalidType => "invalid type", FromSqlError::InvalidType => "invalid type",
FromSqlError::OutOfRange(_) => "value out of range", FromSqlError::OutOfRange(_) => "value out of range",
#[cfg(feature = "i128_blob")]
FromSqlError::InvalidI128Size(_) =>
"unexpected blob size for 128bit value",
FromSqlError::Other(ref err) => err.description(), FromSqlError::Other(ref err) => err.description(),
} }
} }
@ -41,6 +52,8 @@ impl Error for FromSqlError {
match *self { match *self {
FromSqlError::Other(ref err) => err.cause(), FromSqlError::Other(ref err) => err.cause(),
FromSqlError::InvalidType | FromSqlError::OutOfRange(_) => None, FromSqlError::InvalidType | FromSqlError::OutOfRange(_) => None,
#[cfg(feature = "i128_blob")]
FromSqlError::InvalidI128Size(_) => None,
} }
} }
} }
@ -135,6 +148,21 @@ impl FromSql for Vec<u8> {
} }
} }
#[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()))
}
})
}
}
impl<T: FromSql> FromSql for Option<T> { impl<T: FromSql> FromSql for Option<T> {
fn column_result(value: ValueRef) -> FromSqlResult<Self> { fn column_result(value: ValueRef) -> FromSqlResult<Self> {
match value { match value {

View File

@ -59,6 +59,12 @@ from_value!(u32);
from_value!(f64); from_value!(f64);
from_value!(Vec<u8>); 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);
impl<'a> ToSql for ToSqlOutput<'a> { impl<'a> ToSql for ToSqlOutput<'a> {
fn to_sql(&self) -> Result<ToSqlOutput> { fn to_sql(&self) -> Result<ToSqlOutput> {
Ok(match *self { Ok(match *self {
@ -112,6 +118,9 @@ to_sql_self!(u16);
to_sql_self!(u32); to_sql_self!(u32);
to_sql_self!(f64); to_sql_self!(f64);
#[cfg(feature = "i128_blob")]
to_sql_self!(i128);
impl<'a, T: ?Sized> ToSql for &'a T impl<'a, T: ?Sized> ToSql for &'a T
where where
T: ToSql, T: ToSql,
@ -194,4 +203,38 @@ mod test {
let r = cow.to_sql(); let r = cow.to_sql();
assert!(r.is_ok()); assert!(r.is_ok());
} }
#[cfg(feature = "i128_blob")]
#[test]
fn test_i128() {
use {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| (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()),
]);
}
} }

View File

@ -36,6 +36,18 @@ impl From<isize> for Value {
} }
} }
#[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)
}
}
macro_rules! from_i64( macro_rules! from_i64(
($t:ty) => ( ($t:ty) => (
impl From<$t> for Value { impl From<$t> for Value {