From 03bc69f8159476752c63841fcd1386fe66569f2d Mon Sep 17 00:00:00 2001 From: gwenn Date: Mon, 1 Feb 2016 20:42:50 +0100 Subject: [PATCH 1/6] Introduce exists and insert convenient methods --- src/convenient.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 9 ++++++ 2 files changed, 81 insertions(+) create mode 100644 src/convenient.rs diff --git a/src/convenient.rs b/src/convenient.rs new file mode 100644 index 0000000..202c964 --- /dev/null +++ b/src/convenient.rs @@ -0,0 +1,72 @@ +use super::ffi; + +use {Error, Result, Statement}; +use types::ToSql; + +impl<'conn> Statement<'conn> { + /// Execute an INSERT and return the ROWID. + /// + /// # Failure + /// Will return `Err` if no row is inserted or many rows are inserted. + pub fn insert(&mut self, params: &[&ToSql]) -> Result { + let changes = try!(self.execute(params)); + match changes { + 1 => Ok(self.conn.last_insert_rowid()), + _ => Err(Error::QueryInsertedRows(changes)) + } + } + + /// Return `true` if a query in the SQL statement it executes returns one or more rows + /// and `false` if the SQL returns an empty set. + pub fn exists(&mut self, params: &[&ToSql]) -> Result { + self.reset_if_needed(); + unsafe { + try!(self.bind_parameters(params)); + let r = ffi::sqlite3_step(self.stmt); + ffi::sqlite3_reset(self.stmt); + match r { + ffi::SQLITE_DONE => Ok(false), + ffi::SQLITE_ROW => Ok(true), + _ => Err(self.conn.decode_result(r).unwrap_err()), + } + } + } +} + +#[cfg(test)] +mod test { + use {Connection, Error}; + + #[test] + fn test_insert() { + let db = Connection::open_in_memory().unwrap(); + db.execute_batch("CREATE TABLE foo(x INTEGER UNIQUE)").unwrap(); + let mut stmt = db.prepare("INSERT OR IGNORE INTO foo (x) VALUES (?)").unwrap(); + assert_eq!(stmt.insert(&[&1i32]).unwrap(), 1); + assert_eq!(stmt.insert(&[&2i32]).unwrap(), 2); + match stmt.insert(&[&1i32]).unwrap_err() { + Error::QueryInsertedRows(0) => (), + err => panic!("Unexpected error {}", err), + } + let mut multi = db.prepare("INSERT INTO foo (x) SELECT 3 UNION ALL SELECT 4").unwrap(); + match multi.insert(&[]).unwrap_err() { + Error::QueryInsertedRows(2) => (), + err => panic!("Unexpected error {}", err), + } + } + + #[test] + fn test_exists() { + let db = Connection::open_in_memory().unwrap(); + let sql = "BEGIN; + CREATE TABLE foo(x INTEGER); + INSERT INTO foo VALUES(1); + INSERT INTO foo VALUES(2); + END;"; + db.execute_batch(sql).unwrap(); + let mut stmt = db.prepare("SELECT 1 FROM foo WHERE x = ?").unwrap(); + assert!(stmt.exists(&[&1i32]).unwrap()); + assert!(stmt.exists(&[&2i32]).unwrap()); + assert!(!stmt.exists(&[&0i32]).unwrap()); + } +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index ba93df8..2f7ce45 100644 --- a/src/error.rs +++ b/src/error.rs @@ -66,6 +66,9 @@ pub enum Error { #[cfg(feature = "functions")] #[allow(dead_code)] UserFunctionError(Box), + + /// Error when a query that was expected to insert one row did not insert any or insert many. + QueryInsertedRows(c_int), } impl From for Error { @@ -102,6 +105,8 @@ impl fmt::Display for Error { &Error::InvalidFunctionParameterType => write!(f, "Invalid function parameter type"), #[cfg(feature = "functions")] &Error::UserFunctionError(ref err) => err.fmt(f), + + &Error::QueryInsertedRows(i) => write!(f, "Query inserted {} rows", i), } } } @@ -128,6 +133,8 @@ impl error::Error for Error { &Error::InvalidFunctionParameterType => "invalid function parameter type", #[cfg(feature = "functions")] &Error::UserFunctionError(ref err) => err.description(), + + &Error::QueryInsertedRows(_) => "query inserted zero or more than one row", } } @@ -151,6 +158,8 @@ impl error::Error for Error { &Error::InvalidFunctionParameterType => None, #[cfg(feature = "functions")] &Error::UserFunctionError(ref err) => Some(&**err), + + &Error::QueryInsertedRows(_) => None, } } } From 82f1467a9ff63d12ce5d63e3086ace5483ce7828 Mon Sep 17 00:00:00 2001 From: gwenn Date: Tue, 2 Feb 2016 18:57:25 +0100 Subject: [PATCH 2/6] Add declaration for convenient module --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index d4b445e..7c6db92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,7 @@ mod error; #[cfg(feature = "backup")]pub mod backup; #[cfg(feature = "functions")] pub mod functions; #[cfg(feature = "blob")] pub mod blob; +mod convenient; /// Old name for `Result`. `SqliteResult` is deprecated. pub type SqliteResult = Result; From 7b174c97f868ee41d8348c126195933a4153d667 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 16 May 2016 11:15:07 -0500 Subject: [PATCH 3/6] Add sanity check for `insert` that does not do an insertion --- src/convenient.rs | 31 ++++++++++++++++++++++++++----- src/error.rs | 15 +++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/convenient.rs b/src/convenient.rs index 202c964..4f24ddd 100644 --- a/src/convenient.rs +++ b/src/convenient.rs @@ -9,10 +9,16 @@ impl<'conn> Statement<'conn> { /// # Failure /// Will return `Err` if no row is inserted or many rows are inserted. pub fn insert(&mut self, params: &[&ToSql]) -> Result { + // Some non-insertion queries could still return 1 change (an UPDATE, for example), so + // to guard against that we can check that the connection's last_insert_rowid() changes + // after we execute the statement. + let prev_rowid = self.conn.last_insert_rowid(); let changes = try!(self.execute(params)); + let new_rowid = self.conn.last_insert_rowid(); match changes { - 1 => Ok(self.conn.last_insert_rowid()), - _ => Err(Error::QueryInsertedRows(changes)) + 1 if prev_rowid != new_rowid => Ok(new_rowid), + 1 if prev_rowid == new_rowid => Err(Error::StatementFailedToInsertRow), + _ => Err(Error::StatementChangedRows(changes)) } } @@ -45,16 +51,31 @@ mod test { assert_eq!(stmt.insert(&[&1i32]).unwrap(), 1); assert_eq!(stmt.insert(&[&2i32]).unwrap(), 2); match stmt.insert(&[&1i32]).unwrap_err() { - Error::QueryInsertedRows(0) => (), + Error::StatementChangedRows(0) => (), err => panic!("Unexpected error {}", err), } let mut multi = db.prepare("INSERT INTO foo (x) SELECT 3 UNION ALL SELECT 4").unwrap(); match multi.insert(&[]).unwrap_err() { - Error::QueryInsertedRows(2) => (), + Error::StatementChangedRows(2) => (), err => panic!("Unexpected error {}", err), } } + #[test] + fn test_insert_failures() { + let db = Connection::open_in_memory().unwrap(); + db.execute_batch("CREATE TABLE foo(x INTEGER UNIQUE)").unwrap(); + let mut insert = db.prepare("INSERT INTO foo (x) VALUES (?)").unwrap(); + let mut update = db.prepare("UPDATE foo SET x = ?").unwrap(); + + assert_eq!(insert.insert(&[&1i32]).unwrap(), 1); + + match update.insert(&[&2i32]) { + Err(Error::StatementFailedToInsertRow) => (), + r => panic!("Unexpected result {:?}", r), + } + } + #[test] fn test_exists() { let db = Connection::open_in_memory().unwrap(); @@ -69,4 +90,4 @@ mod test { assert!(stmt.exists(&[&2i32]).unwrap()); assert!(!stmt.exists(&[&0i32]).unwrap()); } -} \ No newline at end of file +} diff --git a/src/error.rs b/src/error.rs index bc6d7ae..f277776 100644 --- a/src/error.rs +++ b/src/error.rs @@ -57,7 +57,11 @@ pub enum Error { InvalidColumnType, /// Error when a query that was expected to insert one row did not insert any or insert many. - QueryInsertedRows(c_int), + StatementChangedRows(c_int), + + /// Error when a query that was expected to insert a row did not change the connection's + /// last_insert_rowid(). + StatementFailedToInsertRow, /// Error returned by `functions::Context::get` when the function argument cannot be converted /// to the requested type. @@ -105,7 +109,8 @@ impl fmt::Display for Error { Error::InvalidColumnIndex(i) => write!(f, "Invalid column index: {}", i), Error::InvalidColumnName(ref name) => write!(f, "Invalid column name: {}", name), Error::InvalidColumnType => write!(f, "Invalid column type"), - Error::QueryInsertedRows(i) => write!(f, "Query inserted {} rows", i), + Error::StatementChangedRows(i) => write!(f, "Query changed {} rows", i), + Error::StatementFailedToInsertRow => write!(f, "Statement failed to insert new row"), #[cfg(feature = "functions")] Error::InvalidFunctionParameterType => write!(f, "Invalid function parameter type"), @@ -136,7 +141,8 @@ impl error::Error for Error { Error::InvalidColumnIndex(_) => "invalid column index", Error::InvalidColumnName(_) => "invalid column name", Error::InvalidColumnType => "invalid column type", - Error::QueryInsertedRows(_) => "query inserted zero or more than one row", + Error::StatementChangedRows(_) => "query inserted zero or more than one row", + Error::StatementFailedToInsertRow => "statement failed to insert new row", #[cfg(feature = "functions")] Error::InvalidFunctionParameterType => "invalid function parameter type", @@ -162,7 +168,8 @@ impl error::Error for Error { Error::InvalidColumnName(_) | Error::InvalidColumnType | Error::InvalidPath(_) => None, - Error::QueryInsertedRows(_) => None, + Error::StatementChangedRows(_) => None, + Error::StatementFailedToInsertRow => None, #[cfg(feature = "functions")] Error::InvalidFunctionParameterType => None, From 493446e6d11af3e3197213fa7b9a06dad6d68944 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 16 May 2016 11:18:15 -0500 Subject: [PATCH 4/6] Implement `exists` using `query` instead of FFI. --- src/convenient.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/convenient.rs b/src/convenient.rs index 4f24ddd..3e8750c 100644 --- a/src/convenient.rs +++ b/src/convenient.rs @@ -1,5 +1,3 @@ -use super::ffi; - use {Error, Result, Statement}; use types::ToSql; @@ -26,15 +24,10 @@ impl<'conn> Statement<'conn> { /// and `false` if the SQL returns an empty set. pub fn exists(&mut self, params: &[&ToSql]) -> Result { self.reset_if_needed(); - unsafe { - try!(self.bind_parameters(params)); - let r = ffi::sqlite3_step(self.stmt); - ffi::sqlite3_reset(self.stmt); - match r { - ffi::SQLITE_DONE => Ok(false), - ffi::SQLITE_ROW => Ok(true), - _ => Err(self.conn.decode_result(r).unwrap_err()), - } + let mut rows = try!(self.query(params)); + match rows.next() { + Some(_) => Ok(true), + None => Ok(false), } } } From 757a1f40dc2341d914a99dfbe06d9483c72bafdd Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 16 May 2016 11:18:38 -0500 Subject: [PATCH 5/6] rustfmt --- src/convenient.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/convenient.rs b/src/convenient.rs index 3e8750c..b7102f8 100644 --- a/src/convenient.rs +++ b/src/convenient.rs @@ -16,7 +16,7 @@ impl<'conn> Statement<'conn> { match changes { 1 if prev_rowid != new_rowid => Ok(new_rowid), 1 if prev_rowid == new_rowid => Err(Error::StatementFailedToInsertRow), - _ => Err(Error::StatementChangedRows(changes)) + _ => Err(Error::StatementChangedRows(changes)), } } From 75fcfb4d5223f07b1b868f628444353d3d80fb2a Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 16 May 2016 11:19:38 -0500 Subject: [PATCH 6/6] Add `insert` and `exists` to Changelog. --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index 9e6729a..1a95fdc 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,7 @@ # Version UPCOMING (...) +* Adds `insert` convenience method to `Statement` which returns the row ID of an inserted row. +* Adds `exists` convenience method returning whether a query finds one or more rows. * Adds support for serializing types from the `serde_json` crate. Requires the `serde_json` feature. * Adds support for serializing types from the `chrono` crate. Requires the `chrono` feature. * Removes `load_extension` feature from `libsqlite3-sys`. `load_extension` is still available