Skip to content

Commit c0de15d

Browse files
authored
Expose guc hooks (pgcentralfoundation#2075)
Exposing the GUC hooks to insert extern C code that will execute before assigning a GUC/while assigning a GUC/`show` of GUC.
1 parent 5925df2 commit c0de15d

2 files changed

Lines changed: 334 additions & 0 deletions

File tree

pgrx-tests/src/tests/guc_tests.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
mod tests {
1313
#[allow(unused_imports)]
1414
use crate as pgrx_tests;
15+
use std::ffi::c_char;
1516
use std::ffi::CString;
1617

1718
use pgrx::guc::*;
@@ -231,4 +232,148 @@ mod tests {
231232
assert_eq!(GUC_NO_SHOW.get(), true, "'no_show' should reset after 'RESET ALL'");
232233
});
233234
}
235+
236+
#[pg_test]
237+
#[should_panic(expected = "invalid value for parameter \"test.hooks\": 0")]
238+
fn test_guc_check_hook() {
239+
static SIDE_EFFECT: std::sync::RwLock<i32> = std::sync::RwLock::new(0);
240+
241+
#[pg_guard]
242+
unsafe extern "C-unwind" fn check_hook(
243+
newval: *mut bool,
244+
_extra: *mut *mut std::ffi::c_void,
245+
_source: pg_sys::GucSource::Type,
246+
) -> bool {
247+
if *newval {
248+
*SIDE_EFFECT.write().unwrap() += 1;
249+
}
250+
*newval
251+
}
252+
253+
// Create and register GUC with hooks. As default is true, SIDE_EFFECT will be 1.
254+
static GUC: GucSetting<bool> = GucSetting::<bool>::new(true);
255+
unsafe {
256+
GucRegistry::define_bool_guc_with_hooks(
257+
c"test.hooks",
258+
c"test hooks guc",
259+
c"test hooks guc",
260+
&GUC,
261+
GucContext::Userset,
262+
GucFlags::default(),
263+
Some(check_hook),
264+
None,
265+
None,
266+
);
267+
}
268+
269+
// Test check hook - should reject false and not initialize the GUC
270+
assert!(
271+
Spi::run("SET test.hooks TO false").is_err(),
272+
"Expected panic when setting test.hooks to false"
273+
);
274+
assert_eq!(*SIDE_EFFECT.read().unwrap(), 1);
275+
276+
// Test check hook - should accept true and increment SIDE_EFFECT
277+
assert!(Spi::run("SET test.hooks TO true").is_ok());
278+
assert_eq!(GUC.get(), true);
279+
assert_eq!(*SIDE_EFFECT.read().unwrap(), 2);
280+
}
281+
282+
#[pg_test]
283+
#[should_panic(expected = "should panic!")]
284+
fn test_check_hook_fail() {
285+
#[pg_guard]
286+
unsafe extern "C-unwind" fn check_hook(
287+
newval: *mut bool,
288+
_extra: *mut *mut std::ffi::c_void,
289+
_source: pg_sys::GucSource::Type,
290+
) -> bool {
291+
if *newval {
292+
panic!("should panic!");
293+
}
294+
*newval
295+
}
296+
297+
static GUARDED_GUC: GucSetting<bool> = GucSetting::<bool>::new(true);
298+
unsafe {
299+
GucRegistry::define_bool_guc_with_hooks(
300+
c"test.guarded_hooks",
301+
c"test guarded hooks guc",
302+
c"test guarded hooks guc",
303+
&GUARDED_GUC,
304+
GucContext::Userset,
305+
GucFlags::default(),
306+
Some(check_hook),
307+
None,
308+
None,
309+
);
310+
}
311+
}
312+
313+
#[pg_test]
314+
fn test_assign_hook() {
315+
static SIDE_EFFECT: std::sync::RwLock<i32> = std::sync::RwLock::new(0);
316+
317+
#[pg_guard]
318+
unsafe extern "C-unwind" fn assign_hook(newval: bool, _extra: *mut ::core::ffi::c_void) {
319+
if newval {
320+
*SIDE_EFFECT.write().unwrap() += 1;
321+
}
322+
}
323+
324+
// Create and register GUC with hooks. As default is false, SIDE_EFFECT will be 0.
325+
static GUC: GucSetting<bool> = GucSetting::<bool>::new(false);
326+
unsafe {
327+
GucRegistry::define_bool_guc_with_hooks(
328+
c"test.hooks",
329+
c"test hooks guc",
330+
c"test hooks guc",
331+
&GUC,
332+
GucContext::Userset,
333+
GucFlags::default(),
334+
None,
335+
Some(assign_hook),
336+
None,
337+
);
338+
}
339+
340+
// SIDE_EFFECT should not be updated
341+
Spi::run("SET test.hooks TO false").unwrap();
342+
assert_eq!(*SIDE_EFFECT.read().unwrap(), 0);
343+
344+
// SIDE_EFFECT should be updated
345+
Spi::run("SET test.hooks TO true").unwrap();
346+
assert_eq!(*SIDE_EFFECT.read().unwrap(), 1);
347+
}
348+
349+
#[pg_test]
350+
fn test_show_hook() {
351+
#[pg_guard]
352+
unsafe extern "C-unwind" fn show_hook() -> *const c_char {
353+
CString::new("CUSTOM_SHOW_HOOK").unwrap().into_raw() as *const c_char
354+
}
355+
356+
// Register GUC
357+
static GUC: GucSetting<bool> = GucSetting::<bool>::new(false);
358+
unsafe {
359+
GucRegistry::define_bool_guc_with_hooks(
360+
c"test.hooks",
361+
c"test hooks guc",
362+
c"test hooks guc",
363+
&GUC,
364+
GucContext::Userset,
365+
GucFlags::default(),
366+
None,
367+
None,
368+
Some(show_hook),
369+
);
370+
}
371+
372+
// Test show hook
373+
Spi::connect_mut(|client| {
374+
let r = client.update("SHOW test.hooks", None, &[]).expect("SPI failed");
375+
let value: &str = r.first().get_one::<&str>().unwrap().unwrap();
376+
assert_eq!(value, "CUSTOM_SHOW_HOOK");
377+
});
378+
}
234379
}

