-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(sqlite): add preupdate hook #3625
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -296,6 +296,8 @@ impl EstablishParams { | |
log_settings: self.log_settings.clone(), | ||
progress_handler_callback: None, | ||
update_hook_callback: None, | ||
#[cfg(feature = "preupdate-hook")] | ||
preupdate_hook_callback: None, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see this being set or referenced by anything. Did you mean to expose this on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was being used within the |
||
commit_hook_callback: None, | ||
rollback_hook_callback: None, | ||
}) | ||
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -14,6 +14,8 @@ use libsqlite3_sys::{ | |||
sqlite3, sqlite3_commit_hook, sqlite3_progress_handler, sqlite3_rollback_hook, | ||||
sqlite3_update_hook, SQLITE_DELETE, SQLITE_INSERT, SQLITE_UPDATE, | ||||
}; | ||||
#[cfg(feature = "preupdate-hook")] | ||||
pub use preupdate_hook::*; | ||||
|
||||
pub(crate) use handle::ConnectionHandle; | ||||
use sqlx_core::common::StatementCache; | ||||
|
@@ -88,6 +90,7 @@ pub struct UpdateHookResult<'a> { | |||
pub table: &'a str, | ||||
pub rowid: i64, | ||||
} | ||||
|
||||
pub(crate) struct UpdateHookHandler(NonNull<dyn FnMut(UpdateHookResult) + Send + 'static>); | ||||
unsafe impl Send for UpdateHookHandler {} | ||||
|
||||
|
@@ -112,6 +115,8 @@ pub(crate) struct ConnectionState { | |||
progress_handler_callback: Option<Handler>, | ||||
|
||||
update_hook_callback: Option<UpdateHookHandler>, | ||||
#[cfg(feature = "preupdate-hook")] | ||||
preupdate_hook_callback: Option<preupdate_hook::PreupdateHookHandler>, | ||||
|
||||
commit_hook_callback: Option<CommitHookHandler>, | ||||
|
||||
|
@@ -544,3 +549,219 @@ impl Statements { | |||
self.temp = None; | ||||
} | ||||
} | ||||
|
||||
#[cfg(feature = "preupdate-hook")] | ||||
mod preupdate_hook { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This module has enough going on that it should be its own file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. I considered moving all the hooks into their own module since there are quite a few now, but that would be a larger change. I'm happy to do that if you'd like though. |
||||
use super::ConnectionState; | ||||
use super::LockedSqliteHandle; | ||||
use super::SqliteOperation; | ||||
use crate::type_info::DataType; | ||||
use crate::{SqliteError, SqliteTypeInfo, SqliteValue}; | ||||
use libsqlite3_sys::{ | ||||
sqlite3, sqlite3_preupdate_count, sqlite3_preupdate_depth, sqlite3_preupdate_hook, | ||||
sqlite3_preupdate_new, sqlite3_preupdate_old, sqlite3_value, sqlite3_value_type, SQLITE_OK, | ||||
}; | ||||
use sqlx_core::error::Error; | ||||
use std::ffi::CStr; | ||||
use std::fmt::Debug; | ||||
use std::os::raw::{c_char, c_int, c_void}; | ||||
use std::panic::catch_unwind; | ||||
use std::ptr; | ||||
use std::ptr::NonNull; | ||||
|
||||
pub struct PreupdateHookResult<'a> { | ||||
pub operation: SqliteOperation, | ||||
pub database: &'a str, | ||||
pub table: &'a str, | ||||
pub case: PreupdateCase, | ||||
} | ||||
|
||||
pub(crate) struct PreupdateHookHandler( | ||||
NonNull<dyn FnMut(PreupdateHookResult) + Send + 'static>, | ||||
); | ||||
unsafe impl Send for PreupdateHookHandler {} | ||||
|
||||
/// The possible cases for when a PreUpdate Hook gets triggered. Allows access to the relevant | ||||
/// functions for each case through the contained values. | ||||
pub enum PreupdateCase { | ||||
/// Pre-update hook was triggered by an insert. | ||||
Insert(PreupdateNewValueAccessor), | ||||
/// Pre-update hook was triggered by a delete. | ||||
Delete(PreupdateOldValueAccessor), | ||||
/// Pre-update hook was triggered by an update. | ||||
Update { | ||||
old_value_accessor: PreupdateOldValueAccessor, | ||||
new_value_accessor: PreupdateNewValueAccessor, | ||||
}, | ||||
/// This variant is not normally produced by SQLite. You may encounter it | ||||
/// if you're using a different version than what's supported by this library. | ||||
Unknown, | ||||
} | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm dubious about the utility of this enum and separating Instead, I'd just merge all this functionality into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that it's also undefined behavior to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done 👍 |
||||
|
||||
/// An accessor for the old values of the row being deleted/updated during the preupdate callback. | ||||
#[derive(Debug)] | ||||
pub struct PreupdateOldValueAccessor { | ||||
db: *mut sqlite3, | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs a lifetime tying it to the duration of the callback or else it could lead to a use-after-free. The internal pointer makes this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||
old_row_id: i64, | ||||
} | ||||
|
||||
impl PreupdateOldValueAccessor { | ||||
/// Gets the amount of columns in the row being deleted/updated. | ||||
pub fn get_column_count(&self) -> i32 { | ||||
unsafe { sqlite3_preupdate_count(self.db) } | ||||
} | ||||
|
||||
/// Gets the depth of the query that triggered the preupdate hook. | ||||
/// Returns 0 if the preupdate callback was invoked as a result of | ||||
/// a direct insert, update, or delete operation; | ||||
/// 1 for inserts, updates, or deletes invoked by top-level triggers; | ||||
/// 2 for changes resulting from triggers called by top-level triggers; and so forth. | ||||
pub fn get_query_depth(&self) -> i32 { | ||||
unsafe { sqlite3_preupdate_depth(self.db) } | ||||
} | ||||
|
||||
/// Gets the row id of the row being updated/deleted. | ||||
pub fn get_old_row_id(&self) -> i64 { | ||||
self.old_row_id | ||||
} | ||||
|
||||
/// Gets the value of the row being updated/deleted at the specified index. | ||||
pub fn get_old_column_value(&self, i: i32) -> Result<SqliteValue, Error> { | ||||
let mut p_value: *mut sqlite3_value = ptr::null_mut(); | ||||
unsafe { | ||||
let ret = sqlite3_preupdate_old(self.db, i, &mut p_value); | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
We should handle this as a You'll need to add a new case here: Line 19 in 1678b19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should also check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I can add something to EDIT: added this in a separate commit. I can revert it if this isn't what you had in mind. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think the documentation might be a bit conservative here (or they don't want to make any guarantees) because it does check for these conditions and return an error, but I went ahead and added an explicit check here too. |
||||
if ret != SQLITE_OK { | ||||
return Err(Error::Database(Box::new(SqliteError::new(self.db)))); | ||||
} | ||||
let data_type = DataType::from_code(sqlite3_value_type(p_value)); | ||||
Ok(SqliteValue::new(p_value, SqliteTypeInfo(data_type))) | ||||
} | ||||
} | ||||
} | ||||
|
||||
/// An accessor for the new values of the row being inserted/updated during the preupdate callback. | ||||
#[derive(Debug)] | ||||
pub struct PreupdateNewValueAccessor { | ||||
db: *mut sqlite3, | ||||
new_row_id: i64, | ||||
} | ||||
|
||||
impl PreupdateNewValueAccessor { | ||||
/// Gets the amount of columns in the row being inserted/updated. | ||||
pub fn get_column_count(&self) -> i32 { | ||||
unsafe { sqlite3_preupdate_count(self.db) } | ||||
} | ||||
|
||||
/// Gets the depth of the query that triggered the preupdate hook. | ||||
/// Returns 0 if the preupdate callback was invoked as a result of | ||||
/// a direct insert, update, or delete operation; | ||||
/// 1 for inserts, updates, or deletes invoked by top-level triggers; | ||||
/// 2 for changes resulting from triggers called by top-level triggers; and so forth. | ||||
pub fn get_query_depth(&self) -> i32 { | ||||
unsafe { sqlite3_preupdate_depth(self.db) } | ||||
} | ||||
|
||||
/// Gets the row id of the row being inserted/updated. | ||||
pub fn get_new_row_id(&self) -> i64 { | ||||
self.new_row_id | ||||
} | ||||
|
||||
/// Gets the value of the row being updated/deleted at the specified index. | ||||
pub fn get_new_column_value(&self, i: i32) -> Result<SqliteValue, Error> { | ||||
let mut p_value: *mut sqlite3_value = ptr::null_mut(); | ||||
unsafe { | ||||
let ret = sqlite3_preupdate_new(self.db, i, &mut p_value); | ||||
if ret != SQLITE_OK { | ||||
return Err(Error::Database(Box::new(SqliteError::new(self.db)))); | ||||
} | ||||
let data_type = DataType::from_code(sqlite3_value_type(p_value)); | ||||
Ok(SqliteValue::new(p_value, SqliteTypeInfo(data_type))) | ||||
} | ||||
} | ||||
} | ||||
|
||||
impl ConnectionState { | ||||
pub(crate) fn remove_preupdate_hook(&mut self) { | ||||
if let Some(mut handler) = self.preupdate_hook_callback.take() { | ||||
unsafe { | ||||
sqlite3_preupdate_hook(self.handle.as_ptr(), None, ptr::null_mut()); | ||||
let _ = { Box::from_raw(handler.0.as_mut()) }; | ||||
} | ||||
} | ||||
} | ||||
} | ||||
|
||||
impl LockedSqliteHandle<'_> { | ||||
/// Registers a hook that is invoked prior to each `INSERT`, `UPDATE`, and `DELETE` operation on a database table. | ||||
/// At most one preupdate hook may be registered at a time on a single database connection. | ||||
/// | ||||
/// The preupdate hook only fires for changes to real database tables; | ||||
/// it is not invoked for changes to virtual tables or to system tables like sqlite_sequence or sqlite_stat1. | ||||
/// | ||||
/// See https://sqlite.org/c3ref/preupdate_count.html | ||||
pub fn set_preupdate_hook<F>(&mut self, callback: F) | ||||
where | ||||
F: FnMut(PreupdateHookResult) + Send + 'static, | ||||
{ | ||||
unsafe { | ||||
let callback_boxed = Box::new(callback); | ||||
// SAFETY: `Box::into_raw()` always returns a non-null pointer. | ||||
let callback = NonNull::new_unchecked(Box::into_raw(callback_boxed)); | ||||
let handler = callback.as_ptr() as *mut _; | ||||
self.guard.remove_preupdate_hook(); | ||||
self.guard.preupdate_hook_callback = Some(PreupdateHookHandler(callback)); | ||||
|
||||
sqlite3_preupdate_hook( | ||||
self.as_raw_handle().as_mut(), | ||||
Some(preupdate_hook::<F>), | ||||
handler, | ||||
); | ||||
} | ||||
} | ||||
|
||||
pub fn remove_preupdate_hook(&mut self) { | ||||
self.guard.remove_preupdate_hook(); | ||||
} | ||||
} | ||||
|
||||
extern "C" fn preupdate_hook<F>( | ||||
callback: *mut c_void, | ||||
db: *mut sqlite3, | ||||
op_code: c_int, | ||||
database: *const c_char, | ||||
table: *const c_char, | ||||
old_row_id: i64, | ||||
new_row_id: i64, | ||||
) where | ||||
F: FnMut(PreupdateHookResult), | ||||
{ | ||||
unsafe { | ||||
let _ = catch_unwind(|| { | ||||
let callback: *mut F = callback.cast::<F>(); | ||||
let operation: SqliteOperation = op_code.into(); | ||||
let database = CStr::from_ptr(database).to_str().unwrap_or_default(); | ||||
let table = CStr::from_ptr(table).to_str().unwrap_or_default(); | ||||
|
||||
let preupdate_case = match operation { | ||||
SqliteOperation::Insert => { | ||||
PreupdateCase::Insert(PreupdateNewValueAccessor { db, new_row_id }) | ||||
} | ||||
SqliteOperation::Delete => { | ||||
PreupdateCase::Delete(PreupdateOldValueAccessor { db, old_row_id }) | ||||
} | ||||
SqliteOperation::Update => PreupdateCase::Update { | ||||
old_value_accessor: PreupdateOldValueAccessor { db, old_row_id }, | ||||
new_value_accessor: PreupdateNewValueAccessor { db, new_row_id }, | ||||
}, | ||||
SqliteOperation::Unknown(_) => PreupdateCase::Unknown, | ||||
}; | ||||
(*callback)(PreupdateHookResult { | ||||
operation, | ||||
database, | ||||
table, | ||||
case: preupdate_case, | ||||
}) | ||||
}); | ||||
} | ||||
} | ||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should emit a compile error if neither
sqlite
orsqlite-unbundled
is enabled or else it could cause weird errors if it's only enabled on its own.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this.