diff --git a/.travis.yml b/.travis.yml index 68c645b..2888475 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,6 +37,7 @@ script: - cargo test --features serde_json - cargo test --features bundled - cargo test --features sqlcipher + - cargo test --features i128_blob - cargo test --features "unlock_notify bundled" - cargo test --features "array bundled csvtab vtab" - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace vtab" diff --git a/Cargo.toml b/Cargo.toml index b52cdfe..7def848 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ bundled = ["libsqlite3-sys/bundled"] buildtime_bindgen = ["libsqlite3-sys/buildtime_bindgen"] limits = [] hooks = [] +i128_blob = ["byteorder"] sqlcipher = ["libsqlite3-sys/sqlcipher"] unlock_notify = ["libsqlite3-sys/unlock_notify"] # 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 } csv = { version = "1.0", optional = true } lazy_static = { version = "1.0", optional = true } +byteorder = { version = "1.2", features = ["i128"], optional = true } [dev-dependencies] tempdir = "0.3" diff --git a/README.md b/README.md index 1756efb..deb128e 100644 --- a/README.md +++ b/README.md @@ -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. * [`csvtab`](https://sqlite.org/csv.html), CSV virtual table written in Rust. * [`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 diff --git a/src/lib.rs b/src/lib.rs index 10bf62a..d086a23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,9 @@ extern crate bitflags; #[macro_use] extern crate lazy_static; +#[cfg(feature = "i128_blob")] +extern crate byteorder; + use std::cell::RefCell; use std::convert; use std::default::Default; diff --git a/src/row.rs b/src/row.rs index 60ebe58..0f7b010 100644 --- a/src/row.rs +++ b/src/row.rs @@ -161,6 +161,10 @@ impl<'a, 'stmt> Row<'a, 'stmt> { /// /// Returns an `Error::InvalidColumnName` if `idx` is not a valid column /// 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(&self, idx: I) -> Result { let idx = try!(idx.idx(self.stmt)); let value = self.stmt.value_ref(idx); @@ -170,6 +174,10 @@ impl<'a, 'stmt> Row<'a, 'stmt> { FromSqlError::Other(err) => { Error::FromSqlConversionFailure(idx as usize, value.data_type(), err) } + #[cfg(feature = "i128_blob")] + FromSqlError::InvalidI128Size(_) => { + Error::InvalidColumnType(idx, value.data_type()) + } }) } diff --git a/src/types/from_sql.rs b/src/types/from_sql.rs index 5ec139b..7145173 100644 --- a/src/types/from_sql.rs +++ b/src/types/from_sql.rs @@ -13,6 +13,11 @@ pub enum FromSqlError { /// requested type. 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. Other(Box), } @@ -22,6 +27,9 @@ impl fmt::Display for FromSqlError { 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), FromSqlError::Other(ref err) => err.fmt(f), } } @@ -32,6 +40,9 @@ impl Error for FromSqlError { match *self { FromSqlError::InvalidType => "invalid type", FromSqlError::OutOfRange(_) => "value out of range", + #[cfg(feature = "i128_blob")] + FromSqlError::InvalidI128Size(_) => + "unexpected blob size for 128bit value", FromSqlError::Other(ref err) => err.description(), } } @@ -41,6 +52,8 @@ impl Error for FromSqlError { match *self { FromSqlError::Other(ref err) => err.cause(), FromSqlError::InvalidType | FromSqlError::OutOfRange(_) => None, + #[cfg(feature = "i128_blob")] + FromSqlError::InvalidI128Size(_) => None, } } } @@ -135,6 +148,21 @@ impl FromSql for Vec { } } +#[cfg(feature = "i128_blob")] +impl FromSql for i128 { + fn column_result(value: ValueRef) -> FromSqlResult { + 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 FromSql for Option { fn column_result(value: ValueRef) -> FromSqlResult { match value { diff --git a/src/types/to_sql.rs b/src/types/to_sql.rs index 65ffe39..799f607 100644 --- a/src/types/to_sql.rs +++ b/src/types/to_sql.rs @@ -59,6 +59,12 @@ from_value!(u32); from_value!(f64); from_value!(Vec); +// It would be nice if we could avoid the heap allocation (of the `Vec`) that +// `i128` needs in `Into`, 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> { fn to_sql(&self) -> Result { Ok(match *self { @@ -112,6 +118,9 @@ to_sql_self!(u16); to_sql_self!(u32); to_sql_self!(f64); +#[cfg(feature = "i128_blob")] +to_sql_self!(i128); + impl<'a, T: ?Sized> ToSql for &'a T where T: ToSql, @@ -194,4 +203,38 @@ mod test { let r = cow.to_sql(); 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::, _>>().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()), + ]); + } } diff --git a/src/types/value.rs b/src/types/value.rs index a1fa04f..21b8759 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -36,6 +36,18 @@ impl From for Value { } } +#[cfg(feature = "i128_blob")] +impl From 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( ($t:ty) => ( impl From<$t> for Value {