Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions pgrx-tests/src/tests/guc_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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<i32> = 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<bool> = GucSetting::<bool>::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<bool> = GucSetting::<bool>::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<i32> = 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<bool> = GucSetting::<bool>::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<bool> = GucSetting::<bool>::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");
});
}
}
189 changes: 189 additions & 0 deletions pgrx/src/guc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ impl<T: GucEnum> GucSetting<T> {
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,
Expand Down Expand Up @@ -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<bool>,
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<i32>,
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<Option<CString>>,
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<f64>,
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<T: GucEnum>(
name: &'static CStr,
short_description: &'static CStr,
long_description: &'static CStr,
setting: &'static GucSetting<T>,
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,
);
}
}
}
Loading