-
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?
Conversation
d9eb625
to
5264ada
Compare
}); | ||
|
||
let _ = sqlx::query("INSERT INTO tweet ( id, text ) VALUES ( 3, 'Hello, World' )") | ||
.execute(&mut conn) | ||
.await?; | ||
assert!(CALLED.load(Ordering::Relaxed)); |
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.
Without these asserts, it's possible that these tests would still pass if the callback functions were never invoked. I went ahead and added these checks to the existing tests as well as the new tests that I created.
436694b
to
03d9a3f
Compare
03d9a3f
to
bcdb609
Compare
bcdb609
to
7f1fdd9
Compare
@@ -108,6 +108,7 @@ postgres = ["sqlx-postgres", "sqlx-macros?/postgres"] | |||
mysql = ["sqlx-mysql", "sqlx-macros?/mysql"] | |||
sqlite = ["_sqlite", "sqlx-sqlite/bundled", "sqlx-macros?/sqlite"] | |||
sqlite-unbundled = ["_sqlite", "sqlx-sqlite/unbundled", "sqlx-macros?/sqlite-unbundled"] | |||
sqlite-preupdate-hook = ["sqlx-sqlite/preupdate-hook"] |
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
or sqlite-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.
sqlx-sqlite/src/connection/mod.rs
Outdated
@@ -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 comment
The 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 comment
The 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.
sqlx-sqlite/src/connection/mod.rs
Outdated
/// 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 comment
The 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 !Send
/!Sync
but it could still be stored in a thread-local.
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.
PreupdateHookResult
has a lifetime on it due to the string parameters so I believe this should be fine, especially now that everything is consolidated into that struct. I made a comment in there that these lifetimes need to be preserved for this reason.
sqlx-sqlite/src/connection/mod.rs
Outdated
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 comment
The reason will be displayed to describe this comment to others. Learn more.
The sqlite3_value
returned by this call and sqlite3_preupdate_new
has weird semantics. It's a "protected" value so it's thread-safe, but at the same time the documentation also specifies:
The sqlite3_value that P points to will be destroyed when the preupdate callback returns.
We should handle this as a SqliteValueRef
instead, using the same lifetime. The user can then use .to_owned()
to get a fully independent value if they need it.
You'll need to add a new case here:
Line 19 in 1678b19
Value(&'r SqliteValue), |
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 also check i
against sqlite3_preupdate_count
because the result is undefined if the index is out of bounds.
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.
We should handle this as a SqliteValueRef instead, using the same lifetime. The user can then use .to_owned() to get a fully independent value if they need it.
SqliteValue::new
calls sqlite3_value_dup
which creates a copy of the sqlite3_value
. I tested this out by storing the returned SqliteValue
in a mutex and ensuring the returned value could still be decoded properly after the callback was completed.
I can add something to SqliteValueRef
for this to avoid the call to sqlite3_value_dup
, but I think that would require re-implementing a lot of the logic in SqliteValue
or modifying it so that it can operate on both an owned or borrowed value. Let me know if I'm missing something.
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 also check i against sqlite3_preupdate_count because the result is undefined if the index is out of bounds.
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.
sqlx-sqlite/src/connection/mod.rs
Outdated
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 comment
The reason will be displayed to describe this comment to others. Learn more.
I'm dubious about the utility of this enum and separating PreupdateOldValueAccessor
and PreupdateNewValueAccessor
. If a user wants to handle updates generically, they'd have to match on this and then synthesize some sort of tuple of (Option<PreupdateOldValueAccessor>, Option<PreupdateNewValueAccessor>)
.
Instead, I'd just merge all this functionality into PreUpdateHookResult
.
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.
Note that it's also undefined behavior to use sqlite3_preupdate_old_value()
when it's an INSERT
operation or _new_value()
when it's a DELETE
operation, so that also needs to be checked.
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.
Done 👍
@@ -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 comment
The 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 SqliteConnectOptions
?
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 was being used within the preupdate_hook
module to avoid having to add more cfg
checks in the main connection module and make additional fields pub(super)
, but that does make it a bit harder to find. I went ahead and moved that logic to be with the rest of the hooks.
Adds bindings for SQLite's preupdate hook.
This is exposed as a separate feature because the system SQLite version generally does not have this flag enabled, so using it with
sqlite-unbundled
may cause linker errors.If we don't want to create a new feature flag in the main crate just for this, we could enable it by default with the
sqlite
(bundled) feature only. That would make the configuration a little simpler.