On 12/5/2025 3:24 PM, Bryan Green wrote:
> On 12/5/2025 2:48 PM, Robert Haas wrote:
>> On Fri, Dec 5, 2025 at 12:45 AM Bryan Green <[email protected]> wrote:
>>> I tried implementing a PG_TRY/PG_CATCH approach and it doesn't work.
>>> The switch statement in set_config_with_handle() has multiple early
>>> returns (parse failures, prohibitValueChange checks, etc.) that bypass
>>> both the success path and the PG_CATCH handler. If we've switched into
>>> extra_cxt before entering the switch, these early returns leave
>>> CurrentMemoryContext pointing at a temp context.
>>
>> I'm pretty sure it's not intended that you can return out of a
>> PG_CATCH() block. You could, however, modify the control flow so that
>> you stash the return value in a variable and the actual return happens
>> after you exit the PG_CATCH() block.
>>
>
> I should have been more clear, I was referring to trying the following:
>
>
> if (GUC_EXTRA_IS_CONTEXT && value != NULL)
> {
> extra_cxt = AllocSetContextCreate(CurrentMemoryContext, ...);
> old_context = MemoryContextSwitchTo(extra_cxt);
> }
>
> PG_TRY();
> {
> switch (record->vartype) { ... } /* DIFFERENT RETURN PATHS */
>
> /* Success path */
> if (extra_cxt)
> {
> MemoryContextSwitchTo(old_context);
> MemoryContextSetParent(extra_cxt, GUCMemoryContext);
> }
> }
> PG_CATCH();
> {
> if (extra_cxt)
> MemoryContextDelete(extra_cxt);
> PG_RE_THROW();
> }
> PG_END_TRY();
>
> The early returns are inside the PG_TRY block (in the switch
> statement), not in PG_CATCH. But I see your point - I could refactor
> to use a result variable and only return after PG_END_TRY.
>
> Some of the "return 0" paths happen after the check hook has already
> run and allocated into extra_cxt. If I just break out of the switch
> to avoid the return, I'd still need to distinguish "should I reparent
> this context (success) or delete it (failure)" before exiting PG_TRY.
>
>> But I also don't understand why you want to use a PG_CATCH() block
>> here in the first place. At first glance, I'm inclined to wonder why
>> this wouldn't be a new wrinkle for the existing logic in
>> call_string_check_hook.
>>
>
> I think I'm missing something obvious here. call_string_check_hook
> doesn't do any memory context management - it just calls the hook.
>
> Are you suggesting the context creation/switching should be factored
> into the call_*_check_hook functions themselves? That would keep it
> out of the main switch statement entirely. Something like:
>
> if (record->flags & GUC_EXTRA_IS_CONTEXT)
> return call_string_check_hook_with_context(...);
> else
> return call_string_check_hook(...);
>
> Where the _with_context version handles creating the temp context,
> switching into it, calling the hook, switching back, and cleaning up
> on failure?
>
> That would avoid touching the switch statement at all. Is that what
> you had in mind?
>
>>> The check hook API would be:
>>>
>>> MemoryContext oldcxt = MemoryContextSwitchTo(extra_cxt);
>>> /* allocate complex structures with palloc */
>>> MemoryContextSwitchTo(oldcxt);
>>> *extra = my_data_pointer;
>>>
>>> Not as automatic as Robert's suggestion, but it avoids the early return
>>> problem entirely.
>>
>> This wouldn't be terrible or anything, and someone may prefer it on
>> stylistic grounds, but I don't really think I believe your argument
>> that this is the only way it can work.
>>
>
> I did not mean to imply that this is the ONLY way it could work-- it was
> just the solution that was in my mind currently. I always assume there
> are multiple ways.
>
> Thanks
>
Robert,
I've implemented the GUC_EXTRA_IS_CONTEXT approach I believe you were
suggesting. The basic idea is straightforward: the check hook wrapper
creates a temporary AllocSetContext, switches to it before calling the
hook, then either reparents the context to GUCMemoryContext on success
or deletes it on failure. Cleanup in set_extra_field() uses
GetMemoryChunkContext() to locate and delete the old context.
This required modifications to all five call_*_check_hook() functions
(bool, int, real, string, enum) to follow the same pattern. I also had
to keep the context operations outside the PG_TRY block.
One additional fix: if a check hook succeeds but returns NULL for extra,
we delete the empty context rather than reparenting it to avoid leaking
contexts that would never be cleaned up.
Does this match what you had in mind?
Patch attached.
--
Bryan Green
EDB: https://www.enterprisedb.com
From efa009566623e214b9859efeea768efdf9b00e85 Mon Sep 17 00:00:00 2001
From: Bryan Green <[email protected]>
Date: Fri, 5 Dec 2025 23:56:33 -0600
Subject: [PATCH v2] Allow complex data for GUC extra.
Add support for GUC_EXTRA_IS_CONTEXT flag that allows check hooks to
allocate their extra data in a dedicated memory context. This eliminates
the need for manual memory management in assign and free hooks.
When GUC_EXTRA_IS_CONTEXT is set, the check hook functions
(call_bool_check_hook, call_int_check_hook, call_real_check_hook,
call_string_check_hook, call_enum_check_hook) create a temporary
AllocSetContext before calling the check hook. If the hook succeeds,
the context is reparented to GUCMemoryContext. If it fails, the context
is deleted. The cleanup in set_extra_field() uses GetMemoryChunkContext()
to find and delete the context when freeing old values.
Author: Bryan Green <[email protected]>
Suggested-by: Robert Haas
---
src/backend/utils/misc/guc.c | 174 +++++++++-
src/include/utils/guc.h | 1 +
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_guc/Makefile | 23 ++
.../modules/test_guc/expected/test_guc.out | 188 +++++++++++
src/test/modules/test_guc/meson.build | 33 ++
src/test/modules/test_guc/sql/test_guc.sql | 68 ++++
src/test/modules/test_guc/test_guc--1.0.sql | 24 ++
src/test/modules/test_guc/test_guc.c | 310 ++++++++++++++++++
src/test/modules/test_guc/test_guc.control | 5 +
11 files changed, 822 insertions(+), 6 deletions(-)
create mode 100644 src/test/modules/test_guc/Makefile
create mode 100644 src/test/modules/test_guc/expected/test_guc.out
create mode 100644 src/test/modules/test_guc/meson.build
create mode 100644 src/test/modules/test_guc/sql/test_guc.sql
create mode 100644 src/test/modules/test_guc/test_guc--1.0.sql
create mode 100644 src/test/modules/test_guc/test_guc.c
create mode 100644 src/test/modules/test_guc/test_guc.control
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index c6484aea08..e743269a0d 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -764,6 +764,7 @@ extra_field_used(struct config_generic *gconf, void *extra)
static void
set_extra_field(struct config_generic *gconf, void **field, void *newval)
{
+ MemoryContext ctx = NULL;
void *oldval = *field;
/* Do the assignment */
@@ -771,7 +772,15 @@ set_extra_field(struct config_generic *gconf, void
**field, void *newval)
/* Free old value if it's not NULL and isn't referenced anymore */
if (oldval && !extra_field_used(gconf, oldval))
- guc_free(oldval);
+ {
+ if(gconf->flags & GUC_EXTRA_IS_CONTEXT)
+ {
+ ctx = GetMemoryChunkContext(oldval);
+ MemoryContextDelete(ctx);
+ }
+ else
+ guc_free(oldval);
+ }
}
/*
@@ -6641,17 +6650,47 @@ static bool
call_bool_check_hook(const struct config_generic *conf, bool *newval, void
**extra,
GucSource source, int elevel)
{
+ MemoryContext extra_cxt = NULL;
+ MemoryContext old_cxt = NULL;
+ bool result;
+
/* Quick success if no hook */
if (!conf->_bool.check_hook)
return true;
+ if (conf->flags & GUC_EXTRA_IS_CONTEXT)
+ {
+ extra_cxt = AllocSetContextCreate(CurrentMemoryContext,
+
"GUC check_hook extra context",
+
ALLOCSET_DEFAULT_SIZES);
+ old_cxt = MemoryContextSwitchTo(extra_cxt);
+ }
+
/* Reset variables that might be set by hook */
GUC_check_errcode_value = ERRCODE_INVALID_PARAMETER_VALUE;
GUC_check_errmsg_string = NULL;
GUC_check_errdetail_string = NULL;
GUC_check_errhint_string = NULL;
- if (!conf->_bool.check_hook(newval, extra, source))
+ result = conf->_bool.check_hook(newval, extra, source);
+
+ if (conf->flags & GUC_EXTRA_IS_CONTEXT)
+ {
+ MemoryContextSwitchTo(old_cxt);
+ if (result)
+ {
+ if (*extra != NULL)
+ MemoryContextSetParent(extra_cxt,
GUCMemoryContext);
+ else
+ MemoryContextDelete(extra_cxt);
+ }
+ else
+ {
+ MemoryContextDelete(extra_cxt);
+ }
+ }
+
+ if (!result)
{
ereport(elevel,
(errcode(GUC_check_errcode_value),
@@ -6675,17 +6714,47 @@ static bool
call_int_check_hook(const struct config_generic *conf, int *newval, void
**extra,
GucSource source, int elevel)
{
+ MemoryContext extra_cxt = NULL;
+ MemoryContext old_cxt = NULL;
+ bool result;
+
/* Quick success if no hook */
if (!conf->_int.check_hook)
return true;
+ if (conf->flags & GUC_EXTRA_IS_CONTEXT)
+ {
+ extra_cxt = AllocSetContextCreate(CurrentMemoryContext,
+
"GUC check_hook extra context",
+
ALLOCSET_DEFAULT_SIZES);
+ old_cxt = MemoryContextSwitchTo(extra_cxt);
+ }
+
/* Reset variables that might be set by hook */
GUC_check_errcode_value = ERRCODE_INVALID_PARAMETER_VALUE;
GUC_check_errmsg_string = NULL;
GUC_check_errdetail_string = NULL;
GUC_check_errhint_string = NULL;
- if (!conf->_int.check_hook(newval, extra, source))
+ result = conf->_int.check_hook(newval, extra, source);
+
+ if (conf->flags & GUC_EXTRA_IS_CONTEXT)
+ {
+ MemoryContextSwitchTo(old_cxt);
+ if (result)
+ {
+ if (*extra != NULL)
+ MemoryContextSetParent(extra_cxt,
GUCMemoryContext);
+ else
+ MemoryContextDelete(extra_cxt);
+ }
+ else
+ {
+ MemoryContextDelete(extra_cxt);
+ }
+ }
+
+ if (!result)
{
ereport(elevel,
(errcode(GUC_check_errcode_value),
@@ -6709,17 +6778,47 @@ static bool
call_real_check_hook(const struct config_generic *conf, double *newval, void
**extra,
GucSource source, int elevel)
{
+ MemoryContext extra_cxt = NULL;
+ MemoryContext old_cxt = NULL;
+ bool result;
+
/* Quick success if no hook */
if (!conf->_real.check_hook)
return true;
+ if (conf->flags & GUC_EXTRA_IS_CONTEXT)
+ {
+ extra_cxt = AllocSetContextCreate(CurrentMemoryContext,
+
"GUC check_hook extra context",
+
ALLOCSET_DEFAULT_SIZES);
+ old_cxt = MemoryContextSwitchTo(extra_cxt);
+ }
+
/* Reset variables that might be set by hook */
GUC_check_errcode_value = ERRCODE_INVALID_PARAMETER_VALUE;
GUC_check_errmsg_string = NULL;
GUC_check_errdetail_string = NULL;
GUC_check_errhint_string = NULL;
- if (!conf->_real.check_hook(newval, extra, source))
+ result = conf->_real.check_hook(newval, extra, source);
+
+ if (conf->flags & GUC_EXTRA_IS_CONTEXT)
+ {
+ MemoryContextSwitchTo(old_cxt);
+ if (result)
+ {
+ if (*extra != NULL)
+ MemoryContextSetParent(extra_cxt,
GUCMemoryContext);
+ else
+ MemoryContextDelete(extra_cxt);
+ }
+ else
+ {
+ MemoryContextDelete(extra_cxt);
+ }
+ }
+
+ if (!result)
{
ereport(elevel,
(errcode(GUC_check_errcode_value),
@@ -6743,12 +6842,22 @@ static bool
call_string_check_hook(const struct config_generic *conf, char **newval, void
**extra,
GucSource source, int elevel)
{
+ MemoryContext extra_cxt = NULL;
+ MemoryContext old_cxt = NULL;
volatile bool result = true;
/* Quick success if no hook */
if (!conf->_string.check_hook)
return true;
+ if (conf->flags & GUC_EXTRA_IS_CONTEXT)
+ {
+ extra_cxt = AllocSetContextCreate(CurrentMemoryContext,
+
"GUC check_hook extra context",
+
ALLOCSET_DEFAULT_SIZES);
+ old_cxt = MemoryContextSwitchTo(extra_cxt);
+ }
+
/*
* If elevel is ERROR, or if the check_hook itself throws an elog
* (undesirable, but not always avoidable), make sure we don't leak the
@@ -6762,7 +6871,9 @@ call_string_check_hook(const struct config_generic *conf,
char **newval, void **
GUC_check_errdetail_string = NULL;
GUC_check_errhint_string = NULL;
- if (!conf->_string.check_hook(newval, extra, source))
+ result = conf->_string.check_hook(newval, extra, source);
+
+ if (!result)
{
ereport(elevel,
(errcode(GUC_check_errcode_value),
@@ -6781,11 +6892,32 @@ call_string_check_hook(const struct config_generic
*conf, char **newval, void **
}
PG_CATCH();
{
+ if (conf->flags & GUC_EXTRA_IS_CONTEXT)
+ {
+ MemoryContextSwitchTo(old_cxt);
+ MemoryContextDelete(extra_cxt);
+ }
guc_free(*newval);
PG_RE_THROW();
}
PG_END_TRY();
+ if (conf->flags & GUC_EXTRA_IS_CONTEXT)
+ {
+ MemoryContextSwitchTo(old_cxt);
+ if (result)
+ {
+ if (*extra != NULL)
+ MemoryContextSetParent(extra_cxt,
GUCMemoryContext);
+ else
+ MemoryContextDelete(extra_cxt);
+ }
+ else
+ {
+ MemoryContextDelete(extra_cxt);
+ }
+ }
+
return result;
}
@@ -6793,17 +6925,47 @@ static bool
call_enum_check_hook(const struct config_generic *conf, int *newval, void
**extra,
GucSource source, int elevel)
{
+ MemoryContext extra_cxt = NULL;
+ MemoryContext old_cxt = NULL;
+ bool result;
+
/* Quick success if no hook */
if (!conf->_enum.check_hook)
return true;
+ if (conf->flags & GUC_EXTRA_IS_CONTEXT)
+ {
+ extra_cxt = AllocSetContextCreate(CurrentMemoryContext,
+
"GUC check_hook extra context",
+
ALLOCSET_DEFAULT_SIZES);
+ old_cxt = MemoryContextSwitchTo(extra_cxt);
+ }
+
/* Reset variables that might be set by hook */
GUC_check_errcode_value = ERRCODE_INVALID_PARAMETER_VALUE;
GUC_check_errmsg_string = NULL;
GUC_check_errdetail_string = NULL;
GUC_check_errhint_string = NULL;
- if (!conf->_enum.check_hook(newval, extra, source))
+ result = conf->_enum.check_hook(newval, extra, source);
+
+ if (conf->flags & GUC_EXTRA_IS_CONTEXT)
+ {
+ MemoryContextSwitchTo(old_cxt);
+ if (result)
+ {
+ if (*extra != NULL)
+ MemoryContextSetParent(extra_cxt,
GUCMemoryContext);
+ else
+ MemoryContextDelete(extra_cxt);
+ }
+ else
+ {
+ MemoryContextDelete(extra_cxt);
+ }
+ }
+
+ if (!result)
{
ereport(elevel,
(errcode(GUC_check_errcode_value),
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index f21ec37da8..1783301bce 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -228,6 +228,7 @@ typedef enum
0x002000 /* can't
set in PG_AUTOCONF_FILENAME */
#define GUC_RUNTIME_COMPUTED 0x004000 /* delay processing in 'postgres -C' */
#define GUC_ALLOW_IN_PARALLEL 0x008000 /* allow setting in parallel mode */
+#define GUC_EXTRA_IS_CONTEXT 0x010000 /* extra field is context pointer */
#define GUC_UNIT_KB 0x01000000 /* value is in kilobytes */
#define GUC_UNIT_BLOCKS 0x02000000 /* value is in blocks */
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index d079b91b1a..725b2bfe59 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -27,6 +27,7 @@ SUBDIRS = \
test_escape \
test_extensions \
test_ginpostinglist \
+ test_guc \
test_int128 \
test_integerset \
test_json_parser \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index cc57461e59..f00c8245f2 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -27,6 +27,7 @@ subdir('test_dsm_registry')
subdir('test_escape')
subdir('test_extensions')
subdir('test_ginpostinglist')
+subdir('test_guc')
subdir('test_int128')
subdir('test_integerset')
subdir('test_json_parser')
diff --git a/src/test/modules/test_guc/Makefile
b/src/test/modules/test_guc/Makefile
new file mode 100644
index 0000000000..32d48874ab
--- /dev/null
+++ b/src/test/modules/test_guc/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_guc/Makefile
+
+MODULE_big = test_guc
+OBJS = \
+ $(WIN32RES) \
+ test_guc.o
+
+EXTENSION = test_guc
+DATA = test_guc--1.0.sql
+PGFILEDESC = "test_guc - test module for GUC_EXTRA_IS_CONTEXT"
+
+REGRESS = test_guc
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_guc
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
\ No newline at end of file
diff --git a/src/test/modules/test_guc/expected/test_guc.out
b/src/test/modules/test_guc/expected/test_guc.out
new file mode 100644
index 0000000000..a2ed8a4292
--- /dev/null
+++ b/src/test/modules/test_guc/expected/test_guc.out
@@ -0,0 +1,188 @@
+CREATE EXTENSION test_guc;
+SET test_guc.counter = '42';
+SELECT get_counter_value();
+ get_counter_value
+-------------------
+ 42
+(1 row)
+
+SELECT get_counter_description();
+ get_counter_description
+-------------------------
+ Count is 42
+(1 row)
+
+SET test_guc.counter = '';
+SELECT get_counter_value();
+ get_counter_value
+-------------------
+ 0
+(1 row)
+
+SET test_guc.counter = '100';
+BEGIN;
+SET LOCAL test_guc.counter = '200';
+SELECT get_counter_value();
+ get_counter_value
+-------------------
+ 200
+(1 row)
+
+ROLLBACK;
+SELECT get_counter_value();
+ get_counter_value
+-------------------
+ 100
+(1 row)
+
+BEGIN;
+SET LOCAL test_guc.counter = '10';
+SAVEPOINT sp1;
+SET LOCAL test_guc.counter = '20';
+SELECT get_counter_value();
+ get_counter_value
+-------------------
+ 20
+(1 row)
+
+ROLLBACK TO sp1;
+SELECT get_counter_value();
+ get_counter_value
+-------------------
+ 10
+(1 row)
+
+ROLLBACK;
+SET test_guc.pool =
'prod:db1.example.com,db2.example.com,db3.example.com;max_connections=100;timeout=60';
+SELECT count_servers();
+ count_servers
+---------------
+ 3
+(1 row)
+
+SELECT get_pool_setting('pool_name');
+ get_pool_setting
+------------------
+ prod
+(1 row)
+
+SELECT get_pool_setting('max_connections');
+ get_pool_setting
+------------------
+ 100
+(1 row)
+
+SELECT get_pool_setting('timeout');
+ get_pool_setting
+------------------
+ 60
+(1 row)
+
+SELECT show_server_pool();
+ show_server_pool
+----------------------
+ Pool: prod +
+ Max connections: 100+
+ Timeout: 60 seconds +
+ Servers (3 total): +
+ - db1.example.com +
+ - db2.example.com +
+ - db3.example.com +
+
+(1 row)
+
+SET test_guc.pool = 'dev:localhost,192.168.1.10;max_connections=5;timeout=30';
+SELECT count_servers();
+ count_servers
+---------------
+ 2
+(1 row)
+
+SELECT get_pool_setting('pool_name');
+ get_pool_setting
+------------------
+ dev
+(1 row)
+
+SET test_guc.pool = '';
+SELECT count_servers();
+ count_servers
+---------------
+ 0
+(1 row)
+
+SET test_guc.pool = 'pool1:s1,s2;max_connections=10;timeout=20';
+BEGIN;
+SET LOCAL test_guc.pool = 'pool2:s3,s4,s5;max_connections=50;timeout=90';
+SELECT count_servers();
+ count_servers
+---------------
+ 3
+(1 row)
+
+SELECT get_pool_setting('pool_name');
+ get_pool_setting
+------------------
+ pool2
+(1 row)
+
+ROLLBACK;
+SELECT count_servers();
+ count_servers
+---------------
+ 2
+(1 row)
+
+SELECT get_pool_setting('pool_name');
+ get_pool_setting
+------------------
+ pool1
+(1 row)
+
+BEGIN;
+SET LOCAL test_guc.pool = 'outer:host1,host2;max_connections=10;timeout=30';
+SAVEPOINT sp1;
+SET LOCAL test_guc.pool =
'inner:host3,host4,host5;max_connections=20;timeout=40';
+SELECT count_servers();
+ count_servers
+---------------
+ 3
+(1 row)
+
+SELECT get_pool_setting('pool_name');
+ get_pool_setting
+------------------
+ inner
+(1 row)
+
+ROLLBACK TO sp1;
+SELECT count_servers();
+ count_servers
+---------------
+ 2
+(1 row)
+
+SELECT get_pool_setting('pool_name');
+ get_pool_setting
+------------------
+ outer
+(1 row)
+
+ROLLBACK;
+BEGIN;
+SET LOCAL test_guc.pool = 'first:s1;max_connections=10;timeout=20';
+SET LOCAL test_guc.pool = 'second:s1,s2;max_connections=20;timeout=30';
+SELECT count_servers();
+ count_servers
+---------------
+ 2
+(1 row)
+
+SELECT get_pool_setting('pool_name');
+ get_pool_setting
+------------------
+ second
+(1 row)
+
+ROLLBACK;
+DROP EXTENSION test_guc;
diff --git a/src/test/modules/test_guc/meson.build
b/src/test/modules/test_guc/meson.build
new file mode 100644
index 0000000000..76035d79b1
--- /dev/null
+++ b/src/test/modules/test_guc/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+test_guc_sources = files(
+ 'test_guc.c',
+)
+
+if host_system == 'windows'
+ test_guc_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+ '--NAME', 'test_guc',
+ '--FILEDESC', 'test_guc - test module for GUC_EXTRA_IS_CONTEXT',])
+endif
+
+test_guc = shared_module('test_guc',
+ test_guc_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += test_guc
+
+test_install_data += files(
+ 'test_guc.control',
+ 'test_guc--1.0.sql',
+)
+
+tests += {
+ 'name': 'test_guc',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'regress': {
+ 'sql': [
+ 'test_guc',
+ ],
+ },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_guc/sql/test_guc.sql
b/src/test/modules/test_guc/sql/test_guc.sql
new file mode 100644
index 0000000000..4bbe2742d3
--- /dev/null
+++ b/src/test/modules/test_guc/sql/test_guc.sql
@@ -0,0 +1,68 @@
+CREATE EXTENSION test_guc;
+
+SET test_guc.counter = '42';
+SELECT get_counter_value();
+SELECT get_counter_description();
+
+SET test_guc.counter = '';
+SELECT get_counter_value();
+
+SET test_guc.counter = '100';
+BEGIN;
+SET LOCAL test_guc.counter = '200';
+SELECT get_counter_value();
+ROLLBACK;
+SELECT get_counter_value();
+
+BEGIN;
+SET LOCAL test_guc.counter = '10';
+SAVEPOINT sp1;
+SET LOCAL test_guc.counter = '20';
+SELECT get_counter_value();
+ROLLBACK TO sp1;
+SELECT get_counter_value();
+ROLLBACK;
+
+SET test_guc.pool =
'prod:db1.example.com,db2.example.com,db3.example.com;max_connections=100;timeout=60';
+SELECT count_servers();
+SELECT get_pool_setting('pool_name');
+SELECT get_pool_setting('max_connections');
+SELECT get_pool_setting('timeout');
+
+SELECT show_server_pool();
+
+SET test_guc.pool = 'dev:localhost,192.168.1.10;max_connections=5;timeout=30';
+SELECT count_servers();
+SELECT get_pool_setting('pool_name');
+
+SET test_guc.pool = '';
+SELECT count_servers();
+
+SET test_guc.pool = 'pool1:s1,s2;max_connections=10;timeout=20';
+BEGIN;
+SET LOCAL test_guc.pool = 'pool2:s3,s4,s5;max_connections=50;timeout=90';
+SELECT count_servers();
+SELECT get_pool_setting('pool_name');
+ROLLBACK;
+SELECT count_servers();
+SELECT get_pool_setting('pool_name');
+
+BEGIN;
+SET LOCAL test_guc.pool = 'outer:host1,host2;max_connections=10;timeout=30';
+SAVEPOINT sp1;
+SET LOCAL test_guc.pool =
'inner:host3,host4,host5;max_connections=20;timeout=40';
+SELECT count_servers();
+SELECT get_pool_setting('pool_name');
+ROLLBACK TO sp1;
+SELECT count_servers();
+SELECT get_pool_setting('pool_name');
+ROLLBACK;
+
+BEGIN;
+SET LOCAL test_guc.pool = 'first:s1;max_connections=10;timeout=20';
+SET LOCAL test_guc.pool = 'second:s1,s2;max_connections=20;timeout=30';
+SELECT count_servers();
+SELECT get_pool_setting('pool_name');
+ROLLBACK;
+
+DROP EXTENSION test_guc;
\ No newline at end of file
diff --git a/src/test/modules/test_guc/test_guc--1.0.sql
b/src/test/modules/test_guc/test_guc--1.0.sql
new file mode 100644
index 0000000000..06c2cc22c2
--- /dev/null
+++ b/src/test/modules/test_guc/test_guc--1.0.sql
@@ -0,0 +1,24 @@
+CREATE FUNCTION get_counter_value()
+RETURNS int
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION get_counter_description()
+RETURNS text
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION show_server_pool()
+RETURNS text
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION count_servers()
+RETURNS int
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION get_pool_setting(text)
+RETURNS text
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
\ No newline at end of file
diff --git a/src/test/modules/test_guc/test_guc.c
b/src/test/modules/test_guc/test_guc.c
new file mode 100644
index 0000000000..5a4a90641c
--- /dev/null
+++ b/src/test/modules/test_guc/test_guc.c
@@ -0,0 +1,310 @@
+/*
+ * test_guc.c
+ */
+
+#include "postgres.h"
+#include "funcapi.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+PG_MODULE_MAGIC;
+
+typedef struct SimpleCounterExtra
+{
+ int count;
+ char description[64];
+} SimpleCounterExtra;
+
+static SimpleCounterExtra * counter_extra = NULL;
+static char *counter_string = NULL;
+
+static bool check_counter_guc(char **newval, void **extra, GucSource source);
+static void assign_counter_guc(const char *newval, void *extra);
+
+typedef struct ServerPool
+{
+ char *pool_name;
+ List *servers;
+ int max_connections;
+ int timeout_seconds;
+ char *description;
+} ServerPool;
+
+static ServerPool * pool_extra = NULL;
+static char *pool_string = NULL;
+
+static bool check_pool_guc(char **newval, void **extra, GucSource source);
+static void assign_pool_guc(const char *newval, void *extra);
+
+PG_FUNCTION_INFO_V1(get_counter_value);
+PG_FUNCTION_INFO_V1(get_counter_description);
+PG_FUNCTION_INFO_V1(show_server_pool);
+PG_FUNCTION_INFO_V1(count_servers);
+PG_FUNCTION_INFO_V1(get_pool_setting);
+
+void
+_PG_init(void)
+{
+ DefineCustomStringVariable("test_guc.counter",
+ "Simple GUC without
context (backward compatibility)",
+ "Integer counter
value",
+ &counter_string,
+ NULL,
+ PGC_USERSET,
+ 0, /* No
GUC_EXTRA_IS_CONTEXT flag */
+ check_counter_guc,
+ assign_counter_guc,
+ NULL);
+
+ DefineCustomStringVariable("test_guc.pool",
+ "Server pool
configuration with context",
+ "Format:
name:server1,server2;max_connections=N;timeout=N",
+ &pool_string,
+ NULL,
+ PGC_USERSET,
+
GUC_EXTRA_IS_CONTEXT, /* GUC machinery manages
+
* context */
+ check_pool_guc,
+ assign_pool_guc,
+ NULL);
+}
+
+static bool
+check_counter_guc(char **newval, void **extra, GucSource source)
+{
+ SimpleCounterExtra *data;
+ int count;
+
+ if (*newval == NULL || **newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ count = atoi(*newval);
+
+ data = (SimpleCounterExtra *) guc_malloc(LOG,
sizeof(SimpleCounterExtra));
+ if (data == NULL)
+ return false;
+
+ data->count = count;
+ snprintf(data->description, sizeof(data->description), "Count is %d",
count);
+
+ *extra = data;
+
+ elog(DEBUG1, "counter GUC: parsed count=%d, data=%p", count, data);
+
+ return true;
+}
+
+static void
+assign_counter_guc(const char *newval, void *extra)
+{
+ if (extra == NULL)
+ {
+ counter_extra = NULL;
+ elog(DEBUG1, "Counter GUC: cleared");
+ return;
+ }
+
+ counter_extra = (SimpleCounterExtra *) extra;
+
+ elog(DEBUG1, "counter GUC: active with count=%d, data=%p",
+ counter_extra->count, counter_extra);
+}
+
+Datum
+get_counter_value(PG_FUNCTION_ARGS)
+{
+ if (counter_extra == NULL)
+ PG_RETURN_INT32(0);
+
+ PG_RETURN_INT32(counter_extra->count);
+}
+
+Datum
+get_counter_description(PG_FUNCTION_ARGS)
+{
+ if (counter_extra == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("No counter configured"));
+
+ PG_RETURN_TEXT_P(cstring_to_text(counter_extra->description));
+}
+
+/*
+ * Parse server pool configuration
+ * Format: "pool_name:server1,server2,server3;max_connections=10;timeout=30"
+ */
+static bool
+check_pool_guc(char **newval, void **extra, GucSource source)
+{
+ ServerPool *pool;
+ char *work_str;
+ char *pool_name;
+ char *servers_part;
+ char *settings_part;
+ char *server_token;
+ int max_conn = 10;
+ int timeout = 30;
+ int server_count = 0;
+
+ if (*newval == NULL || **newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ work_str = pstrdup(*newval);
+
+ pool_name = work_str;
+ servers_part = strchr(work_str, ':');
+ if (servers_part == NULL)
+ {
+ pfree(work_str);
+ GUC_check_errdetail("Format should be
'name:server1,server2;setting=val'");
+ return false;
+ }
+ *servers_part++ = '\0';
+
+ settings_part = strchr(servers_part, ';');
+ if (settings_part != NULL)
+ *settings_part++ = '\0';
+
+ if (settings_part != NULL)
+ {
+ char *setting = strtok(settings_part, ";");
+
+ while (setting != NULL)
+ {
+ char *eq = strchr(setting, '=');
+
+ if (eq != NULL)
+ {
+ *eq++ = '\0';
+ if (strcmp(setting, "max_connections") == 0)
+ max_conn = atoi(eq);
+ else if (strcmp(setting, "timeout") == 0)
+ timeout = atoi(eq);
+ }
+ setting = strtok(NULL, ";");
+ }
+ }
+
+ pool = (ServerPool *) palloc(sizeof(ServerPool));
+ pool->pool_name = pstrdup(pool_name);
+ pool->servers = NIL;
+ pool->max_connections = max_conn;
+ pool->timeout_seconds = timeout;
+
+ /* Parse server list */
+ server_token = strtok(servers_part, ",");
+ while (server_token != NULL)
+ {
+ while (*server_token == ' ' || *server_token == '\t')
+ server_token++;
+ char *end = server_token + strlen(server_token) - 1;
+
+ while (end > server_token && (*end == ' ' || *end == '\t'))
+ *end-- = '\0';
+
+ if (*server_token != '\0')
+ {
+ pool->servers = lappend(pool->servers,
pstrdup(server_token));
+ server_count++;
+ }
+
+ server_token = strtok(NULL, ",");
+ }
+
+ pool->description = psprintf("Pool '%s': %d servers, max_conn=%d,
timeout=%d",
+
pool->pool_name,
+ server_count,
+
pool->max_connections,
+
pool->timeout_seconds);
+
+ *extra = pool;
+
+ pfree(work_str);
+
+ elog(DEBUG1, "pool GUC: parsed pool '%s' with %d servers, data=%p",
+ pool->pool_name, server_count, pool);
+
+ return true;
+}
+
+static void
+assign_pool_guc(const char *newval, void *extra)
+{
+ if (extra == NULL)
+ {
+ pool_extra = NULL;
+ elog(DEBUG1, "Server pool GUC: cleared");
+ return;
+ }
+
+ pool_extra = (ServerPool *) extra;
+
+ elog(DEBUG1, "pool GUC: active pool '%s' with %d servers, data=%p",
+ pool_extra->pool_name,
+ list_length(pool_extra->servers),
+ pool_extra);
+}
+
+Datum
+show_server_pool(PG_FUNCTION_ARGS)
+{
+ StringInfoData buf;
+ ListCell *lc;
+
+ if (pool_extra == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("No server pool configured"));
+
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "Pool: %s\n", pool_extra->pool_name);
+ appendStringInfo(&buf, "Max connections: %d\n",
pool_extra->max_connections);
+ appendStringInfo(&buf, "Timeout: %d seconds\n",
pool_extra->timeout_seconds);
+ appendStringInfo(&buf, "Servers (%d total):\n",
list_length(pool_extra->servers));
+
+ foreach(lc, pool_extra->servers)
+ {
+ char *server = (char *) lfirst(lc);
+
+ appendStringInfo(&buf, " - %s\n", server);
+ }
+
+ PG_RETURN_TEXT_P(cstring_to_text(buf.data));
+}
+
+Datum
+count_servers(PG_FUNCTION_ARGS)
+{
+ if (pool_extra == NULL)
+ PG_RETURN_INT32(0);
+
+ PG_RETURN_INT32(list_length(pool_extra->servers));
+}
+
+Datum
+get_pool_setting(PG_FUNCTION_ARGS)
+{
+ text *setting_name = PG_GETARG_TEXT_PP(0);
+ char *name = text_to_cstring(setting_name);
+ char result[256];
+
+ if (pool_extra == NULL)
+ PG_RETURN_TEXT_P(cstring_to_text("No pool configured"));
+
+ if (strcmp(name, "pool_name") == 0)
+ snprintf(result, sizeof(result), "%s", pool_extra->pool_name);
+ else if (strcmp(name, "max_connections") == 0)
+ snprintf(result, sizeof(result), "%d",
pool_extra->max_connections);
+ else if (strcmp(name, "timeout") == 0)
+ snprintf(result, sizeof(result), "%d",
pool_extra->timeout_seconds);
+ else if (strcmp(name, "description") == 0)
+ snprintf(result, sizeof(result), "%s", pool_extra->description);
+ else
+ snprintf(result, sizeof(result), "Unknown setting: %s", name);
+
+ PG_RETURN_TEXT_P(cstring_to_text(result));
+}
diff --git a/src/test/modules/test_guc/test_guc.control
b/src/test/modules/test_guc/test_guc.control
new file mode 100644
index 0000000000..c648e51ef3
--- /dev/null
+++ b/src/test/modules/test_guc/test_guc.control
@@ -0,0 +1,5 @@
+# test_guc extension
+comment = 'Test module for GUC_EXTRA_IS_CONTEXT feature'
+default_version = '1.0'
+module_pathname = '$libdir/test_guc'
+relocatable = true
\ No newline at end of file
--
2.52.0.windows.1