Hi,

Currently extensible.c is not covered by any tests. The proposed patch
fixes this. Also it can serve as an example of using CustomScan and
ExtensibleNode.

For the reviewers
-----------------------

Here is how to check the code coverage:

```
git clean -df
rm -r build
meson setup --buildtype debug -Db_coverage=true -Dcassert=true
-Dinjection_points=true -Dtap_tests=enabled -Dldap=disabled
-Dicu=disabled -DPG_TEST_EXTRA="kerberos ldap libpq_encryption
load_balance oauth regress_dump_restore ssl wal_consistency_checking
xid_wraparound" -Dprefix=/home/eax/pginstall build
ninja -C build
meson test -C build
ninja -C build coverage-html
open build/meson-logs/coveragereport/index.html
```

You are going to need `lcov` 1.16 in your $PATH because there are
certain problems with newer versions [1].

[1]: 
https://postgr.es/m/CAJ7c6TN%2BMCh99EZ8YGhXZAdnqvNQYir6E34B_mmcB5KsxCB00A%40mail.gmail.com

-- 
Best regards,
Aleksander Alekseev
From c632e0065a5c6d14ab7ff11c92316b19e0d8757b Mon Sep 17 00:00:00 2001
From: Aleksander Alekseev <[email protected]>
Date: Tue, 31 Mar 2026 15:38:21 +0300
Subject: [PATCH v1] Add test module for src/backend/nodes/extensible.c

The new test implements a CustomScan that uses an ExtensibleNode to carry
private planning data from the planner into the executor. A SQL query against
a test table exercises the full pipeline end-to-end, while thin wrapper
functions cover GetExtensibleNodeMethods() and GetCustomScanMethods().

Author: Aleksander Alekseev <[email protected]>
Reviewed-by: TODO FIXME
Discussion: TODO FIXME
---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_extensible/Makefile     |  27 ++
 .../expected/test_extensible.out              |  69 ++++
 src/test/modules/test_extensible/meson.build  |  35 ++
 .../test_extensible/sql/test_extensible.sql   |  40 ++
 .../test_extensible/test_extensible--1.0.sql  |   8 +
 .../modules/test_extensible/test_extensible.c | 389 ++++++++++++++++++
 .../test_extensible/test_extensible.conf      |   1 +
 .../test_extensible/test_extensible.control   |   4 +
 10 files changed, 575 insertions(+)
 create mode 100644 src/test/modules/test_extensible/Makefile
 create mode 100644 src/test/modules/test_extensible/expected/test_extensible.out
 create mode 100644 src/test/modules/test_extensible/meson.build
 create mode 100644 src/test/modules/test_extensible/sql/test_extensible.sql
 create mode 100644 src/test/modules/test_extensible/test_extensible--1.0.sql
 create mode 100644 src/test/modules/test_extensible/test_extensible.c
 create mode 100644 src/test/modules/test_extensible/test_extensible.conf
 create mode 100644 src/test/modules/test_extensible/test_extensible.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 28ce3b35eda..4a686b11752 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -28,6 +28,7 @@ SUBDIRS = \
 		  test_dsa \
 		  test_dsm_registry \
 		  test_escape \
+		  test_extensible \
 		  test_extensions \
 		  test_ginpostinglist \
 		  test_int128 \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 3ac291656c1..790b0158d6a 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -29,6 +29,7 @@ subdir('test_ddl_deparse')
 subdir('test_dsa')
 subdir('test_dsm_registry')
 subdir('test_escape')
+subdir('test_extensible')
 subdir('test_extensions')
 subdir('test_ginpostinglist')
 subdir('test_int128')
