From c632e0065a5c6d14ab7ff11c92316b19e0d8757b Mon Sep 17 00:00:00 2001 From: Aleksander Alekseev 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 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