Hi, > > Apart from this, the patch looks good to me. > > Thanks for the fast and detailed feedback. I will submit an updated > patch shortly.
Here is the corrected patch which now also includes tests for the inherited tables. -- Best regards, Aleksander Alekseev
From 3df21378d2e3212b43f12aabe9c8bfec45938b02 Mon Sep 17 00:00:00 2001 From: Aleksander Alekseev <[email protected]> Date: Tue, 31 Mar 2026 15:38:21 +0300 Subject: [PATCH v5] 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(), GetCustomScanMethods() and the four ExtensibleNodeMethods callbacks. The CustomPath is parallel safe, so debug_parallel_query also exercises nodeOut and nodeRead in the executor. Author: Aleksander Alekseev <[email protected]> Reviewed-by: Rafia Sabih <[email protected]> Reviewed-by: Jan Nidzwetzki <[email protected]> Reviewed-by: Michael Paquier <[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/.gitignore | 4 + src/test/modules/test_extensible/Makefile | 27 + .../expected/test_extensible.out | 296 ++++++++ src/test/modules/test_extensible/meson.build | 35 + .../test_extensible/sql/test_extensible.sql | 148 ++++ .../test_extensible/test_extensible--1.0.sql | 25 + .../modules/test_extensible/test_extensible.c | 699 ++++++++++++++++++ .../test_extensible/test_extensible.conf | 1 + .../test_extensible/test_extensible.control | 4 + src/tools/pgindent/typedefs.list | 2 + 12 files changed, 1243 insertions(+) create mode 100644 src/test/modules/test_extensible/.gitignore 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 bb88b3058ed..71a2e65ad70 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 ce09e00531d..77e1a2810e5 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/.gitignore b/src/test/modules/test_extensible/.gitignore new file mode 100644 index 00000000000..5dcb3ff9723 --- /dev/null +++ b/src/test/modules/test_extensible/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ 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..d71c4386311 --- /dev/null +++ b/src/test/modules/test_extensible/expected/test_extensible.out @@ -0,0 +1,296 @@ +-- 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 +-- Both lookup functions are STRICT, so a NULL argument yields NULL rather than +-- reaching the C code, which would dereference the argument unconditionally. +SELECT test_get_extensible_node_methods(NULL, false) IS NULL AS null_name; + null_name +----------- + t +(1 row) + +SELECT test_get_extensible_node_methods('TestExtNode', NULL) IS NULL AS null_missing_ok; + null_missing_ok +----------------- + t +(1 row) + +SELECT test_get_custom_scan_methods(NULL, false) IS NULL AS null_name; + null_name +----------- + t +(1 row) + +SELECT test_get_custom_scan_methods('TestCustomScan', NULL) IS NULL AS null_missing_ok; + null_missing_ok +----------------- + t +(1 row) + +-- ---------------------------------------------------------------- +-- ExtensibleNodeMethods callbacks: nodeCopy, nodeEqual, nodeOut, nodeRead +-- ---------------------------------------------------------------- +-- A TestExtNode travels between these functions in its serialized form, so +-- every one of them runs nodeOut, nodeRead or both. The text below is what the +-- nodeOut callback produced. +SELECT test_ext_node_make('1234'::oid, 2); + test_ext_node_make +----------------------------------------------------------------------- + {EXTENSIBLENODE :extnodename TestExtNode :relid 1234 :repeat_count 2} +(1 row) + +-- nodeRead has to restore both fields exactly as nodeOut wrote them. +SELECT test_ext_node_get_relid(n) AS relid, + test_ext_node_get_repeat_count(n) AS repeat_count + FROM (SELECT test_ext_node_make('1234'::oid, 2)) AS s(n); + relid | repeat_count +-------+-------------- + 1234 | 2 +(1 row) + +-- nodeCopy: the copy compares equal to the original and, since it has to copy +-- every field, serializes to the very same string. +SELECT test_ext_node_equal(n, test_ext_node_copy(n)) AS copy_is_equal, + test_ext_node_copy(n) = n AS copy_is_identical + FROM (SELECT test_ext_node_make('1234'::oid, 2)) AS s(n); + copy_is_equal | copy_is_identical +---------------+------------------- + t | t +(1 row) + +-- nodeEqual: nodes differing in either field must not compare equal. +SELECT test_ext_node_equal(test_ext_node_make('1234'::oid, 2), + test_ext_node_make('1234'::oid, 3)) AS other_repeat_count, + test_ext_node_equal(test_ext_node_make('1234'::oid, 2), + test_ext_node_make('5678'::oid, 2)) AS other_relid; + other_repeat_count | other_relid +--------------------+------------- + f | f +(1 row) + +-- A string describing some other kind of node is rejected rather than being +-- misinterpreted as a TestExtNode. '(b 1 2)' is a serialized Bitmapset. +SELECT test_ext_node_get_repeat_count('(b 1 2)'); +ERROR: argument is not a serialized "TestExtNode" +-- A node of the right type but with fields missing is rejected as well, rather +-- than reaching the field conversions with a NULL token. +SELECT test_ext_node_get_relid('{EXTENSIBLENODE :extnodename TestExtNode}'); +ERROR: unexpected end of "TestExtNode" +-- ---------------------------------------------------------------- +-- 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) + +-- How many times a row is repeated is controlled by a GUC. It is read while +-- planning and stored in the ExtensibleNode, so it is the value in effect at +-- plan time that counts. +SET test_extensible.repeat_count = 3; +SELECT id, val FROM test_extensible_tbl ORDER BY id; + id | val +----+------- + 1 | one + 1 | one + 1 | one + 2 | two + 2 | two + 2 | two + 3 | three + 3 | three + 3 | three +(9 rows) + +RESET test_extensible.repeat_count; +-- Out-of-range values are rejected by the GUC machinery. +SET test_extensible.repeat_count = 0; +ERROR: 0 is outside the valid range for parameter "test_extensible.repeat_count" (1 .. 100) +-- Scans our executor callbacks cannot implement are left to the core. Without +-- this the sample scan would be replaced and the whole table returned. +EXPLAIN (COSTS OFF) SELECT id FROM test_extensible_tbl TABLESAMPLE SYSTEM (0); + QUERY PLAN +------------------------------------ + Sample Scan on test_extensible_tbl + Sampling: system ('0'::real) +(2 rows) + +SELECT count(*) FROM test_extensible_tbl TABLESAMPLE SYSTEM (0); + count +------- + 0 +(1 row) + +-- ---------------------------------------------------------------- +-- The CustomScan below a Gather +-- ---------------------------------------------------------------- +-- Our CustomPath is parallel safe, so the plan can be pushed below a Gather. +-- This is the only case in which the core round-trips our ExtensibleNode +-- through nodeOut and nodeRead on its own: the plan tree is serialized into +-- dynamic shared memory and read back by the parallel worker. +-- +-- max_parallel_workers_per_gather is set explicitly because a value of 0 would +-- silently switch parallelism off altogether, leaving these queries passing +-- but testing nothing. +SET max_parallel_workers_per_gather = 2; +-- With debug_parallel_query = on the Gather is visible, which is what proves +-- the plan really is being serialized. +SET debug_parallel_query = on; +EXPLAIN (COSTS OFF) SELECT id, val FROM test_extensible_tbl ORDER BY id; + QUERY PLAN +----------------------------------------------------------------- + Gather + Workers Planned: 1 + Single Copy: true + -> Sort + Sort Key: id + -> Custom Scan (TestCustomScan) on test_extensible_tbl +(6 rows) + +-- In the regress mode the Gather is hidden, so the plan is expected to look +-- exactly like the serial one further up. +SET debug_parallel_query = regress; +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) + +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) + +RESET debug_parallel_query; +RESET max_parallel_workers_per_gather; +-- ---------------------------------------------------------------- +-- Inheritance +-- ---------------------------------------------------------------- +-- An inheritance parent stands for its whole hierarchy, so it has to be left +-- to the core as well; taking it over would drop the child table's rows. +-- +-- This comes last on purpose: pg_class.relhassubclass stays set once a child +-- has existed, so the planner keeps expanding the parent even after the child +-- is dropped, and no later query would reach our CustomScan again. +CREATE TABLE test_extensible_child () INHERITS (test_extensible_tbl); +INSERT INTO test_extensible_child VALUES (4, 'four'); +EXPLAIN (COSTS OFF) SELECT id, val FROM test_extensible_tbl; + QUERY PLAN +--------------------------------------------------------------- + Append + -> Seq Scan on test_extensible_tbl test_extensible_tbl_1 + -> Seq Scan on test_extensible_child test_extensible_tbl_2 +(3 rows) + +SELECT id, val FROM test_extensible_tbl ORDER BY id; + id | val +----+------- + 1 | one + 2 | two + 3 | three + 4 | four +(4 rows) + +-- Naming just the parent with ONLY is a plain scan again, so this one is ours: +-- the parent's own rows come back repeated, and the child's row stays out. +EXPLAIN (COSTS OFF) SELECT id, val FROM ONLY test_extensible_tbl; + QUERY PLAN +----------------------------------------------------- + Custom Scan (TestCustomScan) on test_extensible_tbl +(1 row) + +SELECT id, val FROM ONLY test_extensible_tbl ORDER BY id; + id | val +----+------- + 1 | one + 1 | one + 2 | two + 2 | two + 3 | three + 3 | three +(6 rows) + +DROP TABLE test_extensible_child; +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..fc972e711c0 --- /dev/null +++ b/src/test/modules/test_extensible/sql/test_extensible.sql @@ -0,0 +1,148 @@ +-- 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); + +-- Both lookup functions are STRICT, so a NULL argument yields NULL rather than +-- reaching the C code, which would dereference the argument unconditionally. +SELECT test_get_extensible_node_methods(NULL, false) IS NULL AS null_name; +SELECT test_get_extensible_node_methods('TestExtNode', NULL) IS NULL AS null_missing_ok; +SELECT test_get_custom_scan_methods(NULL, false) IS NULL AS null_name; +SELECT test_get_custom_scan_methods('TestCustomScan', NULL) IS NULL AS null_missing_ok; + +-- ---------------------------------------------------------------- +-- ExtensibleNodeMethods callbacks: nodeCopy, nodeEqual, nodeOut, nodeRead +-- ---------------------------------------------------------------- + +-- A TestExtNode travels between these functions in its serialized form, so +-- every one of them runs nodeOut, nodeRead or both. The text below is what the +-- nodeOut callback produced. +SELECT test_ext_node_make('1234'::oid, 2); + +-- nodeRead has to restore both fields exactly as nodeOut wrote them. +SELECT test_ext_node_get_relid(n) AS relid, + test_ext_node_get_repeat_count(n) AS repeat_count + FROM (SELECT test_ext_node_make('1234'::oid, 2)) AS s(n); + +-- nodeCopy: the copy compares equal to the original and, since it has to copy +-- every field, serializes to the very same string. +SELECT test_ext_node_equal(n, test_ext_node_copy(n)) AS copy_is_equal, + test_ext_node_copy(n) = n AS copy_is_identical + FROM (SELECT test_ext_node_make('1234'::oid, 2)) AS s(n); + +-- nodeEqual: nodes differing in either field must not compare equal. +SELECT test_ext_node_equal(test_ext_node_make('1234'::oid, 2), + test_ext_node_make('1234'::oid, 3)) AS other_repeat_count, + test_ext_node_equal(test_ext_node_make('1234'::oid, 2), + test_ext_node_make('5678'::oid, 2)) AS other_relid; + +-- A string describing some other kind of node is rejected rather than being +-- misinterpreted as a TestExtNode. '(b 1 2)' is a serialized Bitmapset. +SELECT test_ext_node_get_repeat_count('(b 1 2)'); + +-- A node of the right type but with fields missing is rejected as well, rather +-- than reaching the field conversions with a NULL token. +SELECT test_ext_node_get_relid('{EXTENSIBLENODE :extnodename TestExtNode}'); + +-- ---------------------------------------------------------------- +-- 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; + +-- How many times a row is repeated is controlled by a GUC. It is read while +-- planning and stored in the ExtensibleNode, so it is the value in effect at +-- plan time that counts. +SET test_extensible.repeat_count = 3; +SELECT id, val FROM test_extensible_tbl ORDER BY id; +RESET test_extensible.repeat_count; + +-- Out-of-range values are rejected by the GUC machinery. +SET test_extensible.repeat_count = 0; + +-- Scans our executor callbacks cannot implement are left to the core. Without +-- this the sample scan would be replaced and the whole table returned. +EXPLAIN (COSTS OFF) SELECT id FROM test_extensible_tbl TABLESAMPLE SYSTEM (0); +SELECT count(*) FROM test_extensible_tbl TABLESAMPLE SYSTEM (0); + +-- ---------------------------------------------------------------- +-- The CustomScan below a Gather +-- ---------------------------------------------------------------- + +-- Our CustomPath is parallel safe, so the plan can be pushed below a Gather. +-- This is the only case in which the core round-trips our ExtensibleNode +-- through nodeOut and nodeRead on its own: the plan tree is serialized into +-- dynamic shared memory and read back by the parallel worker. +-- +-- max_parallel_workers_per_gather is set explicitly because a value of 0 would +-- silently switch parallelism off altogether, leaving these queries passing +-- but testing nothing. +SET max_parallel_workers_per_gather = 2; + +-- With debug_parallel_query = on the Gather is visible, which is what proves +-- the plan really is being serialized. +SET debug_parallel_query = on; +EXPLAIN (COSTS OFF) SELECT id, val FROM test_extensible_tbl ORDER BY id; + +-- In the regress mode the Gather is hidden, so the plan is expected to look +-- exactly like the serial one further up. +SET debug_parallel_query = regress; +EXPLAIN (COSTS OFF) SELECT id, val FROM test_extensible_tbl ORDER BY id; +SELECT id, val FROM test_extensible_tbl ORDER BY id; +RESET debug_parallel_query; +RESET max_parallel_workers_per_gather; + +-- ---------------------------------------------------------------- +-- Inheritance +-- ---------------------------------------------------------------- + +-- An inheritance parent stands for its whole hierarchy, so it has to be left +-- to the core as well; taking it over would drop the child table's rows. +-- +-- This comes last on purpose: pg_class.relhassubclass stays set once a child +-- has existed, so the planner keeps expanding the parent even after the child +-- is dropped, and no later query would reach our CustomScan again. +CREATE TABLE test_extensible_child () INHERITS (test_extensible_tbl); +INSERT INTO test_extensible_child VALUES (4, 'four'); +EXPLAIN (COSTS OFF) SELECT id, val FROM test_extensible_tbl; +SELECT id, val FROM test_extensible_tbl ORDER BY id; + +-- Naming just the parent with ONLY is a plain scan again, so this one is ours: +-- the parent's own rows come back repeated, and the child's row stays out. +EXPLAIN (COSTS OFF) SELECT id, val FROM ONLY test_extensible_tbl; +SELECT id, val FROM ONLY test_extensible_tbl ORDER BY id; +DROP TABLE test_extensible_child; + +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..cd904317e7a --- /dev/null +++ b/src/test/modules/test_extensible/test_extensible--1.0.sql @@ -0,0 +1,25 @@ +/* src/test/modules/test_extensible/test_extensible--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_extensible" to load this file. \quit + +CREATE FUNCTION test_get_extensible_node_methods(text, bool) RETURNS text + AS 'MODULE_PATHNAME', 'test_get_extensible_node_methods' LANGUAGE C STRICT; + +CREATE FUNCTION test_get_custom_scan_methods(text, bool) RETURNS text + AS 'MODULE_PATHNAME', 'test_get_custom_scan_methods' LANGUAGE C STRICT; + +CREATE FUNCTION test_ext_node_make(oid, int) RETURNS text + AS 'MODULE_PATHNAME', 'test_ext_node_make' LANGUAGE C STRICT; + +CREATE FUNCTION test_ext_node_copy(text) RETURNS text + AS 'MODULE_PATHNAME', 'test_ext_node_copy' LANGUAGE C STRICT; + +CREATE FUNCTION test_ext_node_equal(text, text) RETURNS bool + AS 'MODULE_PATHNAME', 'test_ext_node_equal' LANGUAGE C STRICT; + +CREATE FUNCTION test_ext_node_get_relid(text) RETURNS oid + AS 'MODULE_PATHNAME', 'test_ext_node_get_relid' LANGUAGE C STRICT; + +CREATE FUNCTION test_ext_node_get_repeat_count(text) RETURNS int + AS 'MODULE_PATHNAME', 'test_ext_node_get_repeat_count' LANGUAGE C STRICT; 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..e75008daae1 --- /dev/null +++ b/src/test/modules/test_extensible/test_extensible.c @@ -0,0 +1,699 @@ +/*------------------------------------------------------------------------- + * + * 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/pathnode.h" +#include "optimizer/paths.h" +#include "optimizer/restrictinfo.h" +#include "utils/builtins.h" +#include "utils/guc.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" + +/* GUC: how many times the custom scan returns each row */ +static int test_repeat_count = 2; + +/* + * 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_cb(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_cb(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_cb(StringInfo str, const ExtensibleNode *node) +{ + appendStringInfo(str, " :relid %u", ((const TestExtNode *) node)->relid); + appendStringInfo(str, " :repeat_count %d", + ((const TestExtNode *) node)->repeat_count); +} + +/* + * Fetch the next token, which is required to be there. + * + * readfuncs.c can afford to skip this check because it only ever parses + * strings that the core itself produced. This callback is also reachable with + * an arbitrary string through the SQL functions at the bottom of this file, so + * a truncated node must not be allowed to reach atoi() with a NULL pointer. + */ +static const char * +test_ext_node_next_token(ReadNodeContext *ctx) +{ + int length; + const char *token = pg_strtok(ctx, &length); + + if (token == NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("unexpected end of \"%s\"", TEST_EXT_NODE_NAME))); + + return token; +} + +static void +test_ext_node_read_cb(ReadNodeContext *ctx, ExtensibleNode *node) +{ + TestExtNode *tnode = (TestExtNode *) node; + + (void) test_ext_node_next_token(ctx); /* skip :relid */ + tnode->relid = atooid(test_ext_node_next_token(ctx)); + + (void) test_ext_node_next_token(ctx); /* skip :repeat_count */ + tnode->repeat_count = atoi(test_ext_node_next_token(ctx)); +} + +static const ExtensibleNodeMethods test_ext_node_methods = +{ + .extnodename = TEST_EXT_NODE_NAME, + .node_size = sizeof(TestExtNode), + .nodeCopy = test_ext_node_copy_cb, + .nodeEqual = test_ext_node_equal_cb, + .nodeOut = test_ext_node_out_cb, + .nodeRead = test_ext_node_read_cb, +}; + +/* + * 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; + tstate->scandesc = NULL; + + /* + * A plain EXPLAIN initializes the plan but never executes it, so there is + * no point in starting a scan. The built-in scan nodes all return early + * here as well. + */ + if (eflags & EXEC_FLAG_EXPLAIN_ONLY) + return; + + /* 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; + + /* No scan was started under EXEC_FLAG_EXPLAIN_ONLY */ + if (tstate->scandesc != NULL) + 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 CustomScanState and fills in its node + * tag and its methods; everything else is left to ExecInitCustomScan(). See + * "Custom Scan Plan Callbacks". + * + * slotOps is the one exception to the "leave the other fields zeroed" rule + * stated there: ExecInitCustomScan() reads it before creating the scan tuple + * slot, so this callback is the only place where a provider can choose + * anything other than the default TTSOpsVirtual. + */ +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. + * + * We have to take the lock rather than pass NoLock: in a parallel worker + * nothing has locked the relation yet at this point. The worker acquires + * its own lock later, in ExecGetRangeTableRelation(), once + * ExecInitCustomScan() gets to open the relation. AccessShareLock is what + * a plain scan uses, so this never introduces an extra conflict, and it + * is held until end of transaction as usual. + */ + rel = table_open(tnode->relid, AccessShareLock); + 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 that our executor callbacks can + * actually scan. + * + * The relkind test matters: table_slot_callbacks() and table_beginscan() + * require a relation with a table AM, which rules out foreign tables, + * partitioned tables and the like. + * + * TABLESAMPLE is excluded because we would otherwise replace the sample + * scan and return the whole relation. An inheritance parent (rte->inh) is + * excluded because it stands for the whole hierarchy: scanning its own + * storage alone would silently drop the rows of the child tables. + */ + if (rel->reloptkind != RELOPT_BASEREL || rte->rtekind != RTE_RELATION) + return; + if (rte->relkind != RELKIND_RELATION && rte->relkind != RELKIND_MATVIEW) + return; + if (rte->tablesample != NULL || rte->inh) + 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; + + /* + * The repeat count is read once, here, and travels to the executor inside + * the ExtensibleNode. Changing the GUC afterwards therefore has no effect + * on an already planned query, which is the behavior one wants from + * something the plan depends on. + */ + tnode->repeat_count = test_repeat_count; + + /* + * Build the CustomPath. Claiming zero cost is what gets our path chosen + * over the sequential scan here; a real provider would estimate the cost + * honestly. Note that this is not a guarantee: add_path() compares costs + * fuzzily and falls back on other path properties when they tie, so a + * provider must not assume that the cheapest-looking path always wins. + * The row count, on the other hand, is reported truthfully: we hand back + * each scanned row repeat_count times. + */ + cpath = makeNode(CustomPath); + cpath->path.pathtype = T_CustomScan; + cpath->path.parent = rel; + cpath->path.pathtarget = rel->reltarget; + cpath->path.rows = rel->rows * tnode->repeat_count; + cpath->path.startup_cost = 0; + cpath->path.total_cost = 0; + + /* + * Our scan touches nothing but its own relation and keeps no state + * outside the CustomScanState, so it is safe to run inside a parallel + * worker. rel->consider_parallel tells us whether the planner deems + * parallelism legal for this relation in the first place, which is what + * the core scan paths key off as well. + * + * This is what lets the plan be pushed below a Gather, and that in turn + * is the only way the nodeOut and nodeRead callbacks of our + * ExtensibleNode get exercised by the core: the plan tree, custom_private + * included, is serialized into dynamic shared memory and read back by the + * worker. + * + * Note that the path is parallel safe but not parallel *aware*: our scan + * does not divide the work between workers, so we never offer it to + * add_partial_path(). + */ + cpath->path.parallel_safe = rel->consider_parallel; + + 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_make); +PG_FUNCTION_INFO_V1(test_ext_node_copy); +PG_FUNCTION_INFO_V1(test_ext_node_equal); +PG_FUNCTION_INFO_V1(test_ext_node_get_relid); +PG_FUNCTION_INFO_V1(test_ext_node_get_repeat_count); + +/* + * 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)); +} + +/* + * The functions below publish the ExtensibleNodeMethods callbacks to the SQL + * level as thin wrappers, so that each operation can be driven and checked + * individually from the regression test rather than from C. + * + * A TestExtNode is passed around as the text produced by nodeToString(), the + * same way test_bitmapset.c passes Bitmapsets around. That representation is + * what makes the nodes composable in SQL, and it has the pleasant side effect + * of running nodeOut and nodeRead on every hop. + */ + +/* Encode a TestExtNode into its serialized representation */ +#define TEST_EXT_NODE_TO_TEXT(node) cstring_to_text(nodeToString(node)) + +/* Decode a function argument back into a TestExtNode */ +#define PG_GETARG_TEST_EXT_NODE(n) text_to_test_ext_node(PG_GETARG_TEXT_PP(n)) + +/* + * Decode a TestExtNode, i.e. run the nodeRead callback via stringToNode(). + * + * The result is checked rather than blindly cast, so that a string describing + * some other kind of node is reported as an error instead of being + * misinterpreted as one of ours. Note that this says nothing about strings + * that stringToNode() itself cannot digest: like the rest of readfuncs.c, it + * assumes well-formed input. + */ +static TestExtNode * +text_to_test_ext_node(text *txt) +{ + Node *node = stringToNode(text_to_cstring(txt)); + + if (node == NULL || !IsA(node, ExtensibleNode) || + strcmp(((ExtensibleNode *) node)->extnodename, TEST_EXT_NODE_NAME) != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("argument is not a serialized \"%s\"", + TEST_EXT_NODE_NAME))); + + return (TestExtNode *) node; +} + +/* + * test_ext_node_make(relid oid, repeat_count int) + * + * Builds a TestExtNode and returns it. The returned text is the output of the + * nodeOut callback. + */ +Datum +test_ext_node_make(PG_FUNCTION_ARGS) +{ + TestExtNode *tnode; + + tnode = (TestExtNode *) newNode(sizeof(TestExtNode), T_ExtensibleNode); + tnode->base.extnodename = TEST_EXT_NODE_NAME; + tnode->relid = PG_GETARG_OID(0); + tnode->repeat_count = PG_GETARG_INT32(1); + + PG_RETURN_TEXT_P(TEST_EXT_NODE_TO_TEXT(tnode)); +} + +/* + * test_ext_node_copy(node text) + * + * Wrapper around copyObject(), which reaches the nodeCopy callback. + */ +Datum +test_ext_node_copy(PG_FUNCTION_ARGS) +{ + TestExtNode *tnode = PG_GETARG_TEST_EXT_NODE(0); + + PG_RETURN_TEXT_P(TEST_EXT_NODE_TO_TEXT(copyObject(tnode))); +} + +/* + * test_ext_node_equal(a text, b text) + * + * Wrapper around equal(), which reaches the nodeEqual callback. + */ +Datum +test_ext_node_equal(PG_FUNCTION_ARGS) +{ + TestExtNode *a = PG_GETARG_TEST_EXT_NODE(0); + TestExtNode *b = PG_GETARG_TEST_EXT_NODE(1); + + PG_RETURN_BOOL(equal(a, b)); +} + +/* + * test_ext_node_get_relid(node text) + * test_ext_node_get_repeat_count(node text) + * + * Field accessors. They exist to check that nodeRead restores what nodeOut + * wrote, without relying on the exact serialized text. + */ +Datum +test_ext_node_get_relid(PG_FUNCTION_ARGS) +{ + PG_RETURN_OID(PG_GETARG_TEST_EXT_NODE(0)->relid); +} + +Datum +test_ext_node_get_repeat_count(PG_FUNCTION_ARGS) +{ + PG_RETURN_INT32(PG_GETARG_TEST_EXT_NODE(0)->repeat_count); +} + +/* + * 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); + + DefineCustomIntVariable("test_extensible.repeat_count", + "Number of times the custom scan returns each row.", + NULL, + &test_repeat_count, + 2, + 1, + 100, + PGC_USERSET, + 0, + NULL, NULL, NULL); + + MarkGUCPrefixReserved("test_extensible"); + + /* 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 diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 59cf40b5bb0..5a123f34fee 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3186,10 +3186,12 @@ Tcl_Obj Tcl_Size Tcl_Time TempNamespaceStatus +TestCustomScanState TestDSMRegistryHashEntry TestDSMRegistryStruct TestDecodingData TestDecodingTxnData +TestExtNode TestShmemData TestSpec TestValueType -- 2.43.0
