Branch data Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * test_extensible.c
4 : : * Tests for extensible nodes and custom scans
5 : : *
6 : : * Copyright (c) 2026, PostgreSQL Global Development Group
7 : : *
8 : : * IDENTIFICATION
9 : : * src/test/modules/test_extensible/test_extensible.c
10 : : *
11 : : *-------------------------------------------------------------------------
12 : : */
13 : :
14 : : #include "postgres.h"
15 : :
16 : : #include "access/table.h"
17 : : #include "access/tableam.h"
18 : : #include "catalog/namespace.h"
19 : : #include "executor/executor.h"
20 : : #include "fmgr.h"
21 : : #include "miscadmin.h"
22 : : #include "nodes/extensible.h"
23 : : #include "nodes/nodes.h"
24 : : #include "nodes/plannodes.h"
25 : : #include "nodes/readfuncs.h"
26 : : #include "optimizer/pathnode.h"
27 : : #include "optimizer/paths.h"
28 : : #include "optimizer/restrictinfo.h"
29 : : #include "utils/builtins.h"
30 : : #include "utils/guc.h"
31 : : #include "utils/lsyscache.h"
32 : :
33 : 2 : PG_MODULE_MAGIC;
34 : :
35 : : /* Name of the test table that triggers our CustomScan injection */
36 : : #define TEST_TABLE_NAME "test_extensible_tbl"
37 : :
38 : : /*
39 : : * TestExtNode - an ExtensibleNode subtype carrying our planning data.
40 : : */
41 : : typedef struct TestExtNode
42 : : {
43 : : ExtensibleNode base; /* must be first */
44 : : Oid relid; /* OID of the relation being scanned */
45 : : int repeat_count; /* how many times to return each scanned row */
46 : : } TestExtNode;
47 : :
48 : : #define TEST_EXT_NODE_NAME "TestExtNode"
49 : : #define TEST_CUSTOM_SCAN_NAME "TestCustomScan"
50 : :
51 : : /* GUC: how many times the custom scan returns each row */
52 : : static int test_repeat_count = 2;
53 : :
54 : : static TestExtNode *text_to_test_ext_node(text *txt);
55 : :
56 : : /* Encode a TestExtNode into its serialized representation */
57 : : #define TEST_EXT_NODE_TO_TEXT(node) cstring_to_text(nodeToString(node))
58 : :
59 : : /* Decode a function argument back into a TestExtNode */
60 : : #define PG_GETARG_TEST_EXT_NODE(n) text_to_test_ext_node(PG_GETARG_TEXT_PP(n))
61 : :
62 : 2 : PG_FUNCTION_INFO_V1(test_get_extensible_node_methods);
63 : 2 : PG_FUNCTION_INFO_V1(test_get_custom_scan_methods);
64 : 2 : PG_FUNCTION_INFO_V1(test_ext_node_make);
65 : 2 : PG_FUNCTION_INFO_V1(test_ext_node_copy);
66 : 2 : PG_FUNCTION_INFO_V1(test_ext_node_equal);
67 : 2 : PG_FUNCTION_INFO_V1(test_ext_node_get_relid);
68 : 2 : PG_FUNCTION_INFO_V1(test_ext_node_get_repeat_count);
69 : :
70 : : /*
71 : : * ExtensibleNodeMethods callbacks.
72 : : *
73 : : * Note that nodeOut() and nodeRead() must agree on the set and order of
74 : : * serialized fields.
75 : : */
76 : : static void
77 : 13 : test_ext_node_copy_cb(ExtensibleNode *newnode, const ExtensibleNode *oldnode)
78 : : {
79 : 13 : ((TestExtNode *) newnode)->relid = ((const TestExtNode *) oldnode)->relid;
80 : 13 : ((TestExtNode *) newnode)->repeat_count =
81 : 13 : ((const TestExtNode *) oldnode)->repeat_count;
82 : 13 : }
83 : :
84 : : static bool
85 : 3 : test_ext_node_equal_cb(const ExtensibleNode *a, const ExtensibleNode *b)
86 : : {
87 : 3 : return ((const TestExtNode *) a)->relid ==
88 [ + + ]: 5 : ((const TestExtNode *) b)->relid &&
89 : 2 : ((const TestExtNode *) a)->repeat_count ==
90 [ + + ]: 2 : ((const TestExtNode *) b)->repeat_count;
91 : : }
92 : :
93 : : static void
94 : 20 : test_ext_node_out_cb(StringInfo str, const ExtensibleNode *node)
95 : : {
96 : 20 : appendStringInfo(str, " :relid %u", ((const TestExtNode *) node)->relid);
97 : 20 : appendStringInfo(str, " :repeat_count %d",
98 : 20 : ((const TestExtNode *) node)->repeat_count);
99 : 20 : }
100 : :
101 : : /*
102 : : * Fetch the next token of the string being read.
103 : : *
104 : : * Unlike anything in readfuncs.c, this is reachable with arbitrary strings
105 : : * through SQL function calls, so the tokens need to be checked.
106 : : *
107 : : * If "expected" is specified, check that the token fetched matches with
108 : : * the input expected. Running out of tokens is a failure.
109 : : */
110 : : static const char *
111 : 137 : test_ext_node_next_token(ReadNodeContext *ctx, const char *expected)
112 : : {
113 : : int length;
114 : 137 : const char *token = pg_strtok(ctx, &length);
115 : :
116 [ + + ]: 137 : if (token == NULL)
117 [ + - ]: 1 : ereport(ERROR,
118 : : (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
119 : : errmsg("unexpected end of \"%s\"", TEST_EXT_NODE_NAME)));
120 : :
121 [ + + ]: 136 : if (expected != NULL)
122 : : {
123 [ + + ]: 51 : if (length != (int) strlen(expected))
124 : 3 : return NULL;
125 [ - + ]: 48 : if (memcmp(token, expected, length) != 0)
126 : 0 : return NULL;
127 : : }
128 : :
129 : 133 : return token;
130 : : }
131 : :
132 : : static void
133 : 22 : test_ext_node_read_cb(ReadNodeContext *ctx, ExtensibleNode *node)
134 : : {
135 : 22 : TestExtNode *tnode = (TestExtNode *) node;
136 : :
137 : 22 : (void) test_ext_node_next_token(ctx, NULL); /* skip :relid */
138 : 22 : tnode->relid = atooid(test_ext_node_next_token(ctx, NULL));
139 : :
140 : 21 : (void) test_ext_node_next_token(ctx, NULL); /* skip :repeat_count */
141 : 21 : tnode->repeat_count = atoi(test_ext_node_next_token(ctx, NULL));
142 : 21 : }
143 : :
144 : : static const ExtensibleNodeMethods test_ext_node_methods =
145 : : {
146 : : .extnodename = TEST_EXT_NODE_NAME,
147 : : .node_size = sizeof(TestExtNode),
148 : : .nodeCopy = test_ext_node_copy_cb,
149 : : .nodeEqual = test_ext_node_equal_cb,
150 : : .nodeOut = test_ext_node_out_cb,
151 : : .nodeRead = test_ext_node_read_cb,
152 : : };
153 : :
154 : : /*
155 : : * TestCustomScanState - execution state for the custom scan
156 : : */
157 : : typedef struct TestCustomScanState
158 : : {
159 : : CustomScanState css; /* must be first */
160 : : TableScanDesc scandesc;
161 : : int repeat_count; /* repeat_count from TestExtNode */
162 : : int repeats_left; /* how many more times to return current row */
163 : : } TestCustomScanState;
164 : :
165 : : /*
166 : : * Executor callbacks
167 : : */
168 : :
169 : : /*
170 : : * Retrieve our private planning data from a CustomScan node.
171 : : */
172 : : static TestExtNode *
173 : 22 : test_get_ext_node(CustomScan *cscan)
174 : : {
175 : : TestExtNode *tnode;
176 : :
177 : : Assert(list_length(cscan->custom_private) == 1);
178 : 22 : tnode = (TestExtNode *) linitial(cscan->custom_private);
179 : :
180 : : Assert(IsA(tnode, ExtensibleNode));
181 : : Assert(strcmp(tnode->base.extnodename, TEST_EXT_NODE_NAME) == 0);
182 : :
183 : 22 : return tnode;
184 : : }
185 : :
186 : : /*
187 : : * BeginCustomScan.
188 : : */
189 : : static void
190 : 11 : test_begin_custom_scan(CustomScanState *node, EState *estate, int eflags)
191 : : {
192 : 11 : TestCustomScanState *tstate = (TestCustomScanState *) node;
193 : 11 : TestExtNode *tnode = test_get_ext_node((CustomScan *) node->ss.ps.plan);
194 : 11 : Relation rel = node->ss.ss_currentRelation;
195 : :
196 : : Assert(tnode->repeat_count > 0);
197 : 11 : tstate->repeat_count = tnode->repeat_count;
198 : 11 : tstate->repeats_left = 0;
199 : 11 : tstate->scandesc = NULL;
200 : :
201 : : /* A plain EXPLAIN never executes the plan, so skip */
202 [ + + ]: 11 : if (eflags & EXEC_FLAG_EXPLAIN_ONLY)
203 : 5 : return;
204 : :
205 : 6 : tstate->scandesc = table_beginscan(rel, estate->es_snapshot, 0, NULL,
206 : : SO_NONE);
207 : : }
208 : :
209 : : /*
210 : : * Access method for ExecScan(): return the next tuple to be considered, or
211 : : * NULL when the scan is done.
212 : : */
213 : : static TupleTableSlot *
214 : 38 : test_scan_next(ScanState *node)
215 : : {
216 : 38 : TestCustomScanState *tstate = (TestCustomScanState *) node;
217 : 38 : TupleTableSlot *slot = node->ss_ScanTupleSlot;
218 : :
219 : : /* Return the current tuple again if it still has repeats left */
220 [ + + ]: 38 : if (tstate->repeats_left > 0)
221 : : {
222 : 18 : tstate->repeats_left--;
223 : 18 : return slot;
224 : : }
225 : :
226 [ + + ]: 20 : if (!table_scan_getnextslot(tstate->scandesc, ForwardScanDirection, slot))
227 : 5 : return NULL;
228 : :
229 : 15 : tstate->repeats_left = tstate->repeat_count - 1;
230 : 15 : return slot;
231 : : }
232 : :
233 : : /*
234 : : * Recheck method for ExecScan(), used only during EvalPlanQual rechecks.
235 : : * We evaluate no quals of our own, so there is nothing to recheck.
236 : : */
237 : : static bool
238 : 0 : test_scan_recheck(ScanState *node, TupleTableSlot *slot)
239 : : {
240 : 0 : return true;
241 : : }
242 : :
243 : : static TupleTableSlot *
244 : 36 : test_exec_custom_scan(CustomScanState *node)
245 : : {
246 : 36 : return ExecScan(&node->ss, test_scan_next, test_scan_recheck);
247 : : }
248 : :
249 : : static void
250 : 11 : test_end_custom_scan(CustomScanState *node)
251 : : {
252 : 11 : TestCustomScanState *tstate = (TestCustomScanState *) node;
253 : :
254 : : /* No scan started under EXEC_FLAG_EXPLAIN_ONLY */
255 [ + + ]: 11 : if (tstate->scandesc != NULL)
256 : 6 : table_endscan(tstate->scandesc);
257 : 11 : }
258 : :
259 : : /*
260 : : * ReScanCustomScan: reset our own state as well as ExecScan()'s.
261 : : */
262 : : static void
263 : 0 : test_rescan_custom_scan(CustomScanState *node)
264 : : {
265 : 0 : TestCustomScanState *tstate = (TestCustomScanState *) node;
266 : :
267 : 0 : tstate->repeats_left = 0;
268 : 0 : table_rescan(tstate->scandesc, NULL);
269 : 0 : ExecScanReScan(&node->ss);
270 : 0 : }
271 : :
272 : : static const CustomExecMethods test_custom_exec_methods =
273 : : {
274 : : .CustomName = TEST_CUSTOM_SCAN_NAME,
275 : : .BeginCustomScan = test_begin_custom_scan,
276 : : .ExecCustomScan = test_exec_custom_scan,
277 : : .EndCustomScan = test_end_custom_scan,
278 : : .ReScanCustomScan = test_rescan_custom_scan,
279 : : };
280 : :
281 : : /*
282 : : * CreateCustomScanState() allocates the CustomScanState and fills in its node
283 : : * tag and its methods; everything else is left to ExecInitCustomScan().
284 : : *
285 : : * slotOps is set here because ExecInitCustomScan() reads it before creating
286 : : * the scan tuple slot, and before it opens the scan relation itself. So we
287 : : * open the relation ourselves (its OID travels via our ExtensibleNode) just
288 : : * to learn its slot type from table_slot_callbacks().
289 : : *
290 : : * A lock is taken rather than passing NoLock, since in a parallel worker
291 : : * nothing has locked the relation yet; the worker takes its own lock later
292 : : * in ExecGetRangeTableRelation(). AccessShareLock matches what a plain scan
293 : : * uses.
294 : : */
295 : : static Node *
296 : 11 : test_create_custom_scan_state(CustomScan *cscan)
297 : : {
298 : : TestCustomScanState *tstate;
299 : 11 : TestExtNode *tnode = test_get_ext_node(cscan);
300 : : Relation rel;
301 : :
302 : : tstate = (TestCustomScanState *)
303 : 11 : newNode(sizeof(TestCustomScanState), T_CustomScanState);
304 : 11 : tstate->css.methods = &test_custom_exec_methods;
305 : :
306 : 11 : rel = table_open(tnode->relid, AccessShareLock);
307 : 11 : tstate->css.slotOps = table_slot_callbacks(rel);
308 : 11 : table_close(rel, NoLock);
309 : :
310 : 11 : return (Node *) tstate;
311 : : }
312 : :
313 : : static const CustomScanMethods test_custom_scan_methods =
314 : : {
315 : : .CustomName = TEST_CUSTOM_SCAN_NAME,
316 : : .CreateCustomScanState = test_create_custom_scan_state,
317 : : };
318 : :
319 : : /*
320 : : * Planner callbacks
321 : : */
322 : :
323 : : /*
324 : : * PlanCustomPath turns our CustomPath into the CustomScan plan node that the
325 : : * executor runs.
326 : : */
327 : : static Plan *
328 : 10 : test_plan_custom_path(PlannerInfo *root,
329 : : RelOptInfo *rel,
330 : : struct CustomPath *best_path,
331 : : List *tlist,
332 : : List *clauses,
333 : : List *custom_plans)
334 : : {
335 : 10 : CustomScan *cscan = makeNode(CustomScan);
336 : :
337 : 10 : cscan->scan.plan.targetlist = tlist;
338 : :
339 : : /*
340 : : * Restriction clauses arrive as RestrictInfos; reduce them to bare
341 : : * expressions for the plan's qual. Pseudoconstants are dropped since the
342 : : * core evaluates them in a gating Result node above us.
343 : : */
344 : 10 : cscan->scan.plan.qual = extract_actual_clauses(clauses, false);
345 : 10 : cscan->scan.scanrelid = rel->relid;
346 : 10 : cscan->flags = best_path->flags;
347 : :
348 : : /* Our CustomPath has no child paths */
349 : 10 : cscan->custom_plans = custom_plans;
350 : 10 : cscan->custom_exprs = NIL;
351 : :
352 : : /* Pass the ExtensibleNode from the path to the plan */
353 : 10 : cscan->custom_private = best_path->custom_private;
354 : 10 : cscan->custom_scan_tlist = NIL;
355 : 10 : cscan->custom_relids = NULL;
356 : 10 : cscan->methods = &test_custom_scan_methods;
357 : :
358 : 10 : return (Plan *) cscan;
359 : : }
360 : :
361 : : static const CustomPathMethods test_custom_path_methods =
362 : : {
363 : : .CustomName = TEST_CUSTOM_SCAN_NAME,
364 : : .PlanCustomPath = test_plan_custom_path,
365 : : };
366 : :
367 : : static set_rel_pathlist_hook_type prev_set_rel_pathlist_hook = NULL;
368 : :
369 : : static void
370 : 21 : test_set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
371 : : Index rti, RangeTblEntry *rte)
372 : : {
373 : : CustomPath *cpath;
374 : : TestExtNode *tnode;
375 : : char *relname;
376 : :
377 [ - + ]: 21 : if (prev_set_rel_pathlist_hook)
378 : 0 : prev_set_rel_pathlist_hook(root, rel, rti, rte);
379 : :
380 : : /*
381 : : * Only consider plain base relations with a table AM, no inheritance and
382 : : * no tablesample, keeping the logic simple.
383 : : */
384 [ + + + + ]: 21 : if (rel->reloptkind != RELOPT_BASEREL || rte->rtekind != RTE_RELATION)
385 : 7 : return;
386 [ - + - - ]: 14 : if (rte->relkind != RELKIND_RELATION && rte->relkind != RELKIND_MATVIEW)
387 : 0 : return;
388 [ + + + + ]: 14 : if (rte->tablesample != NULL || rte->inh)
389 : 4 : return;
390 : :
391 : : /*
392 : : * Only inject our CustomPath for the specific marker table, free of
393 : : * namespace.
394 : : */
395 : 10 : relname = get_rel_name(rte->relid);
396 [ + - - + ]: 10 : if (relname == NULL || strcmp(relname, TEST_TABLE_NAME) != 0)
397 : 0 : return;
398 : :
399 : : /* TestExtNode for the executor callbacks */
400 : 10 : tnode = (TestExtNode *) newNode(sizeof(TestExtNode), T_ExtensibleNode);
401 : 10 : tnode->base.extnodename = TEST_EXT_NODE_NAME;
402 : 10 : tnode->relid = rte->relid;
403 : :
404 : : /*
405 : : * Read once. Changes to the GUC do not affect already-planned queries.
406 : : * Each row is returned repeat_count times.
407 : : */
408 : 10 : tnode->repeat_count = test_repeat_count;
409 : :
410 : : /*
411 : : * Use a cost of zero to force our custom path; a real provider should
412 : : * estimate the cost honestly, but it does not matter for this module.
413 : : */
414 : 10 : cpath = makeNode(CustomPath);
415 : 10 : cpath->path.pathtype = T_CustomScan;
416 : 10 : cpath->path.parent = rel;
417 : 10 : cpath->path.pathtarget = rel->reltarget;
418 : 10 : cpath->path.rows = rel->rows * tnode->repeat_count;
419 : 10 : cpath->path.startup_cost = 0;
420 : 10 : cpath->path.total_cost = 0;
421 : :
422 : : /*
423 : : * Consider it as parallel safe, since our scan touches nothing but its
424 : : * own relation and keeps no state outside the CustomScanState.
425 : : */
426 : 10 : cpath->path.parallel_safe = rel->consider_parallel;
427 : :
428 : 10 : cpath->flags = 0;
429 : 10 : cpath->custom_paths = NIL;
430 : 10 : cpath->custom_private = list_make1(tnode);
431 : 10 : cpath->methods = &test_custom_path_methods;
432 : :
433 : 10 : add_path(rel, (Path *) cpath);
434 : : }
435 : :
436 : : /*
437 : : * test_get_extensible_node_methods
438 : : *
439 : : * Thin wrapper around GetExtensibleNodeMethods(). Returns the registered
440 : : * extnodename, or NULL when missing_ok = true and the name is not found.
441 : : */
442 : : Datum
443 : 3 : test_get_extensible_node_methods(PG_FUNCTION_ARGS)
444 : : {
445 : 3 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
446 : 3 : bool missing_ok = PG_GETARG_BOOL(1);
447 : : const ExtensibleNodeMethods *methods;
448 : :
449 : 3 : methods = GetExtensibleNodeMethods(name, missing_ok);
450 [ + + ]: 2 : if (methods == NULL)
451 : 1 : PG_RETURN_NULL();
452 : :
453 : 1 : PG_RETURN_TEXT_P(cstring_to_text(methods->extnodename));
454 : : }
455 : :
456 : : /*
457 : : * test_get_custom_scan_methods
458 : : *
459 : : * Thin wrapper around GetCustomScanMethods(). Returns the registered
460 : : * CustomName, or NULL when missing_ok = true and the name is not found.
461 : : */
462 : : Datum
463 : 3 : test_get_custom_scan_methods(PG_FUNCTION_ARGS)
464 : : {
465 : 3 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
466 : 3 : bool missing_ok = PG_GETARG_BOOL(1);
467 : : const CustomScanMethods *methods;
468 : :
469 : 3 : methods = GetCustomScanMethods(name, missing_ok);
470 [ + + ]: 2 : if (methods == NULL)
471 : 1 : PG_RETURN_NULL();
472 : :
473 : 1 : PG_RETURN_TEXT_P(cstring_to_text(methods->CustomName));
474 : : }
475 : :
476 : : /*
477 : : * Decode a TestExtNode via stringToNode(), rejecting a string describing
478 : : * some other kind of node instead of misinterpreting it as one of ours.
479 : : */
480 : : static TestExtNode *
481 : 14 : text_to_test_ext_node(text *txt)
482 : : {
483 : 14 : char *str = text_to_cstring(txt);
484 : : ReadNodeContext ctx;
485 : : TestExtNode *tnode;
486 : :
487 : 14 : ctx.str = str;
488 : :
489 [ + + + + ]: 27 : if (test_ext_node_next_token(&ctx, "{") == NULL ||
490 [ + - ]: 25 : test_ext_node_next_token(&ctx, "EXTENSIBLENODE") == NULL ||
491 [ + + ]: 24 : test_ext_node_next_token(&ctx, ":extnodename") == NULL ||
492 : 12 : test_ext_node_next_token(&ctx, TEST_EXT_NODE_NAME) == NULL)
493 [ + - ]: 3 : ereport(ERROR,
494 : : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
495 : : errmsg("argument is not a serialized \"%s\"",
496 : : TEST_EXT_NODE_NAME)));
497 : :
498 : 11 : tnode = (TestExtNode *) stringToNode(str);
499 : : Assert(IsA(tnode, ExtensibleNode) &&
500 : : strcmp(tnode->base.extnodename, TEST_EXT_NODE_NAME) == 0);
501 : :
502 : 10 : return tnode;
503 : : }
504 : :
505 : : /*
506 : : * test_ext_node_make
507 : : *
508 : : * Builds a TestExtNode and returns it.
509 : : */
510 : : Datum
511 : 7 : test_ext_node_make(PG_FUNCTION_ARGS)
512 : : {
513 : : TestExtNode *tnode;
514 : :
515 : 7 : tnode = (TestExtNode *) newNode(sizeof(TestExtNode), T_ExtensibleNode);
516 : 7 : tnode->base.extnodename = TEST_EXT_NODE_NAME;
517 : 7 : tnode->relid = PG_GETARG_OID(0);
518 : 7 : tnode->repeat_count = PG_GETARG_INT32(1);
519 : :
520 : 7 : PG_RETURN_TEXT_P(TEST_EXT_NODE_TO_TEXT(tnode));
521 : : }
522 : :
523 : : /*
524 : : * test_ext_node_copy
525 : : */
526 : : Datum
527 : 2 : test_ext_node_copy(PG_FUNCTION_ARGS)
528 : : {
529 : 2 : TestExtNode *tnode = PG_GETARG_TEST_EXT_NODE(0);
530 : :
531 : 2 : PG_RETURN_TEXT_P(TEST_EXT_NODE_TO_TEXT(copyObject(tnode)));
532 : : }
533 : :
534 : : /*
535 : : * test_ext_node_equal
536 : : */
537 : : Datum
538 : 3 : test_ext_node_equal(PG_FUNCTION_ARGS)
539 : : {
540 : 3 : TestExtNode *a = PG_GETARG_TEST_EXT_NODE(0);
541 : 3 : TestExtNode *b = PG_GETARG_TEST_EXT_NODE(1);
542 : :
543 : 3 : PG_RETURN_BOOL(equal(a, b));
544 : : }
545 : :
546 : : /*
547 : : * test_ext_node_get_relid
548 : : * test_ext_node_get_repeat_count
549 : : *
550 : : * Field accessors for the custom node contents.
551 : : */
552 : : Datum
553 : 5 : test_ext_node_get_relid(PG_FUNCTION_ARGS)
554 : : {
555 : 5 : PG_RETURN_OID(PG_GETARG_TEST_EXT_NODE(0)->relid);
556 : : }
557 : :
558 : : Datum
559 : 1 : test_ext_node_get_repeat_count(PG_FUNCTION_ARGS)
560 : : {
561 : 1 : PG_RETURN_INT32(PG_GETARG_TEST_EXT_NODE(0)->repeat_count);
562 : : }
563 : :
564 : : /*
565 : : * Module initialization
566 : : */
567 : : void
568 : 2 : _PG_init(void)
569 : : {
570 : 2 : RegisterCustomScanMethods(&test_custom_scan_methods);
571 : 2 : RegisterExtensibleNodeMethods(&test_ext_node_methods);
572 : :
573 : 2 : DefineCustomIntVariable("test_extensible.repeat_count",
574 : : "Number of times the custom scan returns each row.",
575 : : NULL,
576 : : &test_repeat_count,
577 : : 2,
578 : : 1,
579 : : 100,
580 : : PGC_USERSET,
581 : : 0,
582 : : NULL, NULL, NULL);
583 : :
584 : 2 : MarkGUCPrefixReserved("test_extensible");
585 : :
586 : : /* Install the path-list hook to inject CustomPaths for the test table */
587 : 2 : prev_set_rel_pathlist_hook = set_rel_pathlist_hook;
588 : 2 : set_rel_pathlist_hook = test_set_rel_pathlist;
589 : 2 : }
|