pgrx/src/guc.rs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ impl<T: GucEnum> GucSetting<T> {
225225
pub struct GucRegistry {}
226226

227227
impl GucRegistry {
228+
// GUC Registration functions that do not expose hooks
228229
pub fn define_bool_guc(
229230
name: &'static CStr,
230231
short_description: &'static CStr,
@@ -354,4 +355,192 @@ impl GucRegistry {
354355
);
355356
}
356357
}
358+
359+
/// Define a boolean GUC with custom hooks.
360+
///
361+
/// # Hooks
362+
///
363+
/// * `check_hook` - Validates new values. Return false to reject.
364+
/// * `assign_hook` - Called after value is set. Use for side effects.
365+
/// * `show_hook` - Returns custom display string for SHOW commands.
366+
///
367+
/// # Safety
368+
///
369+
/// This function is unsafe because hook functions must be properly guarded against Rust panics.
370+
/// Any hook function that might panic must be marked with `#[pg_guard]` to ensure proper
371+
/// conversion of Rust panics into PostgreSQL errors.
372+
///
373+
pub unsafe fn define_bool_guc_with_hooks(
374+
name: &'static CStr,
375+
short_description: &'static CStr,
376+
long_description: &'static CStr,
377+
setting: &'static GucSetting<bool>,
378+
context: GucContext,
379+
flags: GucFlags,
380+
check_hook: pg_sys::GucBoolCheckHook,
381+
assign_hook: pg_sys::GucBoolAssignHook,
382+
show_hook: pg_sys::GucShowHook,
383+
) {
384+
unsafe {
385+
pg_sys::DefineCustomBoolVariable(
386+
name.as_ptr(),
387+
short_description.as_ptr(),
388+
long_description.as_ptr(),
389+
setting.value.as_ptr(),
390+
setting.value.get(),
391+
context as isize as _,
392+
flags.bits(),
393+
check_hook,
394+
assign_hook,
395+
show_hook,
396+
);
397+
}
398+
}
399+
400+
/// Define an integer GUC with custom hooks.
401+
///
402+
/// # Safety
403+
///
404+
/// This function is unsafe because hook functions must be properly guarded against Rust panics.
405+
/// Any hook function that might panic must be marked with `#[pg_guard]` to ensure proper
406+
/// conversion of Rust panics into PostgreSQL errors.
407+
pub unsafe fn define_int_guc_with_hooks(
408+
name: &'static CStr,
409+
short_description: &'static CStr,
410+
long_description: &'static CStr,
411+
setting: &'static GucSetting<i32>,
412+
min_value: i32,
413+
max_value: i32,
414+
context: GucContext,
415+
flags: GucFlags,
416+
check_hook: pg_sys::GucIntCheckHook,
417+
assign_hook: pg_sys::GucIntAssignHook,
418+
show_hook: pg_sys::GucShowHook,
419+
) {
420+
unsafe {
421+
pg_sys::DefineCustomIntVariable(
422+
name.as_ptr(),
423+
short_description.as_ptr(),
424+
long_description.as_ptr(),
425+
setting.value.as_ptr(),
426+
setting.value.get(),
427+
min_value,
428+
max_value,
429+
context as isize as _,
430+
flags.bits(),
431+
check_hook,
432+
assign_hook,
433+
show_hook,
434+
)
435+
}
436+
}
437+
438+
/// Define a string GUC with custom hooks.
439+
///
440+
/// # Safety
441+
///
442+
/// This function is unsafe because hook functions must be properly guarded against Rust panics.
443+
/// Any hook function that might panic must be marked with `#[pg_guard]` to ensure proper
444+
/// conversion of Rust panics into PostgreSQL errors.
445+
pub unsafe fn define_string_guc_with_hooks(
446+
name: &'static CStr,
447+
short_description: &'static CStr,
448+
long_description: &'static CStr,
449+
setting: &'static GucSetting<Option<CString>>,
450+
context: GucContext,
451+
flags: GucFlags,
452+
check_hook: pg_sys::GucStringCheckHook,
453+
assign_hook: pg_sys::GucStringAssignHook,
454+
show_hook: pg_sys::GucShowHook,
455+
) {
456+
unsafe {
457+
pg_sys::DefineCustomStringVariable(
458+
name.as_ptr(),
459+
short_description.as_ptr(),
460+
long_description.as_ptr(),
461+
setting.value.as_ptr(),
462+
setting.value.get(),
463+
context as isize as _,
464+
flags.bits(),
465+
check_hook,
466+
assign_hook,
467+
show_hook,
468+
);
469+
}
470+
}
471+
472+
/// Define a float GUC with custom hooks.
473+
///
474+
/// # Safety
475+
///
476+
/// This function is unsafe because hook functions must be properly guarded against Rust panics.
477+
/// Any hook function that might panic must be marked with `#[pg_guard]` to ensure proper
478+
/// conversion of Rust panics into PostgreSQL errors.
479+
///
480+
pub fn define_float_guc_with_hooks(
481+
name: &'static CStr,
482+
short_description: &'static CStr,
483+
long_description: &'static CStr,
484+
setting: &'static GucSetting<f64>,
485+
min_value: f64,
486+
max_value: f64,
487+
context: GucContext,
488+
flags: GucFlags,
489+
check_hook: pg_sys::GucRealCheckHook,
490+
assign_hook: pg_sys::GucRealAssignHook,
491+
show_hook: pg_sys::GucShowHook,
492+
) {
493+
unsafe {
494+
pg_sys::DefineCustomRealVariable(
495+
name.as_ptr(),
496+
short_description.as_ptr(),
497+
long_description.as_ptr(),
498+
setting.value.as_ptr(),
499+
setting.value.get(),
500+
min_value,
501+
max_value,
502+
context as isize as _,
503+
flags.bits(),
504+
check_hook,
505+
assign_hook,
506+
show_hook,
507+
);
508+
}
509+
}
510+
511+
/// Define an enum GUC with custom hooks.
512+
///
513+
/// # Safety
514+
///
515+
/// This function is unsafe because hook functions must be properly guarded against Rust panics.
516+
/// Any hook function that might panic must be marked with `#[pg_guard]` to ensure proper
517+
/// conversion of Rust panics into PostgreSQL errors.
518+
pub unsafe fn define_enum_guc_with_hooks<T: GucEnum>(
519+
name: &'static CStr,
520+
short_description: &'static CStr,
521+
long_description: &'static CStr,
522+
setting: &'static GucSetting<T>,
523+
context: GucContext,
524+
flags: GucFlags,
525+
check_hook: pg_sys::GucEnumCheckHook,
526+
assign_hook: pg_sys::GucEnumAssignHook,
527+
show_hook: pg_sys::GucShowHook,
528+
) {
529+
setting.value.set(setting.boot_val.to_ordinal());
530+
unsafe {
531+
pg_sys::DefineCustomEnumVariable(
532+
name.as_ptr(),
533+
short_description.as_ptr(),
534+
long_description.as_ptr(),
535+
setting.value.as_ptr(),
536+
setting.value.get(),
537+
T::CONFIG_ENUM_ENTRY,
538+
context as isize as _,
539+
flags.bits(),
540+
check_hook,
541+
assign_hook,
542+
show_hook,
543+
);
544+
}
545+
}
357546
}

0 commit comments

Comments
 (0)