Hi Rafia, > I looked into this patch and have a few comments. > Firstly, in test_create_custom_scan_state() why hard coding css.slotOps = > &TTSOpsBufferHeapTuple and not using the table_slot_callbacks() for the > purpose and getting the real AM. > Next, in test_plan_custom_path, the last two arguments are never used. > There are no tests for nodeCopy, nodeEqual, nodeOut, and nodeRead routines.
Many thanks for the feedback! Here is the corrected patch. -- Best regards, Aleksander Alekseev
From 42e432774f535d42eb6d9125b5ac8c3140e9bfdf Mon Sep 17 00:00:00 2001 From: Aleksander Alekseev <[email protected]> Date: Tue, 31 Mar 2026 15:38:21 +0300 Subject: [PATCH v2] 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(). The test_ext_node_callbacks() function executes all four ExtensibleNodeMethods callbacks. Author: Aleksander Alekseev <[email protected]> Reviewed-by: Rafia Sabih <[email protected]> Discussion: https://postgr.es/m/CAJ7c6TNfn9Fv_Je1etA6rrgq1onVvXbjwBTkbkd4kVQhcu11gg%40mail.gmail.com --- src/test/modules/Makefile | 1 + src/test/modules/meson.build | 1 + src/test/modules/test_extensible/Makefile | 27 + .../expected/test_extensible.out | 97 ++++ src/test/modules/test_extensible/meson.build | 35 ++ .../test_extensible/sql/test_extensible.sql | 52 ++ .../test_extensible/test_extensible--1.0.sql | 11 + .../modules/test_extensible/test_extensible.c | 533 ++++++++++++++++++ .../test_extensible/test_extensible.conf | 1 + .../test_extensible/test_extensible.control | 4 + 10 files changed, 762 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 098bb8142ae..a3c45408e78 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -30,6 +30,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 4bca42bb370..d2c256a311c 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -31,6 +31,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..8e3f4baeb82 --- /dev/null +++ b/src/test/modules/test_extensible/expected/test_extensible.out @@ -0,0 +1,97 @@ +-- 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 +-- ---------------------------------------------------------------- +-- ExtensibleNodeMethods callbacks: nodeCopy, nodeEqual, nodeOut, nodeRead +-- ---------------------------------------------------------------- +-- Copy the node, compare it with the original, serialize it and read it back. +-- The returned string is the output of the nodeOut callback. +SELECT test_ext_node_callbacks('1234'::oid, 2); + test_ext_node_callbacks +----------------------------------------------------------------------- + {EXTENSIBLENODE :extnodename TestExtNode :relid 1234 :repeat_count 2} +(1 row) + +-- ---------------------------------------------------------------- +-- 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; + id | val +----+------- + 1 | one + 1 | one + 2 | two + 2 | two + 3 | three + 3 | three +(6 rows) + +-- The restriction clauses are passed to the CustomScan as its qual. +EXPLAIN (COSTS OFF) SELECT val FROM test_extensible_tbl WHERE id > 1; + QUERY PLAN +----------------------------------------------------- + Custom Scan (TestCustomScan) on test_extensible_tbl + Filter: (id > 1) +(2 rows) + +SELECT val FROM test_extensible_tbl WHERE id > 1 ORDER BY id; + val +------- + two + two + three + three +(4 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..c7c2d9b1be8 --- /dev/null +++ b/src/test/modules/test_extensible/sql/test_extensible.sql @@ -0,0 +1,52 @@ +-- 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); + +-- ---------------------------------------------------------------- +-- ExtensibleNodeMethods callbacks: nodeCopy, nodeEqual, nodeOut, nodeRead +-- ---------------------------------------------------------------- + +-- Copy the node, compare it with the original, serialize it and read it back. +-- The returned string is the output of the nodeOut callback. +SELECT test_ext_node_callbacks('1234'::oid, 2); + +-- ---------------------------------------------------------------- +-- 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; + +-- The restriction clauses are passed to the CustomScan as its qual. +EXPLAIN (COSTS OFF) SELECT val FROM test_extensible_tbl WHERE id > 1; +SELECT val FROM test_extensible_tbl WHERE id > 1 ORDER BY id; + +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..935af576976 --- /dev/null +++ b/src/test/modules/test_extensible/test_extensible--1.0.sql @@ -0,0 +1,11 @@ +-- 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; + +CREATE OR REPLACE FUNCTION test_ext_node_callbacks(oid, int) RETURNS text + AS 'MODULE_PATHNAME', 'test_ext_node_callbacks' LANGUAGE C STRICT; \ 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..68c461c354a --- /dev/null +++ b/src/test/modules/test_extensible/test_extensible.c @@ -0,0 +1,533 @@ +/*------------------------------------------------------------------------- + * + * test_extensible.c + * Test correctness of extensible node and custom scan registration + * functions. For more details see "Writing a Custom Scan Provider" in + * the documentation. + * + * Copyright (c) 2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_extensible/test_extensible.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/table.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 "optimizer/restrictinfo.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 our planning data. + */ +typedef struct TestExtNode +{ + ExtensibleNode base; /* must be first */ + Oid relid; /* OID of the relation being scanned */ + 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 + * + * We never call these ourselves. The generic node routines dispatch to them + * via GetExtensibleNodeMethods() whenever they meet a T_ExtensibleNode: + * copyObject() calls nodeCopy, equal() calls nodeEqual, nodeToString() calls + * nodeOut and stringToNode() calls nodeRead. Note that nodeOut and nodeRead + * have to agree on the set and the order of the serialized fields. + */ + +static void +test_ext_node_copy(ExtensibleNode *newnode, const ExtensibleNode *oldnode) +{ + ((TestExtNode *) newnode)->relid = ((const TestExtNode *) oldnode)->relid; + ((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)->relid == + ((const TestExtNode *) b)->relid && + ((const TestExtNode *) a)->repeat_count == + ((const TestExtNode *) b)->repeat_count; +} + +static void +test_ext_node_out(StringInfo str, const ExtensibleNode *node) +{ + appendStringInfo(str, " :relid %u", ((const TestExtNode *) node)->relid); + 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 :relid */ + token = pg_strtok(&length); /* get value */ + tnode->relid = atooid(token); + + 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 + */ + +/* + * Retrieve our private planning data from a CustomScan node. This is the place + * where the ExtensibleNode crosses from the plan tree into the executor. + */ +static TestExtNode * +test_get_ext_node(CustomScan *cscan) +{ + TestExtNode *tnode; + + Assert(list_length(cscan->custom_private) == 1); + tnode = (TestExtNode *) linitial(cscan->custom_private); + Assert(IsA(tnode, ExtensibleNode)); + Assert(strcmp(tnode->base.extnodename, TEST_EXT_NODE_NAME) == 0); + + return tnode; +} + +/* + * BeginCustomScan is the last thing ExecInitCustomScan() does, so the generic + * parts of the CustomScanState - the scan relation and the tuple slots among + * them - are ready by now. See "Custom Scan Execution Callbacks". + */ +static void +test_begin_custom_scan(CustomScanState *node, EState *estate, int eflags) +{ + TestCustomScanState *tstate = (TestCustomScanState *) node; + TestExtNode *tnode = test_get_ext_node((CustomScan *) node->ss.ps.plan); + Relation rel = node->ss.ss_currentRelation; + + Assert(tnode->repeat_count > 0); + tstate->repeat_count = tnode->repeat_count; + tstate->repeats_left = 0; + + /* Start a plain sequential table scan */ + tstate->scandesc = table_beginscan(rel, estate->es_snapshot, 0, NULL, + SO_NONE); +} + +/* + * Access method for ExecScan(): return the next tuple to be considered, or + * NULL when the scan is done + */ +static TupleTableSlot * +test_scan_next(ScanState *node) +{ + TestCustomScanState *tstate = (TestCustomScanState *) node; + TupleTableSlot *slot = node->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; +} + +/* + * Recheck method for ExecScan(), used only during EvalPlanQual rechecks. Our + * tuples come straight from an MVCC-checked table scan and we evaluate no + * quals of our own, so there is nothing to recheck. + */ +static bool +test_scan_recheck(ScanState *node, TupleTableSlot *slot) +{ + return true; +} + +static TupleTableSlot * +test_exec_custom_scan(CustomScanState *node) +{ + /* + * The core code applies neither the qual nor the projection to the tuples + * we return, so we hand the work over to the generic ExecScan() + * machinery, exactly as the built-in scan nodes do + */ + return ExecScan(&node->ss, test_scan_next, test_scan_recheck); +} + +static void +test_end_custom_scan(CustomScanState *node) +{ + TestCustomScanState *tstate = (TestCustomScanState *) node; + + if (tstate->scandesc) + table_endscan(tstate->scandesc); +} + +/* + * ReScanCustomScan has to reset our own state as well as the state kept by the + * ExecScan() machinery + */ +static void +test_rescan_custom_scan(CustomScanState *node) +{ + TestCustomScanState *tstate = (TestCustomScanState *) node; + + tstate->repeats_left = 0; + table_rescan(tstate->scandesc, NULL); + ExecScanReScan(&node->ss); +} + +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, +}; + +/* + * CreateCustomScanState allocates the CustomScanStateand and is expected + * to fill in its node tag, its methods and, optionally, slotOps. Everything + * else is left to ExecInitCustomScan(). See "Custom Scan Plan Callbacks". + */ +static Node * +test_create_custom_scan_state(CustomScan *cscan) +{ + TestCustomScanState *tstate; + TestExtNode *tnode = test_get_ext_node(cscan); + Relation rel; + + tstate = (TestCustomScanState *) + newNode(sizeof(TestCustomScanState), T_CustomScanState); + tstate->css.methods = &test_custom_exec_methods; + + /* + * Tell ExecInitCustomScan() which kind of tuple slot our ExecCustomScan + * callback is going to fill, so that table_scan_getnextslot() can store + * tuples in it directly. The answer depends on the table AM, hence + * table_slot_callbacks(). + * + * ExecInitCustomScan() opens the scan relation only after this callback + * returns, but the slot type has to be known by then. So we open the + * relation ourselves. Its OID was passed down from the planner in our + * ExtensibleNode. No lock is needed, as the query already holds one on + * every relation mentioned in its range table. + */ + rel = table_open(tnode->relid, NoLock); + tstate->css.slotOps = table_slot_callbacks(rel); + table_close(rel, NoLock); + + return (Node *) tstate; +} + +static const CustomScanMethods test_custom_scan_methods = +{ + .CustomName = TEST_CUSTOM_SCAN_NAME, + .CreateCustomScanState = test_create_custom_scan_state, +}; + +/* + * Planner callbacks + */ + +/* + * PlanCustomPath turns our CustomPath into the CustomScan plan node that the + * executor is going to run. The cost data and custom_relids are copied from + * the path by the core once we return, so we only have to describe what the + * scan does. + */ +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); + + cscan->scan.plan.targetlist = tlist; + + /* + * Unlike the built-in scan nodes, a custom scan provider receives the + * restriction clauses as RestrictInfo nodes rather than as bare + * expressions. That is deliberate: the provider is free to evaluate some + * of the clauses on its own (say, push them down to a remote server), and + * the planner data in RestrictInfo helps it decide which ones. The + * clauses we want the executor to check have to be reduced to bare + * expressions and stored in the plan's qual, as "Creating Custom Scan + * Plans" requires the scan to be initialized like any other one. + * + * The second argument of extract_actual_clauses() selects the regular + * clauses rather than the pseudoconstant ones (constant-TRUE clauses are + * dropped in either case). Pseudoconstants must not end up in the qual: + * they do not depend on the scanned tuple, so the core evaluates them + * once in a gating Result node placed above us. + */ + cscan->scan.plan.qual = extract_actual_clauses(clauses, false); + cscan->scan.scanrelid = rel->relid; + cscan->flags = best_path->flags; + + /* Our CustomPath has no child paths */ + cscan->custom_plans = custom_plans; + cscan->custom_exprs = NIL; + + /* + * 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->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, +}; + +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 data our executor callbacks are going + * to need + */ + tnode = (TestExtNode *) newNode(sizeof(TestExtNode), T_ExtensibleNode); + tnode->base.extnodename = TEST_EXT_NODE_NAME; + tnode->relid = rte->relid; + tnode->repeat_count = 2; /* each row will be returned twice */ + + /* + * Build the CustomPath. Claiming zero cost makes the planner prefer our + * path over the sequential scan, which keeps the test deterministic. A + * real provider would estimate the cost honestly and would also report + * the number of rows it actually returns, which in our case is + * repeat_count times the number of rows in the table. + */ + 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 + */ +PG_FUNCTION_INFO_V1(test_get_extensible_node_methods); +PG_FUNCTION_INFO_V1(test_get_custom_scan_methods); +PG_FUNCTION_INFO_V1(test_ext_node_callbacks); + +/* + * 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)); +} + +/* + * test_ext_node_callbacks(relid oid, repeat_count int) + * + * Exercises all four ExtensibleNodeMethods callbacks, each of which is reached + * through GetExtensibleNodeMethods(): nodeCopy via copyObject(), nodeEqual via + * equal(), nodeOut via nodeToString() and nodeRead via stringToNode(). + * + * Returns the serialized form of the node. Reports an ERROR if any of the + * callbacks misbehaves. + */ +Datum +test_ext_node_callbacks(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + int repeat_count = PG_GETARG_INT32(1); + TestExtNode *tnode; + TestExtNode *copy; + TestExtNode *restored; + char *str; + + tnode = (TestExtNode *) newNode(sizeof(TestExtNode), T_ExtensibleNode); + tnode->base.extnodename = TEST_EXT_NODE_NAME; + tnode->relid = relid; + tnode->repeat_count = repeat_count; + + /* nodeCopy + nodeEqual */ + copy = copyObject(tnode); + if (!equal(tnode, copy)) + elog(ERROR, "copy of " TEST_EXT_NODE_NAME " is not equal to the original"); + + /* nodeEqual must also be able to tell two different nodes apart */ + copy->repeat_count++; + if (equal(tnode, copy)) + elog(ERROR, "nodes with different repeat_count compare as equal"); + + /* nodeOut + nodeRead */ + str = nodeToString(tnode); + restored = (TestExtNode *) stringToNode(str); + if (!equal(tnode, restored)) + elog(ERROR, "deserialized " TEST_EXT_NODE_NAME " is not equal to the original"); + + PG_RETURN_TEXT_P(cstring_to_text(str)); +} + +/* + * Module initialization + */ +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. Every backend that plans or executes + * such a scan has to do this, because the plan tree refers to the methods + * by name only; that includes parallel workers, which read the plan back + * with stringToNode(). Hence the shared_preload_libraries requirement. + */ + 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
