Skip to content

Commit b28919b

Browse files
authored
Add #[pg_guc_hook] macro for simple GUC hooks (#2323)
This adds a `#[pg_guc_hook]` attribute macro that wraps a Rust function to do the unsafe work of converting to and from `GucValue` types. With this macro, a check hook can return a `GucCheckError` type to populate the error code, detail, and hint when rejecting a value. The signatures of `GucRegistry::define_*_guc_with_hooks` do not change.
1 parent 0d03f5b commit b28919b

5 files changed

Lines changed: 463 additions & 6 deletions

File tree

pgrx-macros/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Provides:
66

77
- #[pg_extern]
88
- #[pg_guard]
9+
- #[pg_guc_hook]
910
- #[pg_test]
1011
- #[derive(PostgresType)]
1112
- #[derive(PostgresEnum)]

pgrx-macros/src/lib.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,6 +1429,197 @@ pub fn pgrx(_attr: TokenStream, item: TokenStream) -> TokenStream {
14291429
item
14301430
}
14311431

1432+
/**
1433+
Declare a function as a GUC hook. This takes one argument: `show`, `check`, or `assign`.
1434+
1435+
The first parameter of `check` and `assign` hooks must implement `pgrx::guc::GucValue`.
1436+
1437+
Examples:
1438+
```rust,ignore
1439+
#[pg_guc_hook(show)]
1440+
fn my_show_hook() -> String {
1441+
"CUSTOM_VALUE".to_string()
1442+
}
1443+
1444+
#[pg_guc_hook(check)]
1445+
fn my_check_hook(newval: i32) -> Result<(), GucCheckError> {
1446+
// accept or reject newval
1447+
}
1448+
1449+
#[pg_guc_hook(assign)]
1450+
fn my_assign_hook(newval: i32) {
1451+
// do more now that every change is accepted
1452+
}
1453+
```
1454+
1455+
# Check Hooks
1456+
1457+
This macro adapts check hooks of multiple forms.
1458+
1459+
The simplest of these takes a single argument and returns a `bool`.
1460+
- `true` means the value is valid, and Postgres will store that value in the GUC variable.
1461+
- `false` means the value is invalid, and Postgres will produce an error message automatically.
1462+
1463+
```rust,ignore
1464+
#[pg_guc_hook(check)]
1465+
fn my_check_hook(newval: i32) -> bool { newval > 0 }
1466+
```
1467+
1468+
To apply your own error message for an invalid value, return a `Result<(), GucCheckError>`.
1469+
- `Ok` means the value is valid.
1470+
- `Err` means the value is invalid, and `pgrx` will pass the contents of `GucCheckError` to Postgres.
1471+
1472+
```rust,ignore
1473+
#[pg_guc_hook(check)]
1474+
fn my_check_hook(newval: i32) -> Result<(), GucCheckError> {
1475+
if newval > 0 {
1476+
Ok(())
1477+
} else {
1478+
Err(
1479+
GucCheckError::new("value cannot be negative")
1480+
.with_hint("to configure the opposite behavior, set other_param"),
1481+
)
1482+
}
1483+
}
1484+
```
1485+
1486+
A check hook can also accept a second argument that indicates when the parameter is changing.
1487+
Like the examples above, this can return a bool or a Result.
1488+
1489+
```rust,ignore
1490+
#[pg_guc_hook(check)]
1491+
fn my_check_hook(newval: i32, source: pg_sys::GucSource::Type) -> bool { true }
1492+
```
1493+
*/
1494+
#[proc_macro_attribute]
1495+
pub fn pg_guc_hook(attr: TokenStream, item: TokenStream) -> TokenStream {
1496+
let hook_type = parse_macro_input!(attr as syn::Ident);
1497+
let mut func = parse_macro_input!(item as syn::ItemFn);
1498+
1499+
// Rename the original function and use its name.
1500+
let original_ident = func.sig.ident.clone();
1501+
let inner_ident = format_ident!("{}_INNER", original_ident);
1502+
func.sig.ident = inner_ident.clone();
1503+
1504+
// Remove the original visibility and use it.
1505+
let vis = &func.vis;
1506+
let mut inner_func = func.clone();
1507+
inner_func.vis = syn::Visibility::Inherited;
1508+
1509+
let wrapper = match hook_type.to_string().as_str() {
1510+
"show" => {
1511+
quote! {
1512+
#[::pgrx::pg_guard]
1513+
#vis unsafe extern "C-unwind" fn #original_ident() -> *const ::core::ffi::c_char {
1514+
#[inline(always)]
1515+
#inner_func
1516+
1517+
let show = #inner_ident();
1518+
::pgrx::pg_sys::AsPgCStr::as_pg_cstr(&show)
1519+
}
1520+
}
1521+
}
1522+
"check" => {
1523+
let invoke_inner = match func.sig.inputs.len() {
1524+
1 => quote! { #inner_ident(value) },
1525+
2 => quote! { #inner_ident(value, source) },
1526+
_ => {
1527+
return syn::Error::new(
1528+
func.sig.span(),
1529+
"check hook must have one or two arguments",
1530+
)
1531+
.into_compile_error()
1532+
.into();
1533+
}
1534+
};
1535+
1536+
let arg_type = match func.sig.inputs.first() {
1537+
Some(syn::FnArg::Typed(pat_type)) => &pat_type.ty,
1538+
_ => {
1539+
return syn::Error::new(
1540+
func.sig.span(),
1541+
"check hook first argument must implement GucValue",
1542+
)
1543+
.into_compile_error()
1544+
.into();
1545+
}
1546+
};
1547+
1548+
// The original function may return bool or Result. Pass bool through directly and apply
1549+
// any details present in GucCheckError.
1550+
let invoke_to_bool = if let syn::ReturnType::Type(_, return_type) = &func.sig.output
1551+
&& let syn::Type::Path(type_path) = &**return_type
1552+
&& type_path.path.segments.last().map_or(false, |seg| seg.ident == "bool")
1553+
{
1554+
quote! { #invoke_inner }
1555+
} else {
1556+
quote! {
1557+
match #invoke_inner {
1558+
Ok(()) => true,
1559+
Err(err) => {
1560+
unsafe { ::pgrx::guc::GucCheckError::apply(err) };
1561+
false
1562+
}
1563+
}
1564+
}
1565+
};
1566+
1567+
quote! {
1568+
#[::pgrx::pg_guard]
1569+
#vis unsafe extern "C-unwind" fn #original_ident(
1570+
newval: *mut <#arg_type as ::pgrx::guc::GucValue>::Raw,
1571+
extra: *mut *mut ::core::ffi::c_void,
1572+
source: ::pgrx::pg_sys::GucSource::Type,
1573+
) -> bool {
1574+
#[inline(always)]
1575+
#inner_func
1576+
1577+
debug_assert!(!newval.is_null());
1578+
let value = unsafe { <#arg_type as ::pgrx::guc::GucValue>::from_raw(*newval) };
1579+
#invoke_to_bool
1580+
}
1581+
}
1582+
}
1583+
"assign" => {
1584+
let arg_type = match func.sig.inputs.first() {
1585+
Some(syn::FnArg::Typed(pat_type)) => &pat_type.ty,
1586+
_ => {
1587+
return syn::Error::new(
1588+
func.sig.span(),
1589+
"assign hook first argument must implement GucValue",
1590+
)
1591+
.into_compile_error()
1592+
.into();
1593+
}
1594+
};
1595+
1596+
quote! {
1597+
#[::pgrx::pg_guard]
1598+
#vis unsafe extern "C-unwind" fn #original_ident(
1599+
newval: <#arg_type as ::pgrx::guc::GucValue>::Raw,
1600+
extra: *mut ::core::ffi::c_void,
1601+
) {
1602+
#[inline(always)]
1603+
#inner_func
1604+
1605+
let value = unsafe { <#arg_type as ::pgrx::guc::GucValue>::from_raw(newval) };
1606+
#inner_ident(value);
1607+
}
1608+
}
1609+
}
1610+
_ => {
1611+
return syn::Error::new(
1612+
hook_type.span(),
1613+
"Unknown GUC hook type. Expected 'show', 'check', or 'assign'",
1614+
)
1615+
.into_compile_error()
1616+
.into();
1617+
}
1618+
};
1619+
1620+
wrapper.into()
1621+
}
1622+
14321623
/**
14331624
Create a [PostgreSQL trigger function](https://www.postgresql.org/docs/current/plpgsql-trigger.html)
14341625

0 commit comments

Comments
 (0)