v2-0001-Allow-complex-data-for-GUC-extra.patch

text/plain

Filename: v2-0001-Allow-complex-data-for-GUC-extra.patch
Type: text/plain
Part: 0
Message: Re: [PATCH] Allow complex data for GUC extra.
From efa009566623e214b9859efeea768efdf9b00e85 Mon Sep 17 00:00:00 2001
From: Bryan Green <dbryan.green@gmail.com>
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 <dbryan.green@gmail.com>
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