diff --git a/src/blob/mod.rs b/src/blob/mod.rs index 7d7ec3d..e09156b 100644 --- a/src/blob/mod.rs +++ b/src/blob/mod.rs @@ -136,7 +136,10 @@ //! //! // Insert another BLOB, this time using a parameter passed in from //! // rust (potentially with a dynamic size). -//! db.execute("INSERT INTO test_table (content) VALUES (?)", &[ZeroBlob(64)])?; +//! db.execute( +//! "INSERT INTO test_table (content) VALUES (?)", +//! &[ZeroBlob(64)], +//! )?; //! //! // given a new row ID, we can reopen the blob on that row //! let rowid = db.last_insert_rowid(); @@ -177,7 +180,10 @@ //! //! // Insert another blob, this time using a parameter passed in from //! // rust (potentially with a dynamic size). -//! db.execute("INSERT INTO test_table (content) VALUES (?)", &[ZeroBlob(64)])?; +//! db.execute( +//! "INSERT INTO test_table (content) VALUES (?)", +//! &[ZeroBlob(64)], +//! )?; //! //! // given a new row ID, we can reopen the blob on that row //! let rowid = db.last_insert_rowid(); @@ -196,8 +202,8 @@ use crate::{Connection, DatabaseName, Result}; mod pos_io; -/// `feature = "blob"` Handle to an open BLOB. See [`rusqlite::blob`](crate::blob) documentation for -/// in-depth discussion. +/// `feature = "blob"` Handle to an open BLOB. See +/// [`rusqlite::blob`](crate::blob) documentation for in-depth discussion. pub struct Blob<'conn> { conn: &'conn Connection, blob: *mut ffi::sqlite3_blob, diff --git a/src/busy.rs b/src/busy.rs index b87504a..11cc81c 100644 --- a/src/busy.rs +++ b/src/busy.rs @@ -137,9 +137,7 @@ mod test { #[test] #[ignore] // FIXME: unstable fn test_busy_handler() { - lazy_static::lazy_static! { - static ref CALLED: AtomicBool = AtomicBool::new(false); - } + static CALLED: AtomicBool = AtomicBool::new(false); fn busy_handler(_: i32) -> bool { CALLED.store(true, Ordering::Relaxed); thread::sleep(Duration::from_millis(100)); diff --git a/src/collation.rs b/src/collation.rs index 1168b75..d88d662 100644 --- a/src/collation.rs +++ b/src/collation.rs @@ -15,9 +15,9 @@ unsafe extern "C" fn free_boxed_value(p: *mut c_void) { impl Connection { /// `feature = "collation"` Add or modify a collation. - pub fn create_collation(&self, collation_name: &str, x_compare: C) -> Result<()> + pub fn create_collation<'c, C>(&'c self, collation_name: &str, x_compare: C) -> Result<()> where - C: Fn(&str, &str) -> Ordering + Send + UnwindSafe + 'static, + C: Fn(&str, &str) -> Ordering + Send + UnwindSafe + 'c, { self.db .borrow_mut() @@ -39,9 +39,9 @@ impl Connection { } impl InnerConnection { - fn create_collation(&mut self, collation_name: &str, x_compare: C) -> Result<()> + fn create_collation<'c, C>(&'c mut self, collation_name: &str, x_compare: C) -> Result<()> where - C: Fn(&str, &str) -> Ordering + Send + UnwindSafe + 'static, + C: Fn(&str, &str) -> Ordering + Send + UnwindSafe + 'c, { unsafe extern "C" fn call_boxed_closure( arg1: *mut c_void, diff --git a/src/error.rs b/src/error.rs index 98583cb..e104019 100644 --- a/src/error.rs +++ b/src/error.rs @@ -111,8 +111,9 @@ pub enum Error { InvalidParameterCount(usize, usize), /// Returned from various functions in the Blob IO positional API. For - /// example, [`Blob::raw_read_at_exact`](crate::blob::Blob::raw_read_at_exact) - /// will return it if the blob has insufficient data. + /// example, + /// [`Blob::raw_read_at_exact`](crate::blob::Blob::raw_read_at_exact) will + /// return it if the blob has insufficient data. #[cfg(feature = "blob")] BlobSizeError, } diff --git a/src/functions.rs b/src/functions.rs index b2e9256..b767bb3 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -22,10 +22,9 @@ //! FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC, //! move |ctx| { //! assert_eq!(ctx.len(), 2, "called with unexpected number of arguments"); -//! let regexp: Arc = ctx -//! .get_or_create_aux(0, |vr| -> Result<_, BoxError> { -//! Ok(Regex::new(vr.as_str()?)?) -//! })?; +//! let regexp: Arc = ctx.get_or_create_aux(0, |vr| -> Result<_, BoxError> { +//! Ok(Regex::new(vr.as_str()?)?) +//! })?; //! let is_match = { //! let text = ctx //! .get_raw(1) @@ -334,15 +333,15 @@ impl Connection { /// # Failure /// /// Will return Err if the function could not be attached to the connection. - pub fn create_scalar_function<'a, F, T>( - &'a self, + pub fn create_scalar_function<'c, F, T>( + &'c self, fn_name: &str, n_arg: c_int, flags: FunctionFlags, x_func: F, ) -> Result<()> where - F: FnMut(&Context<'_>) -> Result + Send + UnwindSafe + 'a, + F: FnMut(&Context<'_>) -> Result + Send + UnwindSafe + 'c, T: ToSql, { self.db @@ -411,15 +410,15 @@ impl Connection { } impl InnerConnection { - fn create_scalar_function<'a, F, T>( - &'a mut self, + fn create_scalar_function<'c, F, T>( + &'c mut self, fn_name: &str, n_arg: c_int, flags: FunctionFlags, x_func: F, ) -> Result<()> where - F: FnMut(&Context<'_>) -> Result + Send + UnwindSafe + 'a, + F: FnMut(&Context<'_>) -> Result + Send + UnwindSafe + 'c, T: ToSql, { unsafe extern "C" fn call_boxed_closure( diff --git a/src/hooks.rs b/src/hooks.rs index 53dc041..da34537 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -2,7 +2,7 @@ #![allow(non_camel_case_types)] use std::os::raw::{c_char, c_int, c_void}; -use std::panic::catch_unwind; +use std::panic::{catch_unwind, RefUnwindSafe}; use std::ptr; use crate::ffi; @@ -40,9 +40,9 @@ impl Connection { /// a transaction is committed. /// /// The callback returns `true` to rollback. - pub fn commit_hook(&self, hook: Option) + pub fn commit_hook<'c, F>(&'c self, hook: Option) where - F: FnMut() -> bool + Send + 'static, + F: FnMut() -> bool + Send + 'c, { self.db.borrow_mut().commit_hook(hook); } @@ -51,9 +51,9 @@ impl Connection { /// a transaction is committed. /// /// The callback returns `true` to rollback. - pub fn rollback_hook(&self, hook: Option) + pub fn rollback_hook<'c, F>(&'c self, hook: Option) where - F: FnMut() + Send + 'static, + F: FnMut() + Send + 'c, { self.db.borrow_mut().rollback_hook(hook); } @@ -68,12 +68,27 @@ impl Connection { /// - the name of the database ("main", "temp", ...), /// - the name of the table that is updated, /// - the ROWID of the row that is updated. - pub fn update_hook(&self, hook: Option) + pub fn update_hook<'c, F>(&'c self, hook: Option) where - F: FnMut(Action, &str, &str, i64) + Send + 'static, + F: FnMut(Action, &str, &str, i64) + Send + 'c, { self.db.borrow_mut().update_hook(hook); } + + /// `feature = "hooks"` Register a query progress callback. + /// + /// The parameter `num_ops` is the approximate number of virtual machine + /// instructions that are evaluated between successive invocations of the + /// `handler`. If `num_ops` is less than one then the progress handler + /// is disabled. + /// + /// If the progress callback returns `true`, the operation is interrupted. + pub fn progress_handler(&self, num_ops: c_int, handler: Option) + where + F: FnMut() -> bool + Send + RefUnwindSafe + 'static, + { + self.db.borrow_mut().progress_handler(num_ops, handler); + } } impl InnerConnection { @@ -81,11 +96,12 @@ impl InnerConnection { self.update_hook(None::); self.commit_hook(None:: bool>); self.rollback_hook(None::); + self.progress_handler(0, None:: bool>); } - fn commit_hook(&mut self, hook: Option) + fn commit_hook<'c, F>(&'c mut self, hook: Option) where - F: FnMut() -> bool + Send + 'static, + F: FnMut() -> bool + Send + 'c, { unsafe extern "C" fn call_boxed_closure(p_arg: *mut c_void) -> c_int where @@ -132,9 +148,9 @@ impl InnerConnection { self.free_commit_hook = free_commit_hook; } - fn rollback_hook(&mut self, hook: Option) + fn rollback_hook<'c, F>(&'c mut self, hook: Option) where - F: FnMut() + Send + 'static, + F: FnMut() + Send + 'c, { unsafe extern "C" fn call_boxed_closure(p_arg: *mut c_void) where @@ -173,9 +189,9 @@ impl InnerConnection { self.free_rollback_hook = free_rollback_hook; } - fn update_hook(&mut self, hook: Option) + fn update_hook<'c, F>(&'c mut self, hook: Option) where - F: FnMut(Action, &str, &str, i64) + Send + 'static, + F: FnMut(Action, &str, &str, i64) + Send + 'c, { unsafe extern "C" fn call_boxed_closure( p_arg: *mut c_void, @@ -236,6 +252,45 @@ impl InnerConnection { } self.free_update_hook = free_update_hook; } + + fn progress_handler(&mut self, num_ops: c_int, handler: Option) + where + F: FnMut() -> bool + Send + RefUnwindSafe + 'static, + { + unsafe extern "C" fn call_boxed_closure(p_arg: *mut c_void) -> c_int + where + F: FnMut() -> bool, + { + let r = catch_unwind(|| { + let boxed_handler: *mut F = p_arg as *mut F; + (*boxed_handler)() + }); + if let Ok(true) = r { + 1 + } else { + 0 + } + } + + match handler { + Some(handler) => { + let boxed_handler = Box::new(handler); + unsafe { + ffi::sqlite3_progress_handler( + self.db(), + num_ops, + Some(call_boxed_closure::), + &*boxed_handler as *const F as *mut _, + ) + } + self.progress_handler = Some(boxed_handler); + } + _ => { + unsafe { ffi::sqlite3_progress_handler(self.db(), num_ops, None, ptr::null_mut()) } + self.progress_handler = None; + } + }; + } } unsafe fn free_boxed_hook(p: *mut c_void) { @@ -246,23 +301,20 @@ unsafe fn free_boxed_hook(p: *mut c_void) { mod test { use super::Action; use crate::Connection; - use lazy_static::lazy_static; use std::sync::atomic::{AtomicBool, Ordering}; #[test] fn test_commit_hook() { let db = Connection::open_in_memory().unwrap(); - lazy_static! { - static ref CALLED: AtomicBool = AtomicBool::new(false); - } + let mut called = false; db.commit_hook(Some(|| { - CALLED.store(true, Ordering::Relaxed); + called = true; false })); db.execute_batch("BEGIN; CREATE TABLE foo (t TEXT); COMMIT;") .unwrap(); - assert!(CALLED.load(Ordering::Relaxed)); + assert!(called); } #[test] @@ -282,33 +334,59 @@ mod test { fn test_rollback_hook() { let db = Connection::open_in_memory().unwrap(); - lazy_static! { - static ref CALLED: AtomicBool = AtomicBool::new(false); - } + let mut called = false; db.rollback_hook(Some(|| { - CALLED.store(true, Ordering::Relaxed); + called = true; })); db.execute_batch("BEGIN; CREATE TABLE foo (t TEXT); ROLLBACK;") .unwrap(); - assert!(CALLED.load(Ordering::Relaxed)); + assert!(called); } #[test] fn test_update_hook() { let db = Connection::open_in_memory().unwrap(); - lazy_static! { - static ref CALLED: AtomicBool = AtomicBool::new(false); - } + let mut called = false; db.update_hook(Some(|action, db: &str, tbl: &str, row_id| { assert_eq!(Action::SQLITE_INSERT, action); assert_eq!("main", db); assert_eq!("foo", tbl); assert_eq!(1, row_id); - CALLED.store(true, Ordering::Relaxed); + called = true; })); db.execute_batch("CREATE TABLE foo (t TEXT)").unwrap(); db.execute_batch("INSERT INTO foo VALUES ('lisa')").unwrap(); + assert!(called); + } + + #[test] + fn test_progress_handler() { + let db = Connection::open_in_memory().unwrap(); + + static CALLED: AtomicBool = AtomicBool::new(false); + db.progress_handler( + 1, + Some(|| { + CALLED.store(true, Ordering::Relaxed); + false + }), + ); + db.execute_batch("BEGIN; CREATE TABLE foo (t TEXT); COMMIT;") + .unwrap(); assert!(CALLED.load(Ordering::Relaxed)); } + + #[test] + fn test_progress_handler_interrupt() { + let db = Connection::open_in_memory().unwrap(); + + fn handler() -> bool { + true + } + + db.progress_handler(1, Some(handler)); + db.execute_batch("BEGIN; CREATE TABLE foo (t TEXT); COMMIT;") + .unwrap_err(); + } } diff --git a/src/inner_connection.rs b/src/inner_connection.rs index 0e9ff7c..7b87e75 100644 --- a/src/inner_connection.rs +++ b/src/inner_connection.rs @@ -31,6 +31,8 @@ pub struct InnerConnection { pub free_rollback_hook: Option, #[cfg(feature = "hooks")] pub free_update_hook: Option, + #[cfg(feature = "hooks")] + pub progress_handler: Option bool + Send>>, owned: bool, } @@ -46,6 +48,8 @@ impl InnerConnection { free_rollback_hook: None, #[cfg(feature = "hooks")] free_update_hook: None, + #[cfg(feature = "hooks")] + progress_handler: None, owned, } } diff --git a/src/lib.rs b/src/lib.rs index 53f1773..e3161a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -810,6 +810,67 @@ impl fmt::Debug for Connection { } } +/// Batch iterator +/// ```rust +/// use rusqlite::{Batch, Connection, Result, NO_PARAMS}; +/// +/// fn main() -> Result<()> { +/// let conn = Connection::open_in_memory()?; +/// let sql = r" +/// CREATE TABLE tbl1 (col); +/// CREATE TABLE tbl2 (col); +/// "; +/// let mut batch = Batch::new(&conn, sql); +/// while let Some(mut stmt) = batch.next()? { +/// stmt.execute(NO_PARAMS)?; +/// } +/// Ok(()) +/// } +/// ``` +#[derive(Debug)] +pub struct Batch<'conn, 'sql> { + conn: &'conn Connection, + sql: &'sql str, + tail: usize, +} + +impl<'conn, 'sql> Batch<'conn, 'sql> { + /// Constructor + pub fn new(conn: &'conn Connection, sql: &'sql str) -> Batch<'conn, 'sql> { + Batch { conn, sql, tail: 0 } + } + + /// Iterates on each batch statements. + /// + /// Returns `Ok(None)` when batch is completed. + #[allow(clippy::should_implement_trait)] // fallible iterator + pub fn next(&mut self) -> Result>> { + while self.tail < self.sql.len() { + let sql = &self.sql[self.tail..]; + let next = self.conn.prepare(sql)?; + let tail = next.stmt.tail(); + if tail == 0 { + self.tail = self.sql.len(); + } else { + self.tail += tail; + } + if next.stmt.is_null() { + continue; + } + return Ok(Some(next)); + } + Ok(None) + } +} + +impl<'conn> Iterator for Batch<'conn, '_> { + type Item = Result>; + + fn next(&mut self) -> Option>> { + self.next().transpose() + } +} + bitflags::bitflags! { /// Flags for opening SQLite database connections. /// See [sqlite3_open_v2](http://www.sqlite.org/c3ref/open.html) for details. @@ -1749,63 +1810,77 @@ mod test { err => panic!("Unexpected error {}", err), } } + } - #[test] - fn test_dynamic() { - let db = checked_memory_handle(); - let sql = "BEGIN; + #[test] + fn test_dynamic() { + let db = checked_memory_handle(); + let sql = "BEGIN; CREATE TABLE foo(x INTEGER, y TEXT); INSERT INTO foo VALUES(4, \"hello\"); END;"; - db.execute_batch(sql).unwrap(); + db.execute_batch(sql).unwrap(); - db.query_row("SELECT * FROM foo", params![], |r| { - assert_eq!(2, r.column_count()); - Ok(()) - }) - .unwrap(); - } - #[test] - fn test_dyn_box() { - let db = checked_memory_handle(); - db.execute_batch("CREATE TABLE foo(x INTEGER);").unwrap(); - let b: Box = Box::new(5); - db.execute("INSERT INTO foo VALUES(?)", &[b]).unwrap(); - db.query_row("SELECT x FROM foo", params![], |r| { - assert_eq!(5, r.get_unwrap::<_, i32>(0)); - Ok(()) - }) - .unwrap(); - } + db.query_row("SELECT * FROM foo", NO_PARAMS, |r| { + assert_eq!(2, r.column_count()); + Ok(()) + }) + .unwrap(); + } + #[test] + fn test_dyn_box() { + let db = checked_memory_handle(); + db.execute_batch("CREATE TABLE foo(x INTEGER);").unwrap(); + let b: Box = Box::new(5); + db.execute("INSERT INTO foo VALUES(?)", &[b]).unwrap(); + db.query_row("SELECT x FROM foo", NO_PARAMS, |r| { + assert_eq!(5, r.get_unwrap::<_, i32>(0)); + Ok(()) + }) + .unwrap(); + } - #[test] - fn test_params() { - let db = checked_memory_handle(); - db.query_row( - "SELECT + #[test] + fn test_params() { + let db = checked_memory_handle(); + db.query_row( + "SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?;", - params![ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - ], - |r| { - assert_eq!(1, r.get_unwrap::<_, i32>(0)); - Ok(()) - }, - ) - .unwrap(); - } + params![ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + ], + |r| { + assert_eq!(1, r.get_unwrap::<_, i32>(0)); + Ok(()) + }, + ) + .unwrap(); + } - #[test] - #[cfg(not(feature = "extra_check"))] - fn test_alter_table() { - let db = checked_memory_handle(); - db.execute_batch("CREATE TABLE x(t);").unwrap(); - // `execute_batch` should be used but `execute` should also work - db.execute("ALTER TABLE x RENAME TO y;", params![]).unwrap(); + #[test] + #[cfg(not(feature = "extra_check"))] + fn test_alter_table() { + let db = checked_memory_handle(); + db.execute_batch("CREATE TABLE x(t);").unwrap(); + // `execute_batch` should be used but `execute` should also work + db.execute("ALTER TABLE x RENAME TO y;", NO_PARAMS).unwrap(); + } + + #[test] + fn test_batch() { + let db = checked_memory_handle(); + let sql = r" + CREATE TABLE tbl1 (col); + CREATE TABLE tbl2 (col); + "; + let batch = Batch::new(&db, sql); + for stmt in batch { + let mut stmt = stmt.unwrap(); + stmt.execute(NO_PARAMS).unwrap(); } } } diff --git a/src/session.rs b/src/session.rs index 590953c..17f8b81 100644 --- a/src/session.rs +++ b/src/session.rs @@ -819,9 +819,7 @@ mod test { db.execute_batch("CREATE TABLE foo(t TEXT PRIMARY KEY NOT NULL);") .unwrap(); - lazy_static::lazy_static! { - static ref CALLED: AtomicBool = AtomicBool::new(false); - } + static CALLED: AtomicBool = AtomicBool::new(false); db.apply( &changeset, None:: bool>, diff --git a/src/types/mod.rs b/src/types/mod.rs index fa0d2c8..d4bc440 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -15,8 +15,8 @@ //! `FromSql` has different behaviour depending on the SQL and Rust types, and //! the value. //! -//! * `INTEGER` to integer: returns an `Error::IntegralValueOutOfRange` error -//! if the value does not fit in the Rust type. +//! * `INTEGER` to integer: returns an `Error::IntegralValueOutOfRange` error if +//! the value does not fit in the Rust type. //! * `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. @@ -32,7 +32,6 @@ //! can be parsed by SQLite's builtin //! [datetime](https://www.sqlite.org/lang_datefunc.html) functions. If you //! want different storage for datetimes, you can use a newtype. -//! #![cfg_attr( feature = "time", doc = r##" @@ -387,7 +386,7 @@ mod test { } macro_rules! test_conversion { - ($db_etc:ident, $insert_value:expr, $get_type:ty, expect $expected_value:expr) => { + ($db_etc:ident, $insert_value:expr, $get_type:ty,expect $expected_value:expr) => { $db_etc .insert_statement .execute(params![$insert_value]) @@ -398,7 +397,7 @@ mod test { assert_eq!(res.unwrap(), $expected_value); $db_etc.delete_statement.execute(NO_PARAMS).unwrap(); }; - ($db_etc:ident, $insert_value:expr, $get_type:ty, expect_from_sql_error) => { + ($db_etc:ident, $insert_value:expr, $get_type:ty,expect_from_sql_error) => { $db_etc .insert_statement .execute(params![$insert_value]) @@ -409,7 +408,7 @@ mod test { res.unwrap_err(); $db_etc.delete_statement.execute(NO_PARAMS).unwrap(); }; - ($db_etc:ident, $insert_value:expr, $get_type:ty, expect_to_sql_error) => { + ($db_etc:ident, $insert_value:expr, $get_type:ty,expect_to_sql_error) => { $db_etc .insert_statement .execute(params![$insert_value]) diff --git a/src/util/small_cstr.rs b/src/util/small_cstr.rs index bc4d9cb..7349df0 100644 --- a/src/util/small_cstr.rs +++ b/src/util/small_cstr.rs @@ -100,6 +100,7 @@ impl std::fmt::Debug for SmallCString { impl std::ops::Deref for SmallCString { type Target = CStr; + #[inline] fn deref(&self) -> &CStr { self.as_cstr() diff --git a/src/util/sqlite_string.rs b/src/util/sqlite_string.rs index 18d462e..433288c 100644 --- a/src/util/sqlite_string.rs +++ b/src/util/sqlite_string.rs @@ -130,9 +130,10 @@ impl SqliteMallocString { // This is safe: // - `align` is never 0 // - `align` is always a power of 2. - // - `size` needs no realignment because it's guaranteed to be - // aligned (everything is aligned to 1) - // - `size` is also never zero, although this function doesn't actually require it now. + // - `size` needs no realignment because it's guaranteed to be aligned + // (everything is aligned to 1) + // - `size` is also never zero, although this function doesn't actually require + // it now. let layout = Layout::from_size_align_unchecked(s.len().saturating_add(1), 1); // Note: This call does not return. handle_alloc_error(layout);