diff --git a/src/test/modules/test_extensible/Makefile b/src/test/modules/test_extensible/Makefile
new file mode 100644
index 00000000000..1369ae8e52b
--- /dev/null
+++ b/src/test/modules/test_extensible/Makefile
@@ -0,0 +1,27 @@
+# src/test/modules/test_extensible/Makefile
+
+MODULE_big = test_extensible
+OBJS = \
+	$(WIN32RES) \
+	test_extensible.o
+PGFILEDESC = "test_extensible - test module for extensible node and custom scan registration"
+
+EXTENSION = test_extensible
+DATA = test_extensible--1.0.sql
+
+REGRESS_OPTS = --temp-config $(top_srcdir)/src/test/modules/test_extensible/test_extensible.conf
+REGRESS = test_extensible
+# Disabled because these tests require "shared_preload_libraries=test_extensible",
+# which typical installcheck users do not have (e.g. buildfarm clients).
+NO_INSTALLCHECK = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_extensible
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_extensible/expected/test_extensible.out b/src/test/modules/test_extensible/expected/test_extensible.out
new file mode 100644
index 00000000000..f11769f4597
--- /dev/null
+++ b/src/test/modules/test_extensible/expected/test_extensible.out
@@ -0,0 +1,69 @@
+-- Tests for extensible node and custom scan registration
+-- (src/backend/nodes/extensible.c)
+CREATE EXTENSION test_extensible;
+-- ----------------------------------------------------------------
+-- GetExtensibleNodeMethods() and GetCustomScanMethods() lookup tests
+-- ----------------------------------------------------------------
+-- GetExtensibleNodeMethods: known name returns the registered extnodename.
+SELECT test_get_extensible_node_methods('TestExtNode', false);
+ test_get_extensible_node_methods 
+----------------------------------
+ TestExtNode
+(1 row)
+
+-- GetExtensibleNodeMethods: unknown name with missing_ok=true returns NULL.
+SELECT test_get_extensible_node_methods('NoSuchExtNode', true);
+ test_get_extensible_node_methods 
+----------------------------------
+ 
+(1 row)
+
+-- GetExtensibleNodeMethods: unknown name with missing_ok=false raises ERROR.
+SELECT test_get_extensible_node_methods('NoSuchExtNode', false);
+ERROR:  ExtensibleNodeMethods "NoSuchExtNode" was not registered
+-- GetCustomScanMethods: known name returns the registered CustomName.
+SELECT test_get_custom_scan_methods('TestCustomScan', false);
+ test_get_custom_scan_methods 
+------------------------------
+ TestCustomScan
+(1 row)
+
+-- GetCustomScanMethods: unknown name with missing_ok=true returns NULL.
+SELECT test_get_custom_scan_methods('NoSuchCustomScan', true);
+ test_get_custom_scan_methods 
+------------------------------
+ 
+(1 row)
+
+-- GetCustomScanMethods: unknown name with missing_ok=false raises ERROR.
+SELECT test_get_custom_scan_methods('NoSuchCustomScan', false);
+ERROR:  ExtensibleNodeMethods "NoSuchCustomScan" was not registered
+-- ----------------------------------------------------------------
+-- End-to-end CustomScan test
+-- ----------------------------------------------------------------
+CREATE TABLE test_extensible_tbl (id integer, val text);
+INSERT INTO test_extensible_tbl VALUES (1, 'one'), (2, 'two'), (3, 'three');
+-- Verify the planner chose our CustomScan (not a SeqScan).
+EXPLAIN (COSTS OFF) SELECT id, val FROM test_extensible_tbl ORDER BY id;
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ Sort
+   Sort Key: id
+   ->  Custom Scan (TestCustomScan) on test_extensible_tbl
+(3 rows)
+
+-- Execute through the CustomScan; each of the 3 inserted rows appears
+-- twice (6 rows total), proving the custom scan logic is in effect.
+SELECT id, val FROM test_extensible_tbl ORDER BY id, val;
+ id |  val  
+----+-------
+  1 | one
+  1 | one
+  2 | two
+  2 | two
+  3 | three
+  3 | three
+(6 rows)
+
+DROP TABLE test_extensible_tbl;
+DROP EXTENSION test_extensible;
diff --git a/src/test/modules/test_extensible/meson.build b/src/test/modules/test_extensible/meson.build
new file mode 100644
index 00000000000..bcd5312d1a9
--- /dev/null
+++ b/src/test/modules/test_extensible/meson.build
@@ -0,0 +1,35 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_extensible_sources = files(
+  'test_extensible.c',
+)
+
+if host_system == 'windows'
+  test_extensible_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_extensible',
+    '--FILEDESC', 'test_extensible - test module for extensible node and custom scan registration',])
+endif
+
+test_extensible = shared_module('test_extensible',
+  test_extensible_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_extensible
+
+test_install_data += files(
+  'test_extensible.control',
+  'test_extensible--1.0.sql',
+)
+
+tests += {
+  'name': 'test_extensible',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'test_extensible',
+    ],
+    'regress_args': ['--temp-config', files('test_extensible.conf')],
+    'runningcheck': false,
+  },
+}
diff --git a/src/test/modules/test_extensible/sql/test_extensible.sql b/src/test/modules/test_extensible/sql/test_extensible.sql
new file mode 100644
index 00000000000..30c9da3f658
--- /dev/null
+++ b/src/test/modules/test_extensible/sql/test_extensible.sql
@@ -0,0 +1,40 @@
+-- Tests for extensible node and custom scan registration
+-- (src/backend/nodes/extensible.c)
+
+CREATE EXTENSION test_extensible;
+
+-- ----------------------------------------------------------------
+-- GetExtensibleNodeMethods() and GetCustomScanMethods() lookup tests
+-- ----------------------------------------------------------------
+
+-- GetExtensibleNodeMethods: known name returns the registered extnodename.
+SELECT test_get_extensible_node_methods('TestExtNode', false);
+-- GetExtensibleNodeMethods: unknown name with missing_ok=true returns NULL.
+SELECT test_get_extensible_node_methods('NoSuchExtNode', true);
+-- GetExtensibleNodeMethods: unknown name with missing_ok=false raises ERROR.
+SELECT test_get_extensible_node_methods('NoSuchExtNode', false);
+
+-- GetCustomScanMethods: known name returns the registered CustomName.
+SELECT test_get_custom_scan_methods('TestCustomScan', false);
+-- GetCustomScanMethods: unknown name with missing_ok=true returns NULL.
+SELECT test_get_custom_scan_methods('NoSuchCustomScan', true);
+-- GetCustomScanMethods: unknown name with missing_ok=false raises ERROR.
+SELECT test_get_custom_scan_methods('NoSuchCustomScan', false);
+
+-- ----------------------------------------------------------------
+-- End-to-end CustomScan test
+-- ----------------------------------------------------------------
+
+CREATE TABLE test_extensible_tbl (id integer, val text);
+INSERT INTO test_extensible_tbl VALUES (1, 'one'), (2, 'two'), (3, 'three');
+
+-- Verify the planner chose our CustomScan (not a SeqScan).
+EXPLAIN (COSTS OFF) SELECT id, val FROM test_extensible_tbl ORDER BY id;
+
+-- Execute through the CustomScan; each of the 3 inserted rows appears
+-- twice (6 rows total), proving the custom scan logic is in effect.
+SELECT id, val FROM test_extensible_tbl ORDER BY id, val;
+
+DROP TABLE test_extensible_tbl;
+
+DROP EXTENSION test_extensible;
diff --git a/src/test/modules/test_extensible/test_extensible--1.0.sql b/src/test/modules/test_extensible/test_extensible--1.0.sql
new file mode 100644
index 00000000000..ed7a65f496d
--- /dev/null
+++ b/src/test/modules/test_extensible/test_extensible--1.0.sql
@@ -0,0 +1,8 @@
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_extensible" to load this file. \quit
+
+CREATE OR REPLACE FUNCTION test_get_extensible_node_methods(text, bool) RETURNS text
+  AS 'MODULE_PATHNAME', 'test_get_extensible_node_methods' LANGUAGE C;
+
+CREATE OR REPLACE FUNCTION test_get_custom_scan_methods(text, bool) RETURNS text
+  AS 'MODULE_PATHNAME', 'test_get_custom_scan_methods' LANGUAGE C;
\ No newline at end of file
diff --git a/src/test/modules/test_extensible/test_extensible.c b/src/test/modules/test_extensible/test_extensible.c
new file mode 100644
index 00000000000..2cca68dbde8
--- /dev/null
+++ b/src/test/modules/test_extensible/test_extensible.c
@@ -0,0 +1,389 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_extensible.c
+ *		Test correctness of extensible node and custom scan registration
+ *		functions defined in src/backend/nodes/extensible.c.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_extensible/test_extensible.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/tableam.h"
+#include "executor/executor.h"
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "nodes/extensible.h"
+#include "nodes/nodes.h"
+#include "nodes/plannodes.h"
+#include "nodes/readfuncs.h"
+#include "optimizer/optimizer.h"
+#include "optimizer/pathnode.h"
+#include "optimizer/paths.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+
+PG_MODULE_MAGIC;
+
+/* Name of the test table that triggers our CustomScan injection */
+#define TEST_TABLE_NAME		"test_extensible_tbl"
+
+/* ----------------------------------------------------------------
+ * TestExtNode – an ExtensibleNode subtype carrying a single int.
+ *
+ * In a real extension this would hold planning metadata passed from
+ * the path stage to the execution stage via CustomScan.custom_private.
+ * ----------------------------------------------------------------
+ */
+typedef struct TestExtNode
+{
+	ExtensibleNode base;		/* must be first */
+	int			repeat_count;	/* how many times to return each scanned row */
+}			TestExtNode;
+
+#define TEST_EXT_NODE_NAME	"TestExtNode"
+#define TEST_CUSTOM_SCAN_NAME	"TestCustomScan"
+
+/* ----------------------------------------------------------------
+ * ExtensibleNodeMethods callbacks
+ * ----------------------------------------------------------------
+ */
+
+static void
+test_ext_node_copy(ExtensibleNode *newnode, const ExtensibleNode *oldnode)
+{
+	((TestExtNode *) newnode)->repeat_count =
+		((const TestExtNode *) oldnode)->repeat_count;
+}
+
+static bool
+test_ext_node_equal(const ExtensibleNode *a, const ExtensibleNode *b)
+{
+	return ((const TestExtNode *) a)->repeat_count ==
+		((const TestExtNode *) b)->repeat_count;
+}
+
+static void
+test_ext_node_out(StringInfo str, const ExtensibleNode *node)
+{
+	appendStringInfo(str, " :repeat_count %d",
+					 ((const TestExtNode *) node)->repeat_count);
+}
+
+static void
+test_ext_node_read(ExtensibleNode *node)
+{
+	TestExtNode *tnode = (TestExtNode *) node;
+	const char *token;
+	int			length;
+
+	token = pg_strtok(&length); /* skip :repeat_count */
+	token = pg_strtok(&length); /* get value */
+	tnode->repeat_count = atoi(token);
+}
+
+static const ExtensibleNodeMethods test_ext_node_methods =
+{
+	.extnodename = TEST_EXT_NODE_NAME,
+	.node_size = sizeof(TestExtNode),
+	.nodeCopy = test_ext_node_copy,
+	.nodeEqual = test_ext_node_equal,
+	.nodeOut = test_ext_node_out,
+	.nodeRead = test_ext_node_read,
+};
+
+/* ----------------------------------------------------------------
+ * TestCustomScanState – execution state for the custom scan.
+ * ----------------------------------------------------------------
+ */
+typedef struct TestCustomScanState
+{
+	CustomScanState css;		/* must be first */
+	TableScanDesc scandesc;
+	int			repeat_count;	/* repeat_count from TestExtNode */
+	int			repeats_left;	/* how many more times to return current row */
+}			TestCustomScanState;
+
+/* ----------------------------------------------------------------
+ * Executor callbacks
+ * ----------------------------------------------------------------
+ */
+static void
+test_begin_custom_scan(CustomScanState *node, EState *estate, int eflags)
+{
+	TestCustomScanState *tstate = (TestCustomScanState *) node;
+	CustomScan *cscan = (CustomScan *) node->ss.ps.plan;
+	TestExtNode *tnode;
+	Relation	rel = node->ss.ss_currentRelation;
+
+	/*
+	 * Read repeat_count from the TestExtNode stored in custom_private. This
+	 * is the key moment where the ExtensibleNode private data crosses from
+	 * the plan tree into the executor state.
+	 */
+	Assert(cscan->custom_private != NIL);
+	tnode = (TestExtNode *) linitial(cscan->custom_private);
+	Assert(IsA(tnode, ExtensibleNode));
+	Assert(strcmp(tnode->base.extnodename, TEST_EXT_NODE_NAME) == 0);
+
+	Assert(tnode->repeat_count > 0);
+	tstate->repeat_count = tnode->repeat_count;
+	tstate->repeats_left = 0;
+
+	/* Start a plain sequential heap scan. */
+	tstate->scandesc = table_beginscan(rel, estate->es_snapshot, 0, NULL,
+									   SO_NONE);
+}
+
+static TupleTableSlot *
+test_exec_custom_scan(CustomScanState *node)
+{
+	TestCustomScanState *tstate = (TestCustomScanState *) node;
+	TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+
+	/*
+	 * If the current tuple still has repeats remaining, return it again
+	 * without advancing the heap scan.  The repeat count comes from the
+	 * TestExtNode that was read in BeginCustomScan.
+	 */
+	if (tstate->repeats_left > 0)
+	{
+		tstate->repeats_left--;
+		return slot;
+	}
+
+	/* Fetch the next tuple from the heap. */
+	if (!table_scan_getnextslot(tstate->scandesc, ForwardScanDirection, slot))
+		return NULL;
+
+	/* Schedule (repeat_count - 1) additional returns of this tuple. */
+	tstate->repeats_left = tstate->repeat_count - 1;
+	return slot;
+}
+
+static void
+test_end_custom_scan(CustomScanState *node)
+{
+	TestCustomScanState *tstate = (TestCustomScanState *) node;
+
+	if (tstate->scandesc)
+		table_endscan(tstate->scandesc);
+}
+
+static void
+test_rescan_custom_scan(CustomScanState *node)
+{
+	TestCustomScanState *tstate = (TestCustomScanState *) node;
+
+	tstate->repeats_left = 0;
+	table_rescan(tstate->scandesc, NULL);
+}
+
+static const CustomExecMethods test_custom_exec_methods =
+{
+	.CustomName = TEST_CUSTOM_SCAN_NAME,
+	.BeginCustomScan = test_begin_custom_scan,
+	.ExecCustomScan = test_exec_custom_scan,
+	.EndCustomScan = test_end_custom_scan,
+	.ReScanCustomScan = test_rescan_custom_scan,
+};
+
+static Node *
+test_create_custom_scan_state(CustomScan *cscan)
+{
+	TestCustomScanState *tstate;
+
+	tstate = (TestCustomScanState *)
+		newNode(sizeof(TestCustomScanState), T_CustomScanState);
+	tstate->css.methods = &test_custom_exec_methods;
+
+	/*
+	 * Use the heap-tuple slot type so table_scan_getnextslot() can fill it
+	 * directly.
+	 */
+	tstate->css.slotOps = &TTSOpsBufferHeapTuple;
+
+	return (Node *) tstate;
+}
+
+static const CustomScanMethods test_custom_scan_methods =
+{
+	.CustomName = TEST_CUSTOM_SCAN_NAME,
+	.CreateCustomScanState = test_create_custom_scan_state,
+};
+
+/* ----------------------------------------------------------------
+ * Planner callbacks
+ * ----------------------------------------------------------------
+ */
+static Plan *
+test_plan_custom_path(PlannerInfo *root,
+					  RelOptInfo *rel,
+					  struct CustomPath *best_path,
+					  List *tlist,
+					  List *clauses,
+					  List *custom_plans)
+{
+	CustomScan *cscan = makeNode(CustomScan);
+
+	/*
+	 * Pass the ExtensibleNode from the path to the plan via custom_private.
+	 * This is the recommended pattern for conveying private planning data
+	 * from a CustomPath to its corresponding CustomScan.
+	 */
+	cscan->scan.plan.targetlist = tlist;
+	cscan->scan.plan.qual = NIL;
+	cscan->scan.scanrelid = rel->relid;
+	cscan->flags = best_path->flags;
+	cscan->custom_plans = NIL;
+	cscan->custom_exprs = NIL;
+	cscan->custom_private = best_path->custom_private;
+	cscan->custom_scan_tlist = NIL;
+	cscan->custom_relids = NULL;
+	cscan->methods = &test_custom_scan_methods;
+
+	return (Plan *) cscan;
+}
+
+static const CustomPathMethods test_custom_path_methods =
+{
+	.CustomName = TEST_CUSTOM_SCAN_NAME,
+	.PlanCustomPath = test_plan_custom_path,
+};
+
+/* ----------------------------------------------------------------
+ * set_rel_pathlist_hook – inject our CustomPath for the specific
+ * test table.
+ * ----------------------------------------------------------------
+ */
+static set_rel_pathlist_hook_type prev_set_rel_pathlist_hook = NULL;
+
+static void
+test_set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
+					  Index rti, RangeTblEntry *rte)
+{
+	CustomPath *cpath;
+	TestExtNode *tnode;
+	char	   *relname;
+
+	/* Let previous hooks run first. */
+	if (prev_set_rel_pathlist_hook)
+		prev_set_rel_pathlist_hook(root, rel, rti, rte);
+
+	/* Only handle plain base relations (ordinary tables). */
+	if (rel->reloptkind != RELOPT_BASEREL || rte->rtekind != RTE_RELATION)
+		return;
+
+	/*
+	 * Only inject our CustomPath for the specific marker table.  This
+	 * prevents interference with system-catalog scans.
+	 */
+	relname = get_rel_name(rte->relid);
+	if (relname == NULL || strcmp(relname, TEST_TABLE_NAME) != 0)
+		return;
+
+	/*
+	 * Build a TestExtNode carrying the planner's row estimate.  In a real
+	 * extension this might be a device handle or a cache key.
+	 */
+	tnode = (TestExtNode *) newNode(sizeof(TestExtNode), T_ExtensibleNode);
+	tnode->base.extnodename = TEST_EXT_NODE_NAME;
+	tnode->repeat_count = 2;	/* each row will be returned twice */
+
+	/*
+	 * Build the CustomPath.  We match the cost of the cheapest existing path
+	 * so the planner is free to choose ours when it is equally cheap.
+	 */
+	cpath = makeNode(CustomPath);
+	cpath->path.pathtype = T_CustomScan;
+	cpath->path.parent = rel;
+	cpath->path.pathtarget = rel->reltarget;
+	cpath->path.rows = rel->rows;
+	cpath->path.startup_cost = 0;
+	cpath->path.total_cost = 0;
+	cpath->flags = 0;
+	cpath->custom_paths = NIL;
+	cpath->custom_private = list_make1(tnode);
+	cpath->methods = &test_custom_path_methods;
+
+	add_path(rel, (Path *) cpath);
+}
+
+/* ----------------------------------------------------------------
+ * SQL-callable test functions for Get* lookups
+ * ----------------------------------------------------------------
+ */
+PG_FUNCTION_INFO_V1(test_get_extensible_node_methods);
+PG_FUNCTION_INFO_V1(test_get_custom_scan_methods);
+
+/*
+ * test_get_extensible_node_methods(name text, missing_ok bool)
+ *
+ * Thin wrapper around GetExtensibleNodeMethods().  Returns the registered
+ * extnodename, or NULL when missing_ok = true and the name is not found.
+ * Raises ERROR when missing_ok = false and the name is not found.
+ */
+Datum
+test_get_extensible_node_methods(PG_FUNCTION_ARGS)
+{
+	char	   *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+	bool		missing_ok = PG_GETARG_BOOL(1);
+	const ExtensibleNodeMethods *methods;
+
+	methods = GetExtensibleNodeMethods(name, missing_ok);
+	if (methods == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(cstring_to_text(methods->extnodename));
+}
+
+/*
+ * test_get_custom_scan_methods(name text, missing_ok bool)
+ *
+ * Thin wrapper around GetCustomScanMethods().  Returns the registered
+ * CustomName, or NULL when missing_ok = true and the name is not found.
+ * Raises ERROR when missing_ok = false and the name is not found.
+ */
+Datum
+test_get_custom_scan_methods(PG_FUNCTION_ARGS)
+{
+	char	   *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+	bool		missing_ok = PG_GETARG_BOOL(1);
+	const CustomScanMethods *methods;
+
+	methods = GetCustomScanMethods(name, missing_ok);
+	if (methods == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(cstring_to_text(methods->CustomName));
+}
+
+/* ----------------------------------------------------------------
+ * Module initialisation
+ * ----------------------------------------------------------------
+ */
+void
+_PG_init(void)
+{
+	if (!process_shared_preload_libraries_in_progress)
+		ereport(ERROR,
+				(errmsg("cannot load \"%s\" after startup",
+						"test_extensible"),
+				 errdetail("\"%s\" must be loaded with "
+						   "\"shared_preload_libraries\".",
+						   "test_extensible")));
+
+	/* Register the custom scan methods. */
+	RegisterCustomScanMethods(&test_custom_scan_methods);
+
+	/* Register the extensible node type */
+	RegisterExtensibleNodeMethods(&test_ext_node_methods);
+
+	/* Install the path-list hook to inject CustomPaths for the test table. */
+	prev_set_rel_pathlist_hook = set_rel_pathlist_hook;
+	set_rel_pathlist_hook = test_set_rel_pathlist;
+}
diff --git a/src/test/modules/test_extensible/test_extensible.conf b/src/test/modules/test_extensible/test_extensible.conf
new file mode 100644
index 00000000000..a5b643cb256
--- /dev/null
+++ b/src/test/modules/test_extensible/test_extensible.conf
@@ -0,0 +1 @@
+shared_preload_libraries = 'test_extensible'
diff --git a/src/test/modules/test_extensible/test_extensible.control b/src/test/modules/test_extensible/test_extensible.control
new file mode 100644
index 00000000000..7a19fa6570e
--- /dev/null
+++ b/src/test/modules/test_extensible/test_extensible.control
@@ -0,0 +1,4 @@
+comment = 'Test code for extensible node and custom scan registration'
+default_version = '1.0'
+module_pathname = '$libdir/test_extensible'
+relocatable = false
-- 
2.43.0

Reply via email to