diff --git a/pgrx-tests/src/tests/guc_tests.rs b/pgrx-tests/src/tests/guc_tests.rs index 79c570954c..75dd5068b1 100644 --- a/pgrx-tests/src/tests/guc_tests.rs +++ b/pgrx-tests/src/tests/guc_tests.rs @@ -12,6 +12,7 @@ mod tests { #[allow(unused_imports)] use crate as pgrx_tests; + use std::ffi::c_char; use std::ffi::CString; use pgrx::guc::*; @@ -231,4 +232,148 @@ mod tests { assert_eq!(GUC_NO_SHOW.get(), true, "'no_show' should reset after 'RESET ALL'"); }); } + + #[pg_test] + #[should_panic(expected = "invalid value for parameter \"test.hooks\": 0")] + fn test_guc_check_hook() { + static SIDE_EFFECT: std::sync::RwLock = std::sync::RwLock::new(0); + + #[pg_guard] + unsafe extern "C-unwind" fn check_hook( + newval: *mut bool, + _extra: *mut *mut std::ffi::c_void, + _source: pg_sys::GucSource::Type, + ) -> bool { + if *newval { + *SIDE_EFFECT.write().unwrap() += 1; + } + *newval + } + + // Create and register GUC with hooks. As default is true, SIDE_EFFECT will be 1. + static GUC: GucSetting = GucSetting::::new(true); + unsafe { + GucRegistry::define_bool_guc_with_hooks( + c"test.hooks", + c"test hooks guc", + c"test hooks guc", + &GUC, + GucContext::Userset, + GucFlags::default(), + Some(check_hook), + None, + None, + ); + } + + // Test check hook - should reject false and not initialize the GUC + assert!( + Spi::run("SET test.hooks TO false").is_err(), + "Expected panic when setting test.hooks to false" + ); + assert_eq!(*SIDE_EFFECT.read().unwrap(), 1); + + // Test check hook - should accept true and increment SIDE_EFFECT + assert!(Spi::run("SET test.hooks TO true").is_ok()); + assert_eq!(GUC.get(), true); + assert_eq!(*SIDE_EFFECT.read().unwrap(), 2); + } + + #[pg_test] + #[should_panic(expected = "should panic!")] + fn test_check_hook_fail() { + #[pg_guard] + unsafe extern "C-unwind" fn check_hook( + newval: *mut bool, + _extra: *mut *mut std::ffi::c_void, + _source: pg_sys::GucSource::Type, + ) -> bool { + if *newval { + panic!("should panic!"); + } + *newval + } + + static GUARDED_GUC: GucSetting = GucSetting::::new(true); + unsafe { + GucRegistry::define_bool_guc_with_hooks( + c"test.guarded_hooks", + c"test guarded hooks guc", + c"test guarded hooks guc", + &GUARDED_GUC, + GucContext::Userset, + GucFlags::default(), + Some(check_hook), + None, + None, + ); + } + } + + #[pg_test] + fn test_assign_hook() { + static SIDE_EFFECT: std::sync::RwLock = std::sync::RwLock::new(0); + + #[pg_guard] + unsafe extern "C-unwind" fn assign_hook(newval: bool, _extra: *mut ::core::ffi::c_void) { + if newval { + *SIDE_EFFECT.write().unwrap() += 1; + } + } + + // Create and register GUC with hooks. As default is false, SIDE_EFFECT will be 0. + static GUC: GucSetting = GucSetting::::new(false); + unsafe { + GucRegistry::define_bool_guc_with_hooks( + c"test.hooks", + c"test hooks guc", + c"test hooks guc", + &GUC, + GucContext::Userset, + GucFlags::default(), + None, + Some(assign_hook), + None, + ); + } + + // SIDE_EFFECT should not be updated + Spi::run("SET test.hooks TO false").unwrap(); + assert_eq!(*SIDE_EFFECT.read().unwrap(), 0); + + // SIDE_EFFECT should be updated + Spi::run("SET test.hooks TO true").unwrap(); + assert_eq!(*SIDE_EFFECT.read().unwrap(), 1); + } + + #[pg_test] + fn test_show_hook() { + #[pg_guard] + unsafe extern "C-unwind" fn show_hook() -> *const c_char { + CString::new("CUSTOM_SHOW_HOOK").unwrap().into_raw() as *const c_char + } + + // Register GUC + static GUC: GucSetting = GucSetting::::new(false); + unsafe { + GucRegistry::define_bool_guc_with_hooks( + c"test.hooks", + c"test hooks guc", + c"test hooks guc", + &GUC, + GucContext::Userset, + GucFlags::default(), + None, + None, + Some(show_hook), + ); + } + + // Test show hook + Spi::connect_mut(|client| { + let r = client.update("SHOW test.hooks", None, &[]).expect("SPI failed"); + let value: &str = r.first().get_one::<&str>().unwrap().unwrap(); + assert_eq!(value, "CUSTOM_SHOW_HOOK"); + }); + } } diff --git a/pgrx/src/guc.rs b/pgrx/src/guc.rs index 46948db9bb..d011a13835 100644 --- a/pgrx/src/guc.rs +++ b/pgrx/src/guc.rs @@ -225,6 +225,7 @@ impl GucSetting { pub struct GucRegistry {} impl GucRegistry { + // GUC Registration functions that do not expose hooks pub fn define_bool_guc( name: &'static CStr, short_description: &'static CStr, @@ -354,4 +355,192 @@ impl GucRegistry { ); } } + + /// Define a boolean GUC with custom hooks. + /// + /// # Hooks + /// + /// * `check_hook` - Validates new values. Return false to reject. + /// * `assign_hook` - Called after value is set. Use for side effects. + /// * `show_hook` - Returns custom display string for SHOW commands. + /// + /// # Safety + /// + /// This function is unsafe because hook functions must be properly guarded against Rust panics. + /// Any hook function that might panic must be marked with `#[pg_guard]` to ensure proper + /// conversion of Rust panics into PostgreSQL errors. + /// + pub unsafe fn define_bool_guc_with_hooks( + name: &'static CStr, + short_description: &'static CStr, + long_description: &'static CStr, + setting: &'static GucSetting, + context: GucContext, + flags: GucFlags, + check_hook: pg_sys::GucBoolCheckHook, + assign_hook: pg_sys::GucBoolAssignHook, + show_hook: pg_sys::GucShowHook, + ) { + unsafe { + pg_sys::DefineCustomBoolVariable( + name.as_ptr(), + short_description.as_ptr(), + long_description.as_ptr(), + setting.value.as_ptr(), + setting.value.get(), + context as isize as _, + flags.bits(), + check_hook, + assign_hook, + show_hook, + ); + } + } + + /// Define an integer GUC with custom hooks. + /// + /// # Safety + /// + /// This function is unsafe because hook functions must be properly guarded against Rust panics. + /// Any hook function that might panic must be marked with `#[pg_guard]` to ensure proper + /// conversion of Rust panics into PostgreSQL errors. + pub unsafe fn define_int_guc_with_hooks( + name: &'static CStr, + short_description: &'static CStr, + long_description: &'static CStr, + setting: &'static GucSetting, + min_value: i32, + max_value: i32, + context: GucContext, + flags: GucFlags, + check_hook: pg_sys::GucIntCheckHook, + assign_hook: pg_sys::GucIntAssignHook, + show_hook: pg_sys::GucShowHook, + ) { + unsafe { + pg_sys::DefineCustomIntVariable( + name.as_ptr(), + short_description.as_ptr(), + long_description.as_ptr(), + setting.value.as_ptr(), + setting.value.get(), + min_value, + max_value, + context as isize as _, + flags.bits(), + check_hook, + assign_hook, + show_hook, + ) + } + } + + /// Define a string GUC with custom hooks. + /// + /// # Safety + /// + /// This function is unsafe because hook functions must be properly guarded against Rust panics. + /// Any hook function that might panic must be marked with `#[pg_guard]` to ensure proper + /// conversion of Rust panics into PostgreSQL errors. + pub unsafe fn define_string_guc_with_hooks( + name: &'static CStr, + short_description: &'static CStr, + long_description: &'static CStr, + setting: &'static GucSetting>, + context: GucContext, + flags: GucFlags, + check_hook: pg_sys::GucStringCheckHook, + assign_hook: pg_sys::GucStringAssignHook, + show_hook: pg_sys::GucShowHook, + ) { + unsafe { + pg_sys::DefineCustomStringVariable( + name.as_ptr(), + short_description.as_ptr(), + long_description.as_ptr(), + setting.value.as_ptr(), + setting.value.get(), + context as isize as _, + flags.bits(), + check_hook, + assign_hook, + show_hook, + ); + } + } + + /// Define a float GUC with custom hooks. + /// + /// # Safety + /// + /// This function is unsafe because hook functions must be properly guarded against Rust panics. + /// Any hook function that might panic must be marked with `#[pg_guard]` to ensure proper + /// conversion of Rust panics into PostgreSQL errors. + /// + pub fn define_float_guc_with_hooks( + name: &'static CStr, + short_description: &'static CStr, + long_description: &'static CStr, + setting: &'static GucSetting, + min_value: f64, + max_value: f64, + context: GucContext, + flags: GucFlags, + check_hook: pg_sys::GucRealCheckHook, + assign_hook: pg_sys::GucRealAssignHook, + show_hook: pg_sys::GucShowHook, + ) { + unsafe { + pg_sys::DefineCustomRealVariable( + name.as_ptr(), + short_description.as_ptr(), + long_description.as_ptr(), + setting.value.as_ptr(), + setting.value.get(), + min_value, + max_value, + context as isize as _, + flags.bits(), + check_hook, + assign_hook, + show_hook, + ); + } + } + + /// Define an enum GUC with custom hooks. + /// + /// # Safety + /// + /// This function is unsafe because hook functions must be properly guarded against Rust panics. + /// Any hook function that might panic must be marked with `#[pg_guard]` to ensure proper + /// conversion of Rust panics into PostgreSQL errors. + pub unsafe fn define_enum_guc_with_hooks( + name: &'static CStr, + short_description: &'static CStr, + long_description: &'static CStr, + setting: &'static GucSetting, + context: GucContext, + flags: GucFlags, + check_hook: pg_sys::GucEnumCheckHook, + assign_hook: pg_sys::GucEnumAssignHook, + show_hook: pg_sys::GucShowHook, + ) { + setting.value.set(setting.boot_val.to_ordinal()); + unsafe { + pg_sys::DefineCustomEnumVariable( + name.as_ptr(), + short_description.as_ptr(), + long_description.as_ptr(), + setting.value.as_ptr(), + setting.value.get(), + T::CONFIG_ENUM_ENTRY, + context as isize as _, + flags.bits(), + check_hook, + assign_hook, + show_hook, + ); + } + } }