From 8bb624ccccf9d194521b922a652b34b9d4235f88 Mon Sep 17 00:00:00 2001 From: Gwenael Treguier Date: Tue, 4 Aug 2015 18:52:57 +0200 Subject: [PATCH 01/18] Factorize code --- src/lib.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f98780b..3dd84ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -614,14 +614,7 @@ impl<'conn> SqliteStatement<'conn> { self.reset_if_needed(); unsafe { - assert!(params.len() as c_int == ffi::sqlite3_bind_parameter_count(self.stmt), - "incorrect number of parameters to execute(): expected {}, got {}", - ffi::sqlite3_bind_parameter_count(self.stmt), - params.len()); - - for (i, p) in params.iter().enumerate() { - try!(self.conn.decode_result(p.bind_parameter(self.stmt, (i + 1) as c_int))); - } + try!(self.bind_parameters(params)); self.needs_reset = true; let r = ffi::sqlite3_step(self.stmt); @@ -660,11 +653,12 @@ impl<'conn> SqliteStatement<'conn> { try!(self.bind_parameters(params)); } + self.needs_reset = true; Ok(SqliteRows::new(self)) } /// Executes the prepared statement and maps a function over the resulting - /// rows. + /// rows. /// /// Unlike the iterator produced by `query`, the returned iterator does not expose the possibility /// for accessing stale rows. @@ -698,8 +692,6 @@ impl<'conn> SqliteStatement<'conn> { try!(self.conn.decode_result(p.bind_parameter(self.stmt, (i + 1) as c_int))); } - self.needs_reset = true; - Ok(()) } From e81757e86d6da929b7458e3282ca7e007fe50d8f Mon Sep 17 00:00:00 2001 From: Gwenael Treguier Date: Tue, 4 Aug 2015 21:48:54 +0200 Subject: [PATCH 02/18] Add Stmt::named_execute and Stmt::parameter_index. --- Cargo.toml | 1 + src/lib.rs | 20 ++++++++----- src/named_params.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 src/named_params.rs diff --git a/Cargo.toml b/Cargo.toml index 5d05ed5..18c2bd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ name = "rusqlite" [features] load_extension = ["libsqlite3-sys/load_extension"] +named_params = [] [dependencies] time = "~0.1.0" diff --git a/src/lib.rs b/src/lib.rs index 3dd84ae..f2d1791 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,6 +79,7 @@ pub use transaction::{SqliteTransactionBehavior, pub mod types; mod transaction; #[cfg(feature = "load_extension")] mod load_extension_guard; +#[cfg(feature = "named_params")] pub mod named_params; /// A typedef of the result returned by many methods. pub type SqliteResult = Result; @@ -615,15 +616,18 @@ impl<'conn> SqliteStatement<'conn> { unsafe { try!(self.bind_parameters(params)); + self.execute_() + } + } - self.needs_reset = true; - let r = ffi::sqlite3_step(self.stmt); - match r { - ffi::SQLITE_DONE => Ok(self.conn.changes()), - ffi::SQLITE_ROW => Err(SqliteError{ code: r, - message: "Unexpected row result - did you mean to call query?".to_string() }), - _ => Err(self.conn.decode_result(r).unwrap_err()), - } + unsafe fn execute_(&mut self) -> SqliteResult { + let r = ffi::sqlite3_step(self.stmt); + ffi::sqlite3_reset(self.stmt); + match r { + ffi::SQLITE_DONE => Ok(self.conn.changes()), + ffi::SQLITE_ROW => Err(SqliteError{ code: r, + message: "Unexpected row result - did you mean to call query?".to_string() }), + _ => Err(self.conn.decode_result(r).unwrap_err()), } } diff --git a/src/named_params.rs b/src/named_params.rs new file mode 100644 index 0000000..b8e78fa --- /dev/null +++ b/src/named_params.rs @@ -0,0 +1,72 @@ +//use std::collections::HashMap; +use std::ffi::{CString}; +use libc::{c_int}; + +use super::ffi; + +use {SqliteResult, SqliteError, SqliteConnection, SqliteStatement}; +use types::{ToSql}; + +impl SqliteConnection { +} + +impl<'conn> SqliteStatement<'conn> { + /*pub fn parameter_names(&self) -> HashMap { + let n = unsafe { ffi::sqlite3_bind_parameter_count(self.stmt) }; + let mut index_by_name = HashMap::with_capacity(n as usize); + for i in 1..n+1 { + let c_name = unsafe { ffi::sqlite3_bind_parameter_name(self.stmt, i) }; + if !c_name.is_null() { + let c_slice = unsafe { CStr::from_ptr(c_name).to_bytes() }; + index_by_name.insert(str::from_utf8(c_slice).unwrap().to_string(), n); + } + } + index_by_name + }*/ + + /// Return the index of an SQL parameter given its name. + /// Return None if `name` is invalid (NulError) or if no matching parameter is found. + pub fn parameter_index(&self, name: &str) -> Option { + unsafe { + CString::new(name).ok().and_then(|c_name| + match ffi::sqlite3_bind_parameter_index(self.stmt, c_name.as_ptr()) { + 0 => None, // A zero is returned if no matching parameter is found. + n => Some(n) + } + ) + + } + } + + /// Execute the prepared statement with named parameter(s). + /// + /// On success, returns the number of rows that were changed or inserted or deleted (via + /// `sqlite3_changes`). + pub fn named_execute(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult { + unsafe { + for &(name, value) in params { + let i = try!(self.parameter_index(name).ok_or(SqliteError{ + code: ffi::SQLITE_MISUSE, + message: format!("Invalid parameter name: {}", name) + })); + try!(self.conn.decode_result(value.bind_parameter(self.stmt, i))); + } + self.execute_() + } + } +} + +#[cfg(test)] +mod test { + use SqliteConnection; + + #[test] + fn test_named_execute() { + let db = SqliteConnection::open_in_memory().unwrap(); + let sql = "CREATE TABLE test (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, flag INTEGER)"; + db.execute_batch(sql).unwrap(); + + let mut stmt = db.prepare("INSERT INTO test (id, name, flag) VALUES (:id, :name, :flag)").unwrap(); + stmt.named_execute(&[(":name", &"one")]).unwrap(); + } +} \ No newline at end of file From 7ff9d4056ba612b0014ee28c2386ceb7f8557428 Mon Sep 17 00:00:00 2001 From: Gwenael Treguier Date: Wed, 5 Aug 2015 22:07:49 +0200 Subject: [PATCH 03/18] Add SqliteStatement.named_query. --- src/named_params.rs | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/src/named_params.rs b/src/named_params.rs index b8e78fa..e196830 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -4,7 +4,7 @@ use libc::{c_int}; use super::ffi; -use {SqliteResult, SqliteError, SqliteConnection, SqliteStatement}; +use {SqliteResult, SqliteError, SqliteConnection, SqliteStatement, SqliteRows}; use types::{ToSql}; impl SqliteConnection { @@ -44,16 +44,33 @@ impl<'conn> SqliteStatement<'conn> { /// `sqlite3_changes`). pub fn named_execute(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult { unsafe { - for &(name, value) in params { - let i = try!(self.parameter_index(name).ok_or(SqliteError{ - code: ffi::SQLITE_MISUSE, - message: format!("Invalid parameter name: {}", name) - })); - try!(self.conn.decode_result(value.bind_parameter(self.stmt, i))); - } + try!(self.bind_named_parameters(params)); self.execute_() } } + + /// Execute the prepared statement with named parameter(s), returning an iterator over the resulting rows. + pub fn named_query<'a>(&'a mut self, params: &[(&str, &ToSql)]) -> SqliteResult> { + self.reset_if_needed(); + + unsafe { + try!(self.bind_named_parameters(params)); + } + + self.needs_reset = true; + Ok(SqliteRows::new(self)) + } + + unsafe fn bind_named_parameters(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult<()> { + for &(name, value) in params { + let i = try!(self.parameter_index(name).ok_or(SqliteError{ + code: ffi::SQLITE_MISUSE, + message: format!("Invalid parameter name: {}", name) + })); + try!(self.conn.decode_result(value.bind_parameter(self.stmt, i))); + } + Ok(()) + } } #[cfg(test)] @@ -69,4 +86,14 @@ mod test { let mut stmt = db.prepare("INSERT INTO test (id, name, flag) VALUES (:id, :name, :flag)").unwrap(); stmt.named_execute(&[(":name", &"one")]).unwrap(); } + + #[test] + fn test_named_query() { + let db = SqliteConnection::open_in_memory().unwrap(); + let sql = "CREATE TABLE test (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, flag INTEGER)"; + db.execute_batch(sql).unwrap(); + + let mut stmt = db.prepare("SELECT * FROM test where name = :name").unwrap(); + stmt.named_query(&[(":name", &"one")]).unwrap(); + } } \ No newline at end of file From 3b673a74b2c80f7520d56f5cc7eda322023aabe1 Mon Sep 17 00:00:00 2001 From: Gwenael Treguier Date: Fri, 7 Aug 2015 02:19:57 +0200 Subject: [PATCH 04/18] Add SqliteConnection.execute_named,query_named_row. --- src/named_params.rs | 46 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/named_params.rs b/src/named_params.rs index e196830..b9a5410 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -4,10 +4,31 @@ use libc::{c_int}; use super::ffi; -use {SqliteResult, SqliteError, SqliteConnection, SqliteStatement, SqliteRows}; +use {SqliteResult, SqliteError, SqliteConnection, SqliteStatement, SqliteRows, SqliteRow}; use types::{ToSql}; impl SqliteConnection { + /// Convenience method to prepare and execute a single SQL statement with named parameter(s). + pub fn execute_named(&self, sql: &str, params: &[(&str, &ToSql)]) -> SqliteResult { + self.prepare(sql).and_then(|mut stmt| stmt.execute_named(params)) + } + + /// Convenience method to execute a query with named parameter(s) that is expected to return a single row. + /// + /// If the query returns more than one row, all rows except the first are ignored. + pub fn query_named_row(&self, sql: &str, params: &[(&str, &ToSql)], f: F) -> SqliteResult + where F: FnOnce(SqliteRow) -> T { + let mut stmt = try!(self.prepare(sql)); + let mut rows = try!(stmt.query_named(params)); + + match rows.next() { + Some(row) => row.map(f), + None => Err(SqliteError{ + code: ffi::SQLITE_NOTICE, + message: "Query did not return a row".to_string(), + }) + } + } } impl<'conn> SqliteStatement<'conn> { @@ -42,7 +63,7 @@ impl<'conn> SqliteStatement<'conn> { /// /// On success, returns the number of rows that were changed or inserted or deleted (via /// `sqlite3_changes`). - pub fn named_execute(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult { + pub fn execute_named(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult { unsafe { try!(self.bind_named_parameters(params)); self.execute_() @@ -50,7 +71,7 @@ impl<'conn> SqliteStatement<'conn> { } /// Execute the prepared statement with named parameter(s), returning an iterator over the resulting rows. - pub fn named_query<'a>(&'a mut self, params: &[(&str, &ToSql)]) -> SqliteResult> { + pub fn query_named<'a>(&'a mut self, params: &[(&str, &ToSql)]) -> SqliteResult> { self.reset_if_needed(); unsafe { @@ -77,23 +98,34 @@ impl<'conn> SqliteStatement<'conn> { mod test { use SqliteConnection; + #[test] + fn test_execute_named() { + let db = SqliteConnection::open_in_memory().unwrap(); + db.execute_batch("CREATE TABLE foo(x INTEGER)").unwrap(); + + assert_eq!(db.execute_named("INSERT INTO foo(x) VALUES (:x)", &[(":x", &1i32)]).unwrap(), 1); + assert_eq!(db.execute_named("INSERT INTO foo(x) VALUES (:x)", &[(":x", &2i32)]).unwrap(), 1); + + assert_eq!(3i32, db.query_named_row("SELECT SUM(x) FROM foo WHERE x > :x", &[(":x", &0i32)], |r| r.get(0)).unwrap()); + } + #[test] - fn test_named_execute() { + fn test_stmt_execute_named() { let db = SqliteConnection::open_in_memory().unwrap(); let sql = "CREATE TABLE test (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, flag INTEGER)"; db.execute_batch(sql).unwrap(); let mut stmt = db.prepare("INSERT INTO test (id, name, flag) VALUES (:id, :name, :flag)").unwrap(); - stmt.named_execute(&[(":name", &"one")]).unwrap(); + stmt.execute_named(&[(":name", &"one")]).unwrap(); } #[test] - fn test_named_query() { + fn test_query_named() { let db = SqliteConnection::open_in_memory().unwrap(); let sql = "CREATE TABLE test (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, flag INTEGER)"; db.execute_batch(sql).unwrap(); let mut stmt = db.prepare("SELECT * FROM test where name = :name").unwrap(); - stmt.named_query(&[(":name", &"one")]).unwrap(); + stmt.query_named(&[(":name", &"one")]).unwrap(); } } \ No newline at end of file From eb7f670ce1bc2a231419c77328619abb554c436a Mon Sep 17 00:00:00 2001 From: Gwenael Treguier Date: Sat, 8 Aug 2015 16:19:05 +0200 Subject: [PATCH 05/18] Make named_params module private --- src/lib.rs | 2 +- src/named_params.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index f2d1791..82c7604 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,7 +79,7 @@ pub use transaction::{SqliteTransactionBehavior, pub mod types; mod transaction; #[cfg(feature = "load_extension")] mod load_extension_guard; -#[cfg(feature = "named_params")] pub mod named_params; +#[cfg(feature = "named_params")] mod named_params; /// A typedef of the result returned by many methods. pub type SqliteResult = Result; diff --git a/src/named_params.rs b/src/named_params.rs index b9a5410..32f9732 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -46,6 +46,8 @@ impl<'conn> SqliteStatement<'conn> { }*/ /// Return the index of an SQL parameter given its name. + /// + /// ## Failures /// Return None if `name` is invalid (NulError) or if no matching parameter is found. pub fn parameter_index(&self, name: &str) -> Option { unsafe { From ddd976c158ed456d078ebf1066bba4768fbc4f70 Mon Sep 17 00:00:00 2001 From: Gwenael Treguier Date: Wed, 11 Nov 2015 14:39:54 +0100 Subject: [PATCH 06/18] Cleanup use statements. --- src/named_params.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/named_params.rs b/src/named_params.rs index 32f9732..e103bf7 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -1,11 +1,11 @@ //use std::collections::HashMap; -use std::ffi::{CString}; -use libc::{c_int}; +use std::ffi::CString; +use libc::c_int; use super::ffi; use {SqliteResult, SqliteError, SqliteConnection, SqliteStatement, SqliteRows, SqliteRow}; -use types::{ToSql}; +use types::ToSql; impl SqliteConnection { /// Convenience method to prepare and execute a single SQL statement with named parameter(s). @@ -130,4 +130,4 @@ mod test { let mut stmt = db.prepare("SELECT * FROM test where name = :name").unwrap(); stmt.query_named(&[(":name", &"one")]).unwrap(); } -} \ No newline at end of file +} From da69584b9f26c2191aecfd6332358df70ba4244b Mon Sep 17 00:00:00 2001 From: Gwenael Treguier Date: Sat, 5 Dec 2015 13:43:03 +0100 Subject: [PATCH 07/18] Add Example and Failure documentation --- src/named_params.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/named_params.rs b/src/named_params.rs index e103bf7..eb70989 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -9,6 +9,20 @@ use types::ToSql; impl SqliteConnection { /// Convenience method to prepare and execute a single SQL statement with named parameter(s). + /// + /// ## Example + /// + /// ```rust,no_run + /// # use rusqlite::{SqliteConnection, SqliteResult}; + /// fn insert(conn: &SqliteConnection) -> SqliteResult { + /// conn.execute_named("INSERT INTO test (id, name, flag) VALUES (:id, :name, :flag)", &[(":name", &"one")]) + /// } + /// ``` + /// + /// # Failure + /// + /// Will return `Err` if `sql` cannot be converted to a C-compatible string or if the + /// underlying SQLite call fails. pub fn execute_named(&self, sql: &str, params: &[(&str, &ToSql)]) -> SqliteResult { self.prepare(sql).and_then(|mut stmt| stmt.execute_named(params)) } @@ -16,6 +30,11 @@ impl SqliteConnection { /// Convenience method to execute a query with named parameter(s) that is expected to return a single row. /// /// If the query returns more than one row, all rows except the first are ignored. + /// + /// # Failure + /// + /// Will return `Err` if `sql` cannot be converted to a C-compatible string or if the + /// underlying SQLite call fails. pub fn query_named_row(&self, sql: &str, params: &[(&str, &ToSql)], f: F) -> SqliteResult where F: FnOnce(SqliteRow) -> T { let mut stmt = try!(self.prepare(sql)); @@ -47,7 +66,8 @@ impl<'conn> SqliteStatement<'conn> { /// Return the index of an SQL parameter given its name. /// - /// ## Failures + /// # Failure + /// /// Return None if `name` is invalid (NulError) or if no matching parameter is found. pub fn parameter_index(&self, name: &str) -> Option { unsafe { @@ -65,6 +85,21 @@ impl<'conn> SqliteStatement<'conn> { /// /// On success, returns the number of rows that were changed or inserted or deleted (via /// `sqlite3_changes`). + /// + /// ## Example + /// + /// ```rust,no_run + /// # use rusqlite::{SqliteConnection, SqliteResult}; + /// fn insert(conn: &SqliteConnection) -> SqliteResult { + /// let mut stmt = try!(conn.prepare("INSERT INTO test (id, name, flag) VALUES (:id, :name, :flag)")); + /// return stmt.execute_named(&[(":name", &"one")]); + /// } + /// ``` + /// + /// # Failure + /// + /// Will return `Err` if binding parameters fails, the executed statement returns rows (in + /// which case `query` should be used instead), or the underling SQLite call fails. pub fn execute_named(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult { unsafe { try!(self.bind_named_parameters(params)); @@ -73,6 +108,24 @@ impl<'conn> SqliteStatement<'conn> { } /// Execute the prepared statement with named parameter(s), returning an iterator over the resulting rows. + /// + /// ## Example + /// + /// ```rust,no_run + /// # use rusqlite::{SqliteConnection, SqliteResult, SqliteRows}; + /// fn query(conn: &SqliteConnection) -> SqliteResult<()> { + /// let mut stmt = try!(conn.prepare("SELECT * FROM test where name = :name")); + /// let mut rows = try!(stmt.query_named(&[(":name", &"one")])); + /// for row in rows { + /// // ... + /// } + /// return Ok(()) + /// } + /// ``` + /// + /// # Failure + /// + /// Will return `Err` if binding parameters fails. pub fn query_named<'a>(&'a mut self, params: &[(&str, &ToSql)]) -> SqliteResult> { self.reset_if_needed(); From ab6ab3b2e90c25608c81f38d7913d28ecde084db Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Thu, 10 Dec 2015 17:01:19 -0500 Subject: [PATCH 08/18] Run rustfmt on named_params.rs --- src/named_params.rs | 84 +++++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 33 deletions(-) diff --git a/src/named_params.rs b/src/named_params.rs index eb70989..dd9cc2f 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -1,4 +1,4 @@ -//use std::collections::HashMap; +// use std::collections::HashMap; use std::ffi::CString; use libc::c_int; @@ -35,34 +35,41 @@ impl SqliteConnection { /// /// Will return `Err` if `sql` cannot be converted to a C-compatible string or if the /// underlying SQLite call fails. - pub fn query_named_row(&self, sql: &str, params: &[(&str, &ToSql)], f: F) -> SqliteResult - where F: FnOnce(SqliteRow) -> T { + pub fn query_named_row(&self, + sql: &str, + params: &[(&str, &ToSql)], + f: F) + -> SqliteResult + where F: FnOnce(SqliteRow) -> T + { let mut stmt = try!(self.prepare(sql)); let mut rows = try!(stmt.query_named(params)); match rows.next() { Some(row) => row.map(f), - None => Err(SqliteError{ - code: ffi::SQLITE_NOTICE, - message: "Query did not return a row".to_string(), - }) + None => { + Err(SqliteError { + code: ffi::SQLITE_NOTICE, + message: "Query did not return a row".to_string(), + }) + } } } } impl<'conn> SqliteStatement<'conn> { - /*pub fn parameter_names(&self) -> HashMap { - let n = unsafe { ffi::sqlite3_bind_parameter_count(self.stmt) }; - let mut index_by_name = HashMap::with_capacity(n as usize); - for i in 1..n+1 { - let c_name = unsafe { ffi::sqlite3_bind_parameter_name(self.stmt, i) }; - if !c_name.is_null() { - let c_slice = unsafe { CStr::from_ptr(c_name).to_bytes() }; - index_by_name.insert(str::from_utf8(c_slice).unwrap().to_string(), n); - } - } - index_by_name - }*/ + // pub fn parameter_names(&self) -> HashMap { + // let n = unsafe { ffi::sqlite3_bind_parameter_count(self.stmt) }; + // let mut index_by_name = HashMap::with_capacity(n as usize); + // for i in 1..n+1 { + // let c_name = unsafe { ffi::sqlite3_bind_parameter_name(self.stmt, i) }; + // if !c_name.is_null() { + // let c_slice = unsafe { CStr::from_ptr(c_name).to_bytes() }; + // index_by_name.insert(str::from_utf8(c_slice).unwrap().to_string(), n); + // } + // } + // index_by_name + // } /// Return the index of an SQL parameter given its name. /// @@ -71,12 +78,12 @@ impl<'conn> SqliteStatement<'conn> { /// Return None if `name` is invalid (NulError) or if no matching parameter is found. pub fn parameter_index(&self, name: &str) -> Option { unsafe { - CString::new(name).ok().and_then(|c_name| + CString::new(name).ok().and_then(|c_name| { match ffi::sqlite3_bind_parameter_index(self.stmt, c_name.as_ptr()) { 0 => None, // A zero is returned if no matching parameter is found. - n => Some(n) + n => Some(n), } - ) + }) } } @@ -126,7 +133,9 @@ impl<'conn> SqliteStatement<'conn> { /// # Failure /// /// Will return `Err` if binding parameters fails. - pub fn query_named<'a>(&'a mut self, params: &[(&str, &ToSql)]) -> SqliteResult> { + pub fn query_named<'a>(&'a mut self, + params: &[(&str, &ToSql)]) + -> SqliteResult> { self.reset_if_needed(); unsafe { @@ -139,9 +148,9 @@ impl<'conn> SqliteStatement<'conn> { unsafe fn bind_named_parameters(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult<()> { for &(name, value) in params { - let i = try!(self.parameter_index(name).ok_or(SqliteError{ + let i = try!(self.parameter_index(name).ok_or(SqliteError { code: ffi::SQLITE_MISUSE, - message: format!("Invalid parameter name: {}", name) + message: format!("Invalid parameter name: {}", name), })); try!(self.conn.decode_result(value.bind_parameter(self.stmt, i))); } @@ -158,26 +167,35 @@ mod test { let db = SqliteConnection::open_in_memory().unwrap(); db.execute_batch("CREATE TABLE foo(x INTEGER)").unwrap(); - assert_eq!(db.execute_named("INSERT INTO foo(x) VALUES (:x)", &[(":x", &1i32)]).unwrap(), 1); - assert_eq!(db.execute_named("INSERT INTO foo(x) VALUES (:x)", &[(":x", &2i32)]).unwrap(), 1); + assert_eq!(db.execute_named("INSERT INTO foo(x) VALUES (:x)", &[(":x", &1i32)]).unwrap(), + 1); + assert_eq!(db.execute_named("INSERT INTO foo(x) VALUES (:x)", &[(":x", &2i32)]).unwrap(), + 1); - assert_eq!(3i32, db.query_named_row("SELECT SUM(x) FROM foo WHERE x > :x", &[(":x", &0i32)], |r| r.get(0)).unwrap()); + assert_eq!(3i32, + db.query_named_row("SELECT SUM(x) FROM foo WHERE x > :x", + &[(":x", &0i32)], + |r| r.get(0)) + .unwrap()); } - #[test] + #[test] fn test_stmt_execute_named() { let db = SqliteConnection::open_in_memory().unwrap(); - let sql = "CREATE TABLE test (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, flag INTEGER)"; + let sql = "CREATE TABLE test (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, flag \ + INTEGER)"; db.execute_batch(sql).unwrap(); - let mut stmt = db.prepare("INSERT INTO test (id, name, flag) VALUES (:id, :name, :flag)").unwrap(); + let mut stmt = db.prepare("INSERT INTO test (id, name, flag) VALUES (:id, :name, :flag)") + .unwrap(); stmt.execute_named(&[(":name", &"one")]).unwrap(); } - #[test] + #[test] fn test_query_named() { let db = SqliteConnection::open_in_memory().unwrap(); - let sql = "CREATE TABLE test (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, flag INTEGER)"; + let sql = "CREATE TABLE test (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, flag \ + INTEGER)"; db.execute_batch(sql).unwrap(); let mut stmt = db.prepare("SELECT * FROM test where name = :name").unwrap(); From 2f220161a5c55be8d9abe205010b78cca2d9c47c Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Thu, 10 Dec 2015 20:16:46 -0500 Subject: [PATCH 09/18] Add extra check to named-parameter insertion unit test --- src/named_params.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/named_params.rs b/src/named_params.rs index dd9cc2f..ecafe56 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -189,6 +189,12 @@ mod test { let mut stmt = db.prepare("INSERT INTO test (id, name, flag) VALUES (:id, :name, :flag)") .unwrap(); stmt.execute_named(&[(":name", &"one")]).unwrap(); + + assert_eq!(1i32, + db.query_named_row("SELECT COUNT(*) FROM test WHERE name = :name", + &[(":name", &"one")], + |r| r.get(0)) + .unwrap()); } #[test] From 7338f23d4b25002573ced0dd35c7febaf190f78d Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Thu, 10 Dec 2015 20:27:09 -0500 Subject: [PATCH 10/18] Add extra assertions around binding named parameters --- src/named_params.rs | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/named_params.rs b/src/named_params.rs index ecafe56..743918e 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -147,6 +147,25 @@ impl<'conn> SqliteStatement<'conn> { } unsafe fn bind_named_parameters(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult<()> { + // Always check that the number of parameters is correct. + assert!(params.len() as c_int == ffi::sqlite3_bind_parameter_count(self.stmt), + "incorrect number of parameters to query(): expected {}, got {}", + ffi::sqlite3_bind_parameter_count(self.stmt), + params.len()); + + // In debug, also sanity check that we got distinct parameter names. + debug_assert!({ + use std::collections::HashSet; + + let mut s = HashSet::with_capacity(params.len()); + for &(name, _) in params { + s.insert(name); + } + + s.len() == params.len() + }, + "named parameters must be unique"); + for &(name, value) in params { let i = try!(self.parameter_index(name).ok_or(SqliteError { code: ffi::SQLITE_MISUSE, @@ -186,8 +205,7 @@ mod test { INTEGER)"; db.execute_batch(sql).unwrap(); - let mut stmt = db.prepare("INSERT INTO test (id, name, flag) VALUES (:id, :name, :flag)") - .unwrap(); + let mut stmt = db.prepare("INSERT INTO test (name) VALUES (:name)").unwrap(); stmt.execute_named(&[(":name", &"one")]).unwrap(); assert_eq!(1i32, @@ -207,4 +225,23 @@ mod test { let mut stmt = db.prepare("SELECT * FROM test where name = :name").unwrap(); stmt.query_named(&[(":name", &"one")]).unwrap(); } + + #[test] + #[should_panic] + fn test_panic_on_incorrect_number_of_parameters() { + let db = SqliteConnection::open_in_memory().unwrap(); + + let mut stmt = db.prepare("SELECT 1 WHERE 1 = :one AND 2 = :two").unwrap(); + let _ = stmt.query_named(&[(":one", &1i32)]); + } + + #[test] + #[cfg(debug_assertions)] + #[should_panic] + fn test_debug_panic_on_incorrect_parameter_names() { + let db = SqliteConnection::open_in_memory().unwrap(); + + let mut stmt = db.prepare("SELECT 1 WHERE 1 = :one AND 2 = :two").unwrap(); + let _ = stmt.query_named(&[(":one", &1i32), (":one", &2i32)]); + } } From 21528452d768e46790d53208f8106a06117f1258 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Thu, 10 Dec 2015 20:30:05 -0500 Subject: [PATCH 11/18] Clean up comments on named parameter methods --- src/named_params.rs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/named_params.rs b/src/named_params.rs index 743918e..55ce06f 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -10,12 +10,15 @@ use types::ToSql; impl SqliteConnection { /// Convenience method to prepare and execute a single SQL statement with named parameter(s). /// + /// On success, returns the number of rows that were changed or inserted or deleted (via + /// `sqlite3_changes`). + /// /// ## Example /// /// ```rust,no_run /// # use rusqlite::{SqliteConnection, SqliteResult}; /// fn insert(conn: &SqliteConnection) -> SqliteResult { - /// conn.execute_named("INSERT INTO test (id, name, flag) VALUES (:id, :name, :flag)", &[(":name", &"one")]) + /// conn.execute_named("INSERT INTO test (name) VALUES (:name)", &[(":name", &"one")]) /// } /// ``` /// @@ -27,7 +30,8 @@ impl SqliteConnection { self.prepare(sql).and_then(|mut stmt| stmt.execute_named(params)) } - /// Convenience method to execute a query with named parameter(s) that is expected to return a single row. + /// Convenience method to execute a query with named parameter(s) that is expected to return + /// a single row. /// /// If the query returns more than one row, all rows except the first are ignored. /// @@ -75,7 +79,7 @@ impl<'conn> SqliteStatement<'conn> { /// /// # Failure /// - /// Return None if `name` is invalid (NulError) or if no matching parameter is found. + /// Return None if `name` is invalid or if no matching parameter is found. pub fn parameter_index(&self, name: &str) -> Option { unsafe { CString::new(name).ok().and_then(|c_name| { @@ -98,8 +102,8 @@ impl<'conn> SqliteStatement<'conn> { /// ```rust,no_run /// # use rusqlite::{SqliteConnection, SqliteResult}; /// fn insert(conn: &SqliteConnection) -> SqliteResult { - /// let mut stmt = try!(conn.prepare("INSERT INTO test (id, name, flag) VALUES (:id, :name, :flag)")); - /// return stmt.execute_named(&[(":name", &"one")]); + /// let mut stmt = try!(conn.prepare("INSERT INTO test (name) VALUES (:name)")); + /// stmt.execute_named(&[(":name", &"one")]) /// } /// ``` /// @@ -114,19 +118,20 @@ impl<'conn> SqliteStatement<'conn> { } } - /// Execute the prepared statement with named parameter(s), returning an iterator over the resulting rows. + /// Execute the prepared statement with named parameter(s), returning an iterator over the + /// resulting rows. /// /// ## Example /// /// ```rust,no_run /// # use rusqlite::{SqliteConnection, SqliteResult, SqliteRows}; /// fn query(conn: &SqliteConnection) -> SqliteResult<()> { - /// let mut stmt = try!(conn.prepare("SELECT * FROM test where name = :name")); - /// let mut rows = try!(stmt.query_named(&[(":name", &"one")])); - /// for row in rows { - /// // ... - /// } - /// return Ok(()) + /// let mut stmt = try!(conn.prepare("SELECT * FROM test where name = :name")); + /// let mut rows = try!(stmt.query_named(&[(":name", &"one")])); + /// for row in rows { + /// // ... + /// } + /// Ok(()) /// } /// ``` /// From 8d4b3e6a31637e01a04a371a10510075b0fe407d Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Thu, 10 Dec 2015 20:30:43 -0500 Subject: [PATCH 12/18] Remove commented-out code --- src/named_params.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/named_params.rs b/src/named_params.rs index 55ce06f..8df8e96 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -1,4 +1,3 @@ -// use std::collections::HashMap; use std::ffi::CString; use libc::c_int; @@ -62,19 +61,6 @@ impl SqliteConnection { } impl<'conn> SqliteStatement<'conn> { - // pub fn parameter_names(&self) -> HashMap { - // let n = unsafe { ffi::sqlite3_bind_parameter_count(self.stmt) }; - // let mut index_by_name = HashMap::with_capacity(n as usize); - // for i in 1..n+1 { - // let c_name = unsafe { ffi::sqlite3_bind_parameter_name(self.stmt, i) }; - // if !c_name.is_null() { - // let c_slice = unsafe { CStr::from_ptr(c_name).to_bytes() }; - // index_by_name.insert(str::from_utf8(c_slice).unwrap().to_string(), n); - // } - // } - // index_by_name - // } - /// Return the index of an SQL parameter given its name. /// /// # Failure From 186cb5893bae2d338d6bc5f018d46433d9a40d7e Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Thu, 10 Dec 2015 20:31:07 -0500 Subject: [PATCH 13/18] Rename query_named_row -> query_row_named. I think this is more consistent with Rust's tendency to group similar methods like `iter`, `iter_mut` by appending the difference as a suffix. --- src/named_params.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/named_params.rs b/src/named_params.rs index 8df8e96..5a8e7b7 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -38,7 +38,7 @@ impl SqliteConnection { /// /// Will return `Err` if `sql` cannot be converted to a C-compatible string or if the /// underlying SQLite call fails. - pub fn query_named_row(&self, + pub fn query_row_named(&self, sql: &str, params: &[(&str, &ToSql)], f: F) @@ -183,7 +183,7 @@ mod test { 1); assert_eq!(3i32, - db.query_named_row("SELECT SUM(x) FROM foo WHERE x > :x", + db.query_row_named("SELECT SUM(x) FROM foo WHERE x > :x", &[(":x", &0i32)], |r| r.get(0)) .unwrap()); @@ -200,7 +200,7 @@ mod test { stmt.execute_named(&[(":name", &"one")]).unwrap(); assert_eq!(1i32, - db.query_named_row("SELECT COUNT(*) FROM test WHERE name = :name", + db.query_row_named("SELECT COUNT(*) FROM test WHERE name = :name", &[(":name", &"one")], |r| r.get(0)) .unwrap()); From b7468b2c4bde0386d065108d1e64f0e28e6d3c46 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Thu, 10 Dec 2015 20:41:31 -0500 Subject: [PATCH 14/18] Make parameter_index return a Result> instead of squashing string conversion errors into None. --- src/named_params.rs | 50 ++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/named_params.rs b/src/named_params.rs index 5a8e7b7..f40abaa 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -1,9 +1,9 @@ -use std::ffi::CString; use libc::c_int; use super::ffi; -use {SqliteResult, SqliteError, SqliteConnection, SqliteStatement, SqliteRows, SqliteRow}; +use {SqliteResult, SqliteError, SqliteConnection, SqliteStatement, SqliteRows, SqliteRow, + str_to_cstring}; use types::ToSql; impl SqliteConnection { @@ -65,17 +65,15 @@ impl<'conn> SqliteStatement<'conn> { /// /// # Failure /// - /// Return None if `name` is invalid or if no matching parameter is found. - pub fn parameter_index(&self, name: &str) -> Option { - unsafe { - CString::new(name).ok().and_then(|c_name| { - match ffi::sqlite3_bind_parameter_index(self.stmt, c_name.as_ptr()) { - 0 => None, // A zero is returned if no matching parameter is found. - n => Some(n), - } - }) - - } + /// Will return Err if `name` is invalid. Will return Ok(None) if the name + /// is valid but not a bound parameter of this statement. + pub fn parameter_index(&self, name: &str) -> SqliteResult> { + let c_name = try!(str_to_cstring(name)); + let c_index = unsafe { ffi::sqlite3_bind_parameter_index(self.stmt, c_name.as_ptr()) }; + Ok(match c_index { + 0 => None, // A zero is returned if no matching parameter is found. + n => Some(n), + }) } /// Execute the prepared statement with named parameter(s). @@ -98,8 +96,8 @@ impl<'conn> SqliteStatement<'conn> { /// Will return `Err` if binding parameters fails, the executed statement returns rows (in /// which case `query` should be used instead), or the underling SQLite call fails. pub fn execute_named(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult { + try!(self.bind_named_parameters(params)); unsafe { - try!(self.bind_named_parameters(params)); self.execute_() } } @@ -128,20 +126,17 @@ impl<'conn> SqliteStatement<'conn> { params: &[(&str, &ToSql)]) -> SqliteResult> { self.reset_if_needed(); - - unsafe { - try!(self.bind_named_parameters(params)); - } + try!(self.bind_named_parameters(params)); self.needs_reset = true; Ok(SqliteRows::new(self)) } - unsafe fn bind_named_parameters(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult<()> { + fn bind_named_parameters(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult<()> { // Always check that the number of parameters is correct. - assert!(params.len() as c_int == ffi::sqlite3_bind_parameter_count(self.stmt), + assert!(params.len() as c_int == unsafe { ffi::sqlite3_bind_parameter_count(self.stmt) }, "incorrect number of parameters to query(): expected {}, got {}", - ffi::sqlite3_bind_parameter_count(self.stmt), + unsafe { ffi::sqlite3_bind_parameter_count(self.stmt) }, params.len()); // In debug, also sanity check that we got distinct parameter names. @@ -158,11 +153,14 @@ impl<'conn> SqliteStatement<'conn> { "named parameters must be unique"); for &(name, value) in params { - let i = try!(self.parameter_index(name).ok_or(SqliteError { - code: ffi::SQLITE_MISUSE, - message: format!("Invalid parameter name: {}", name), - })); - try!(self.conn.decode_result(value.bind_parameter(self.stmt, i))); + if let Some(i) = try!(self.parameter_index(name)) { + try!(self.conn.decode_result(unsafe { value.bind_parameter(self.stmt, i) })); + } else { + return Err(SqliteError { + code: ffi::SQLITE_MISUSE, + message: format!("Invalid parameter name: {}", name), + }); + } } Ok(()) } From 0051ff47a6cc79c96490a1073b151e75801750c6 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Thu, 10 Dec 2015 20:48:38 -0500 Subject: [PATCH 15/18] Refactor: Extract match to get an expected row into its own method. --- src/lib.rs | 48 +++++++++++++++++++++------------------------ src/named_params.rs | 10 +--------- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2832900..520faba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -369,20 +369,12 @@ impl SqliteConnection { /// underlying SQLite call fails. pub fn query_row(&self, sql: &str, params: &[&ToSql], f: F) -> SqliteResult where F: FnOnce(SqliteRow) -> T - { - let mut stmt = try!(self.prepare(sql)); - let mut rows = try!(stmt.query(params)); + { + let mut stmt = try!(self.prepare(sql)); + let mut rows = try!(stmt.query(params)); - match rows.next() { - Some(row) => row.map(f), - None => { - Err(SqliteError { - code: ffi::SQLITE_NOTICE, - message: "Query did not return a row".to_string(), - }) - } - } - } + rows.get_expected_row().map(f) + } /// Convenience method to execute a query that is expected to return a single row, /// and execute a mapping via `f` on that returned row with the possibility of failure. @@ -408,20 +400,12 @@ impl SqliteConnection { pub fn query_row_and_then(&self, sql: &str, params: &[&ToSql], f: F) -> Result where F: FnOnce(SqliteRow) -> Result, E: convert::From - { - let mut stmt = try!(self.prepare(sql)); - let mut rows = try!(stmt.query(params)); + { + let mut stmt = try!(self.prepare(sql)); + let mut rows = try!(stmt.query(params)); - match rows.next() { - Some(row) => row.map_err(E::from).and_then(f), - None => { - Err(E::from(SqliteError { - code: ffi::SQLITE_NOTICE, - message: "Query did not return a row".to_string(), - })) - } - } - } + rows.get_expected_row().map_err(E::from).and_then(f) + } /// Convenience method to execute a query that is expected to return a single row. /// @@ -1049,6 +1033,18 @@ impl<'stmt> SqliteRows<'stmt> { failed: false, } } + + fn get_expected_row(&mut self) -> SqliteResult> { + match self.next() { + Some(row) => row, + None => { + Err(SqliteError { + code: ffi::SQLITE_NOTICE, + message: "Query did not return a row".to_string(), + }) + } + } + } } impl<'stmt> Iterator for SqliteRows<'stmt> { diff --git a/src/named_params.rs b/src/named_params.rs index f40abaa..80a43a3 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -48,15 +48,7 @@ impl SqliteConnection { let mut stmt = try!(self.prepare(sql)); let mut rows = try!(stmt.query_named(params)); - match rows.next() { - Some(row) => row.map(f), - None => { - Err(SqliteError { - code: ffi::SQLITE_NOTICE, - message: "Query did not return a row".to_string(), - }) - } - } + rows.get_expected_row().map(f) } } From 599e30f3724f0113b294882571eea0932d86f650 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Thu, 10 Dec 2015 20:49:47 -0500 Subject: [PATCH 16/18] Rename bind_named_parameters -> bind_parameters_named. --- src/named_params.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/named_params.rs b/src/named_params.rs index 80a43a3..f46b77c 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -88,7 +88,7 @@ impl<'conn> SqliteStatement<'conn> { /// Will return `Err` if binding parameters fails, the executed statement returns rows (in /// which case `query` should be used instead), or the underling SQLite call fails. pub fn execute_named(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult { - try!(self.bind_named_parameters(params)); + try!(self.bind_parameters_named(params)); unsafe { self.execute_() } @@ -118,13 +118,13 @@ impl<'conn> SqliteStatement<'conn> { params: &[(&str, &ToSql)]) -> SqliteResult> { self.reset_if_needed(); - try!(self.bind_named_parameters(params)); + try!(self.bind_parameters_named(params)); self.needs_reset = true; Ok(SqliteRows::new(self)) } - fn bind_named_parameters(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult<()> { + fn bind_parameters_named(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult<()> { // Always check that the number of parameters is correct. assert!(params.len() as c_int == unsafe { ffi::sqlite3_bind_parameter_count(self.stmt) }, "incorrect number of parameters to query(): expected {}, got {}", From 489b7df4510c225b7fe07632e06d226c23ddae5c Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Thu, 10 Dec 2015 20:51:16 -0500 Subject: [PATCH 17/18] Add named parameters to Changelog --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index 822668b..b29a517 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,7 @@ # Version UPCOMING (TBD) * Adds `backup` feature that exposes SQLite's online backup API. +* Adds a variety of `..._named` methods for executing queries using named placeholder parameters. # Version 0.5.0 (2015-12-08) From 5fdb2e1fdaea36ab8fa20e0116d41b79d691dd7b Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Fri, 11 Dec 2015 16:34:58 -0500 Subject: [PATCH 18/18] Allow named parameters to be omitted. If the parameters have never been bound, they default to `NULL`. If they have previously been bound, they keep the existing value. --- src/named_params.rs | 56 +++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/src/named_params.rs b/src/named_params.rs index f46b77c..49305e7 100644 --- a/src/named_params.rs +++ b/src/named_params.rs @@ -68,7 +68,10 @@ impl<'conn> SqliteStatement<'conn> { }) } - /// Execute the prepared statement with named parameter(s). + /// Execute the prepared statement with named parameter(s). If any parameters + /// that were in the prepared statement are not included in `params`, they + /// will continue to use the most-recently bound value from a previous call + /// to `execute_named`, or `NULL` if they have never been bound. /// /// On success, returns the number of rows that were changed or inserted or deleted (via /// `sqlite3_changes`). @@ -95,7 +98,9 @@ impl<'conn> SqliteStatement<'conn> { } /// Execute the prepared statement with named parameter(s), returning an iterator over the - /// resulting rows. + /// resulting rows. If any parameters that were in the prepared statement are not included in + /// `params`, they will continue to use the most-recently bound value from a previous call to + /// `query_named`, or `NULL` if they have never been bound. /// /// ## Example /// @@ -125,25 +130,6 @@ impl<'conn> SqliteStatement<'conn> { } fn bind_parameters_named(&mut self, params: &[(&str, &ToSql)]) -> SqliteResult<()> { - // Always check that the number of parameters is correct. - assert!(params.len() as c_int == unsafe { ffi::sqlite3_bind_parameter_count(self.stmt) }, - "incorrect number of parameters to query(): expected {}, got {}", - unsafe { ffi::sqlite3_bind_parameter_count(self.stmt) }, - params.len()); - - // In debug, also sanity check that we got distinct parameter names. - debug_assert!({ - use std::collections::HashSet; - - let mut s = HashSet::with_capacity(params.len()); - for &(name, _) in params { - s.insert(name); - } - - s.len() == params.len() - }, - "named parameters must be unique"); - for &(name, value) in params { if let Some(i) = try!(self.parameter_index(name)) { try!(self.conn.decode_result(unsafe { value.bind_parameter(self.stmt, i) })); @@ -208,21 +194,31 @@ mod test { } #[test] - #[should_panic] - fn test_panic_on_incorrect_number_of_parameters() { + fn test_unbound_parameters_are_null() { let db = SqliteConnection::open_in_memory().unwrap(); + let sql = "CREATE TABLE test (x TEXT, y TEXT)"; + db.execute_batch(sql).unwrap(); - let mut stmt = db.prepare("SELECT 1 WHERE 1 = :one AND 2 = :two").unwrap(); - let _ = stmt.query_named(&[(":one", &1i32)]); + let mut stmt = db.prepare("INSERT INTO test (x, y) VALUES (:x, :y)").unwrap(); + stmt.execute_named(&[(":x", &"one")]).unwrap(); + + let result = db.query_row("SELECT y FROM test WHERE x = 'one'", &[], + |row| row.get::>(0)).unwrap(); + assert!(result.is_none()); } #[test] - #[cfg(debug_assertions)] - #[should_panic] - fn test_debug_panic_on_incorrect_parameter_names() { + fn test_unbound_parameters_are_reused() { let db = SqliteConnection::open_in_memory().unwrap(); + let sql = "CREATE TABLE test (x TEXT, y TEXT)"; + db.execute_batch(sql).unwrap(); - let mut stmt = db.prepare("SELECT 1 WHERE 1 = :one AND 2 = :two").unwrap(); - let _ = stmt.query_named(&[(":one", &1i32), (":one", &2i32)]); + let mut stmt = db.prepare("INSERT INTO test (x, y) VALUES (:x, :y)").unwrap(); + stmt.execute_named(&[(":x", &"one")]).unwrap(); + stmt.execute_named(&[(":y", &"two")]).unwrap(); + + let result = db.query_row("SELECT x FROM test WHERE y = 'two'", &[], + |row| row.get::(0)).unwrap(); + assert_eq!(result, "one"); } }