This is an automated email from the ASF dual-hosted git repository.

yjhjstz pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudberry.git


The following commit(s) were added to refs/heads/main by this push:
     new 78153b4d53a Feature: dispatch T_CustomScanState in CBDB parallel 
walkers (#1855)
78153b4d53a is described below

commit 78153b4d53a77c42cc9c6c858045688b6e735f7b
Author: roseduan <[email protected]>
AuthorDate: Sun Aug 9 17:34:13 2026 +0800

    Feature: dispatch T_CustomScanState in CBDB parallel walkers (#1855)
    
    Three GP-side walkers used during parallel setup —
    EstimateGpParallelDSMEntrySize, InitializeGpParallelWorkers,
    InitializeGpParallelDSMEntry — cased on other scan/join states but
    skipped T_CustomScanState.  A parallel_aware CustomScan therefore
    silently under-sized its DSM and failed to attach in workers, with
    no diagnostic.
    
    Add T_CustomScanState arms that dispatch to
    ExecCustomScan{Estimate,InitializeDSM,InitializeWorker}, gated on
    parallel_aware to match the other node types in these switches.
    
    Also handle T_CustomScanState in planstate_walk_kids. Upstream
    planstate_tree_walker walks only css->custom_ps for a
    CustomScanState; the CBDB walker previously fell through to
    default (lefttree/righttree), skipping custom_ps children under
    stateful walkers (cdbexplain_*, getMotionState, ...).  The new
    case walks custom_ps first, then lefttree/righttree if set, with
    an Assert that a CustomScanState never populates both — otherwise
    the child would be walked twice.
---
 src/backend/executor/execParallel.c                |  12 +
 src/backend/executor/execProcnode.c                |  22 ++
 src/test/modules/Makefile                          |   2 +-
 src/test/modules/parallel_customscan/Makefile      |  29 ++
 .../expected/parallel_customscan.out               | 212 +++++++++++
 .../parallel_customscan--1.0.sql                   |  15 +
 .../parallel_customscan/parallel_customscan.c      | 396 +++++++++++++++++++++
 .../parallel_customscan.control                    |   5 +
 .../sql/parallel_customscan.sql                    |  76 ++++
 9 files changed, 768 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/execParallel.c 
b/src/backend/executor/execParallel.c
index 16c4abe9b76..8d1a72cced7 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -1551,6 +1551,10 @@ EstimateGpParallelDSMEntrySize(PlanState *planstate, 
ParallelContext *pctx)
                case T_SortState:
                        ExecSortEstimate((SortState *) planstate, pctx);
                        break;
+               case T_CustomScanState:
+                       if (planstate->plan->parallel_aware)
+                               ExecCustomScanEstimate((CustomScanState *) 
planstate, pctx);
+                       break;
                default:
                        break;
 
@@ -1604,6 +1608,10 @@ InitializeGpParallelWorkers(PlanState *planstate, 
ParallelWorkerContext *pwcxt)
                        if (planstate->plan->parallel_aware)
                                ExecHashJoinInitializeWorker((HashJoinState *) 
planstate, pwcxt);
                        break;
+               case T_CustomScanState:
+                       if (planstate->plan->parallel_aware)
+                               ExecCustomScanInitializeWorker((CustomScanState 
*) planstate, pwcxt);
+                       break;
                default:
                        break;
        }
@@ -1662,6 +1670,10 @@ InitializeGpParallelDSMEntry(PlanState *planstate, 
ParallelContext *pctx)
                case T_SortState:
                        ExecSortInitializeDSM((SortState *) planstate, pctx);
                        break;
+               case T_CustomScanState:
+                       if (planstate->plan->parallel_aware)
+                               ExecCustomScanInitializeDSM((CustomScanState *) 
planstate, pctx);
+                       break;
                default:
                        break;
        }
diff --git a/src/backend/executor/execProcnode.c 
b/src/backend/executor/execProcnode.c
index 3f98f99f267..954ba935de1 100644
--- a/src/backend/executor/execProcnode.c
+++ b/src/backend/executor/execProcnode.c
@@ -1295,6 +1295,28 @@ planstate_walk_kids(PlanState *planstate,
                        Assert(!planstate->lefttree && !planstate->righttree);
                        break;
 
+               case T_CustomScanState:
+                       {
+                               CustomScanState *css = (CustomScanState *) 
planstate;
+                               ListCell   *lc;
+
+                               Assert(!(css->custom_ps != NIL &&
+                                                (planstate->lefttree || 
planstate->righttree)));
+
+                               v = CdbVisit_Walk;
+                               foreach(lc, css->custom_ps)
+                               {
+                                       v = 
planstate_walk_node_extended((PlanState *) lfirst(lc), walker, context, flags);
+                                       if (v != CdbVisit_Walk)
+                                               break;
+                               }
+                               if (v == CdbVisit_Walk && planstate->lefttree)
+                                       v = 
planstate_walk_node_extended(planstate->lefttree, walker, context, flags);
+                               if (v == CdbVisit_Walk && planstate->righttree)
+                                       v = 
planstate_walk_node_extended(planstate->righttree, walker, context, flags);
+                               break;
+                       }
+
                default:
                        /* Left subtree */
                        v = planstate_walk_node_extended(planstate->lefttree, 
walker, context, flags);
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 44f9ac5fc7d..77af107b55e 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -43,7 +43,7 @@ SUBDIRS = \
 #      special check for DML on system relations in GPDB
 
 # GPDB subdirs
-SUBDIRS += test_planner
+SUBDIRS += test_planner parallel_customscan
 ifeq ($(with_ssl),openssl)
 SUBDIRS += ssl_passphrase_callback
 else
diff --git a/src/test/modules/parallel_customscan/Makefile 
b/src/test/modules/parallel_customscan/Makefile
new file mode 100644
index 00000000000..3e4297d638a
--- /dev/null
+++ b/src/test/modules/parallel_customscan/Makefile
@@ -0,0 +1,29 @@
+# src/test/modules/parallel_customscan/Makefile
+
+MODULE_big = parallel_customscan
+OBJS = \
+       $(WIN32RES) \
+       parallel_customscan.o
+
+EXTENSION = parallel_customscan
+DATA = parallel_customscan--1.0.sql
+PGFILEDESC = "parallel_customscan - exercise parallel CustomScan dispatch"
+
+REGRESS = parallel_customscan
+
+# Run against an existing cluster (gpdemo).  The cluster must have
+# 'parallel_customscan' in shared_preload_libraries so segment backends
+# and parallel workers have the CustomScan methods registered:
+#   gpconfig -c shared_preload_libraries -v "'time_series,parallel_customscan'"
+#   gpstop -ra
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/parallel_customscan
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git 
a/src/test/modules/parallel_customscan/expected/parallel_customscan.out 
b/src/test/modules/parallel_customscan/expected/parallel_customscan.out
new file mode 100644
index 00000000000..867b5c60e9a
--- /dev/null
+++ b/src/test/modules/parallel_customscan/expected/parallel_customscan.out
@@ -0,0 +1,212 @@
+-- start_matchsubs
+-- m/\(actual rows=[^)]*\)/
+-- s/\(actual rows=[^)]*\)/(actual rows=...)/
+-- end_matchsubs
+-- start_matchignore
+-- m/^\s*Buckets: \d+  Batches: \d+  Memory Usage: \d+kB/
+-- end_matchignore
+CREATE EXTENSION parallel_customscan;
+-- Set up data BEFORE enabling our hook so ANALYZE doesn't traverse it.
+CREATE TABLE pcs_t (a int)
+    WITH (parallel_workers = 2)
+    DISTRIBUTED BY (a);
+INSERT INTO pcs_t SELECT generate_series(1, 1000);
+ANALYZE pcs_t;
+SET optimizer = off;
+SET enable_parallel = on;
+SET enable_seqscan = off;
+SET min_parallel_table_scan_size = 0;
+SET parallel_setup_cost = 0;
+SET parallel_tuple_cost = 0;
+SET max_parallel_workers_per_gather = 2;
+-- (1) Enabling the hook replaces the Parallel Seq Scan with a Custom Scan.
+EXPLAIN (COSTS OFF) SELECT count(*) FROM pcs_t;
+                   QUERY PLAN                   
+------------------------------------------------
+ Finalize Aggregate
+   ->  Gather Motion 6:1  (slice1; segments: 6)
+         ->  Partial Aggregate
+               ->  Parallel Seq Scan on pcs_t
+ Optimizer: Postgres query optimizer
+(5 rows)
+
+SET parallel_customscan.enabled = on;
+EXPLAIN (COSTS OFF) SELECT count(*) FROM pcs_t;
+                         QUERY PLAN                          
+-------------------------------------------------------------
+ Finalize Aggregate
+   ->  Gather Motion 6:1  (slice1; segments: 6)
+         ->  Partial Aggregate
+               ->  Parallel Custom Scan (ParallelCustomScan)
+                     ->  Parallel Seq Scan on pcs_t
+ Optimizer: Postgres query optimizer
+(6 rows)
+
+-- (2) Correctness: results match only if every parallel worker runs our scan.
+SELECT count(*) FROM pcs_t;
+ count 
+-------
+  1000
+(1 row)
+
+SELECT sum(a) FROM pcs_t;
+  sum   
+--------
+ 500500
+(1 row)
+
+-- (3) Scan-level qualifier and projection through the custom scan.
+EXPLAIN (COSTS OFF) SELECT a FROM pcs_t WHERE a > 990;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Gather Motion 6:1  (slice1; segments: 6)
+   ->  Parallel Custom Scan (ParallelCustomScan)
+         ->  Parallel Seq Scan on pcs_t
+               Filter: (a > 990)
+ Optimizer: Postgres query optimizer
+(5 rows)
+
+SELECT a FROM pcs_t WHERE a > 990 ORDER BY a;
+  a   
+------
+  991
+  992
+  993
+  994
+  995
+  996
+  997
+  998
+  999
+ 1000
+(10 rows)
+
+SELECT count(*) FROM pcs_t WHERE a % 2 = 0;
+ count 
+-------
+   500
+(1 row)
+
+-- (4) Join with both inputs scanned by the custom scan, plus a scan-level 
qual.
+CREATE TABLE pcs_t2 (a int)
+    WITH (parallel_workers = 2)
+    DISTRIBUTED BY (a);
+INSERT INTO pcs_t2 SELECT generate_series(1, 500);
+ANALYZE pcs_t2;
+EXPLAIN (COSTS OFF)
+    SELECT count(*) FROM pcs_t x JOIN pcs_t2 y ON x.a = y.a WHERE x.a <= 100;
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Finalize Aggregate
+   ->  Gather Motion 6:1  (slice1; segments: 6)
+         ->  Partial Aggregate
+               ->  Parallel Hash Join
+                     Hash Cond: (x.a = y.a)
+                     ->  Parallel Custom Scan (ParallelCustomScan)
+                           ->  Parallel Seq Scan on pcs_t x
+                                 Filter: (a <= 100)
+                     ->  Parallel Hash
+                           ->  Parallel Custom Scan (ParallelCustomScan)
+                                 ->  Parallel Seq Scan on pcs_t2 y
+                                       Filter: (a <= 100)
+ Optimizer: Postgres query optimizer
+(13 rows)
+
+SELECT count(*) FROM pcs_t x JOIN pcs_t2 y ON x.a = y.a WHERE x.a <= 100;
+ count 
+-------
+   100
+(1 row)
+
+-- (5) Empty relation: the custom scan must handle an immediate end-of-scan.
+CREATE TABLE pcs_empty (a int)
+    WITH (parallel_workers = 2)
+    DISTRIBUTED BY (a);
+ANALYZE pcs_empty;
+EXPLAIN (COSTS OFF) SELECT count(*) FROM pcs_empty;
+                   QUERY PLAN                   
+------------------------------------------------
+ Aggregate
+   ->  Gather Motion 3:1  (slice1; segments: 3)
+         ->  Custom Scan (ParallelCustomScan)
+               ->  Seq Scan on pcs_empty
+ Optimizer: Postgres query optimizer
+(5 rows)
+
+SELECT count(*) FROM pcs_empty;
+ count 
+-------
+     0
+(1 row)
+
+SELECT * FROM pcs_empty;
+ a 
+---
+(0 rows)
+
+-- (6) Serial path: with no workers, a non-parallel Custom Scan is used.
+SET max_parallel_workers_per_gather = 0;
+EXPLAIN (COSTS OFF) SELECT count(*) FROM pcs_t;
+                     QUERY PLAN                     
+----------------------------------------------------
+ Finalize Aggregate
+   ->  Gather Motion 3:1  (slice1; segments: 3)
+         ->  Partial Aggregate
+               ->  Custom Scan (ParallelCustomScan)
+                     ->  Seq Scan on pcs_t
+ Optimizer: Postgres query optimizer
+(6 rows)
+
+SELECT count(*) FROM pcs_t;
+ count 
+-------
+  1000
+(1 row)
+
+SELECT a FROM pcs_t WHERE a > 995 ORDER BY a;
+  a   
+------
+  996
+  997
+  998
+  999
+ 1000
+(5 rows)
+
+SET max_parallel_workers_per_gather = 2;
+-- (7) EXPLAIN ANALYZE: exercises planstate_walk_kids' custom_ps recursion.
+EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
+    SELECT count(*) FROM pcs_t;
+                                      QUERY PLAN                               
        
+---------------------------------------------------------------------------------------
+ Finalize Aggregate (actual rows=1 loops=1)
+   ->  Gather Motion 6:1  (slice1; segments: 6) (actual rows=6 loops=1)
+         ->  Partial Aggregate (actual rows=1 loops=1)
+               ->  Parallel Custom Scan (ParallelCustomScan) (actual rows=340 
loops=1)
+                     ->  Parallel Seq Scan on pcs_t (actual rows=340 loops=1)
+ Optimizer: Postgres query optimizer
+(6 rows)
+
+EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
+    SELECT count(*) FROM pcs_t x JOIN pcs_t2 y ON x.a = y.a;
+                                           QUERY PLAN                          
                  
+-------------------------------------------------------------------------------------------------
+ Finalize Aggregate (actual rows=1 loops=1)
+   ->  Gather Motion 6:1  (slice1; segments: 6) (actual rows=6 loops=1)
+         ->  Partial Aggregate (actual rows=1 loops=1)
+               ->  Parallel Hash Join (actual rows=0 loops=1)
+                     Hash Cond: (x.a = y.a)
+                     ->  Parallel Custom Scan (ParallelCustomScan) (actual 
rows=340 loops=1)
+                           ->  Parallel Seq Scan on pcs_t x (actual rows=340 
loops=1)
+                     ->  Parallel Hash (actual rows=0 loops=1)
+                           Buckets: 524288  Batches: 1  Memory Usage: 4128kB
+                           ->  Parallel Custom Scan (ParallelCustomScan) 
(actual rows=0 loops=1)
+                                 ->  Parallel Seq Scan on pcs_t2 y (actual 
rows=0 loops=1)
+ Optimizer: Postgres query optimizer
+(12 rows)
+
+-- cleanup
+DROP TABLE pcs_t2;
+DROP TABLE pcs_empty;
+DROP TABLE pcs_t;
+DROP EXTENSION parallel_customscan;
diff --git a/src/test/modules/parallel_customscan/parallel_customscan--1.0.sql 
b/src/test/modules/parallel_customscan/parallel_customscan--1.0.sql
new file mode 100644
index 00000000000..1304f8af9a0
--- /dev/null
+++ b/src/test/modules/parallel_customscan/parallel_customscan--1.0.sql
@@ -0,0 +1,15 @@
+/* src/test/modules/parallel_customscan/parallel_customscan--1.0.sql */
+
+-- Complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION parallel_customscan" to load this file. \quit
+
+CREATE FUNCTION pcs_get_hook_calls(
+    OUT estimate_calls    bigint,
+    OUT init_dsm_calls    bigint,
+    OUT reinit_dsm_calls  bigint,
+    OUT init_worker_calls bigint,
+    OUT shutdown_calls    bigint
+)
+RETURNS record
+AS 'MODULE_PATHNAME', 'pcs_get_hook_calls'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/parallel_customscan/parallel_customscan.c 
b/src/test/modules/parallel_customscan/parallel_customscan.c
new file mode 100644
index 00000000000..9f5ae66b8b8
--- /dev/null
+++ b/src/test/modules/parallel_customscan/parallel_customscan.c
@@ -0,0 +1,396 @@
+#include "postgres.h"
+
+#include "access/relscan.h"
+#include "access/tableam.h"
+#include "catalog/pg_type.h"
+#include "executor/executor.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "nodes/extensible.h"
+#include "nodes/makefuncs.h"
+#include "optimizer/optimizer.h"
+#include "cdb/cdbpathlocus.h"
+#include "optimizer/pathnode.h"
+#include "optimizer/paths.h"
+#include "optimizer/restrictinfo.h"
+#include "port/atomics.h"
+#include "storage/shm_toc.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/rel.h"
+
+PG_MODULE_MAGIC;
+
+void           _PG_init(void);
+void           _PG_fini(void);
+
+PG_FUNCTION_INFO_V1(pcs_get_hook_calls);
+
+/* GUC */
+static bool pcs_enabled = false;
+
+/*
+ * Per-process hook-call counters.  Reset in EstimateDSMCustomScan (which is
+ * called once per parallel-mode invocation in the leader).  The cross-worker
+ * InitializeWorkerCustomScan count is aggregated into the DSM atomic and
+ * harvested back into the leader-local counter in ShutdownCustomScan.
+ */
+static int64 pcs_n_estimate = 0;
+static int64 pcs_n_init_dsm = 0;
+static int64 pcs_n_reinit_dsm = 0;
+static int64 pcs_n_init_worker = 0;
+static int64 pcs_n_shutdown = 0;
+
+/*
+ * DSM header.  The child SeqScan owns the parallel table scan descriptor, so
+ * the wrapper only needs a tiny shared area to count InitializeWorker calls
+ * across workers.
+ */
+typedef struct PcsDSM
+{
+       pg_atomic_uint32 init_worker_calls;
+} PcsDSM;
+
+typedef struct PcsState
+{
+       CustomScanState csstate;
+       PcsDSM     *dsm;                                /* set in InitDSM / 
InitWorker */
+} PcsState;
+
+static set_rel_pathlist_hook_type prev_pathlist_hook = NULL;
+
+static Plan *pcs_plan_path(PlannerInfo *root, RelOptInfo *rel,
+                                                  CustomPath *best_path, List 
*tlist,
+                                                  List *clauses, List 
*custom_plans);
+static Node *pcs_create_state(CustomScan *cscan);
+static void pcs_begin(CustomScanState *node, EState *estate, int eflags);
+static TupleTableSlot *pcs_exec(CustomScanState *node);
+static void pcs_end(CustomScanState *node);
+static void pcs_rescan(CustomScanState *node);
+static Size pcs_estimate_dsm(CustomScanState *node, ParallelContext *pcxt);
+static void pcs_init_dsm(CustomScanState *node, ParallelContext *pcxt,
+                                                void *coord);
+static void pcs_reinit_dsm(CustomScanState *node, ParallelContext *pcxt,
+                                                  void *coord);
+static void pcs_init_worker(CustomScanState *node, shm_toc *toc, void *coord);
+static void pcs_shutdown(CustomScanState *node);
+
+static const CustomPathMethods pcs_path_methods =
+{
+       .CustomName = "ParallelCustomScan",
+       .PlanCustomPath = pcs_plan_path,
+};
+
+static const CustomScanMethods pcs_scan_methods =
+{
+       .CustomName = "ParallelCustomScan",
+       .CreateCustomScanState = pcs_create_state,
+};
+
+static const CustomExecMethods pcs_exec_methods =
+{
+       .CustomName = "ParallelCustomScan",
+       .BeginCustomScan = pcs_begin,
+       .ExecCustomScan = pcs_exec,
+       .EndCustomScan = pcs_end,
+       .ReScanCustomScan = pcs_rescan,
+       .EstimateDSMCustomScan = pcs_estimate_dsm,
+       .InitializeDSMCustomScan = pcs_init_dsm,
+       .ReInitializeDSMCustomScan = pcs_reinit_dsm,
+       .InitializeWorkerCustomScan = pcs_init_worker,
+       .ShutdownCustomScan = pcs_shutdown,
+};
+
+static Plan *
+pcs_plan_path(PlannerInfo *root, RelOptInfo *rel, CustomPath *best_path,
+                         List *tlist, List *clauses, List *custom_plans)
+{
+       CustomScan *cs = makeNode(CustomScan);
+
+       cs->scan.plan.targetlist = tlist;
+       /*
+        * The child SeqScan does the filtering (create_scan_plan attaches the
+        * base restrictions to it), so the wrapper itself carries no qual.
+        */
+       cs->scan.plan.qual = NIL;
+       cs->scan.plan.parallel_aware = best_path->path.parallel_aware;
+       cs->scan.plan.parallel_safe = best_path->path.parallel_safe;
+       /*
+        * The wrapper delegates scanning to its child, so it is not itself a
+        * base-relation scan: scanrelid = 0.  custom_scan_tlist must then
+        * describe the scan tuple we hand upward; we mirror the child's output
+        * targetlist, and set_customscan_references() rewrites our own
+        * targetlist to reference it via INDEX_VAR.  This keeps projection
+        * correct for multi-column relations -- e.g. the catalog scans
+        * (pg_class) that ANALYZE issues internally, which a fixed
+        * base-relation descriptor would mis-deform.
+        */
+       cs->scan.scanrelid = 0;
+       cs->flags = best_path->flags;
+       cs->custom_plans = custom_plans;
+       cs->custom_exprs = NIL;
+       cs->custom_private = NIL;
+       cs->custom_scan_tlist =
+               copyObject(((Plan *) linitial(custom_plans))->targetlist);
+       cs->methods = &pcs_scan_methods;
+
+       return (Plan *) cs;
+}
+
+static Node *
+pcs_create_state(CustomScan *cscan)
+{
+       PcsState   *st = (PcsState *) newNode(sizeof(PcsState), 
T_CustomScanState);
+
+       st->csstate.methods = &pcs_exec_methods;
+       return (Node *) st;
+}
+
+static void
+pcs_begin(CustomScanState *node, EState *estate, int eflags)
+{
+       CustomScan *cscan = (CustomScan *) node->ss.ps.plan;
+       Plan       *childplan = (Plan *) linitial(cscan->custom_plans);
+
+       /*
+        * ExecInitCustomScan has already opened the scan relation and set up 
our
+        * scan/result slots and projection.  All we add is the child plan 
state,
+        * which becomes our sole custom_ps entry.  That child performs the 
actual
+        * (parallel) heap scan and is the node the MPP planstate walkers 
recurse
+        * into via the T_CustomScanState arm.
+        */
+       node->custom_ps = list_make1(ExecInitNode(childplan, estate, eflags));
+}
+
+static TupleTableSlot *
+pcs_child_next(CustomScanState *node)
+{
+       PlanState  *child = (PlanState *) linitial(node->custom_ps);
+       TupleTableSlot *childslot = ExecProcNode(child);
+
+       if (TupIsNull(childslot))
+               return NULL;
+
+       /*
+        * Move the child's tuple into our scan slot.  That slot's descriptor 
was
+        * built from custom_scan_tlist, which is an exact copy of the child's
+        * output targetlist, so the columns line up positionally and ExecScan's
+        * projection (compiled against the same slot) reads correct values.
+        */
+       ExecCopySlot(node->ss.ss_ScanTupleSlot, childslot);
+       return node->ss.ss_ScanTupleSlot;
+}
+
+static bool
+pcs_recheck(CustomScanState *node, TupleTableSlot *slot)
+{
+       return true;
+}
+
+static TupleTableSlot *
+pcs_exec(CustomScanState *node)
+{
+       return ExecScan(&node->ss,
+                                       (ExecScanAccessMtd) pcs_child_next,
+                                       (ExecScanRecheckMtd) pcs_recheck);
+}
+
+static void
+pcs_end(CustomScanState *node)
+{
+       ExecEndNode((PlanState *) linitial(node->custom_ps));
+}
+
+static void
+pcs_rescan(CustomScanState *node)
+{
+       ExecReScan((PlanState *) linitial(node->custom_ps));
+}
+
+static Size
+pcs_estimate_dsm(CustomScanState *node, ParallelContext *pcxt)
+{
+       /* Reset per-process counters at the start of a parallel run. */
+       pcs_n_estimate++;
+       pcs_n_init_dsm = 0;
+       pcs_n_reinit_dsm = 0;
+       pcs_n_init_worker = 0;
+       pcs_n_shutdown = 0;
+
+       return sizeof(PcsDSM);
+}
+
+static void
+pcs_init_dsm(CustomScanState *node, ParallelContext *pcxt, void *coord)
+{
+       PcsState   *st = (PcsState *) node;
+       PcsDSM     *dsm = (PcsDSM *) coord;
+
+       pg_atomic_init_u32(&dsm->init_worker_calls, 0);
+       st->dsm = dsm;
+       pcs_n_init_dsm++;
+}
+
+static void
+pcs_reinit_dsm(CustomScanState *node, ParallelContext *pcxt, void *coord)
+{
+       PcsState   *st = (PcsState *) node;
+       PcsDSM     *dsm = (PcsDSM *) coord;
+
+       pg_atomic_write_u32(&dsm->init_worker_calls, 0);
+       st->dsm = dsm;
+       pcs_n_reinit_dsm++;
+}
+
+static void
+pcs_init_worker(CustomScanState *node, shm_toc *toc, void *coord)
+{
+       PcsState   *st = (PcsState *) node;
+       PcsDSM     *dsm = (PcsDSM *) coord;
+
+       st->dsm = dsm;
+
+       /* This runs in a worker; the leader reads the aggregate in Shutdown. */
+       pg_atomic_fetch_add_u32(&dsm->init_worker_calls, 1);
+}
+
+static void
+pcs_shutdown(CustomScanState *node)
+{
+       PcsState   *st = (PcsState *) node;
+
+       if (st->dsm != NULL)
+               pcs_n_init_worker = 
pg_atomic_read_u32(&st->dsm->init_worker_calls);
+       pcs_n_shutdown++;
+}
+
+static void
+pcs_set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti,
+                                        RangeTblEntry *rte)
+{
+       CustomPath *cp;
+       CustomPath *pp;
+       double          rows;
+       CdbPathLocus inherited_locus;
+
+       if (prev_pathlist_hook)
+               prev_pathlist_hook(root, rel, rti, rte);
+
+       if (!pcs_enabled)
+               return;
+       if (rte->rtekind != RTE_RELATION || rte->relkind != RELKIND_RELATION)
+               return;
+
+       /*
+        * In Cloudberry every Path must carry a CdbPathLocus.  Inherit it from
+        * the first existing pathlist entry (set up by set_plain_rel_pathlist)
+        * so the planner can dispatch our CustomScan the same way a regular
+        * SeqScan would be dispatched.
+        */
+       if (rel->pathlist == NIL)
+               return;
+       inherited_locus = ((Path *) linitial(rel->pathlist))->locus;
+
+       rows = rel->tuples > 0 ? rel->tuples : 1.0;
+
+       cp = makeNode(CustomPath);
+       cp->path.pathtype = T_CustomScan;
+       cp->path.parent = rel;
+       cp->path.pathtarget = rel->reltarget;
+       cp->path.param_info = NULL;
+       cp->path.parallel_aware = false;
+       cp->path.parallel_safe = true;
+       cp->path.parallel_workers = 0;
+       cp->path.pathkeys = NIL;
+       cp->path.rows = rows;
+       cp->path.startup_cost = 0;
+       cp->path.total_cost = rows * 0.001;     /* cheaper than seqscan to win 
*/
+       cp->path.locus = inherited_locus;
+       cp->flags = 0;
+       /*
+        * A freshly built SeqScan path is the wrapper's child.  It must be a 
new
+        * node (not one already in rel->pathlist): add_path() below may pfree a
+        * dominated seqscan path, which would leave a dangling child pointer.
+        */
+       cp->custom_paths = list_make1(create_seqscan_path(root, rel, NULL, 0));
+       cp->custom_private = NIL;
+       cp->methods = &pcs_path_methods;
+       add_path(rel, &cp->path, root);
+
+       if (rel->consider_parallel)
+       {
+               CdbPathLocus parallel_locus = cdbpathlocus_from_baserel(root, 
rel, 2);
+
+               pp = makeNode(CustomPath);
+               pp->path.pathtype = T_CustomScan;
+               pp->path.parent = rel;
+               pp->path.pathtarget = rel->reltarget;
+               pp->path.param_info = NULL;
+               pp->path.parallel_aware = true;
+               pp->path.parallel_safe = true;
+               pp->path.parallel_workers = parallel_locus.parallel_workers;
+               pp->path.pathkeys = NIL;
+               pp->path.rows = rows / Max(2, parallel_locus.parallel_workers);
+               pp->path.startup_cost = 0;
+               pp->path.total_cost = pp->path.rows * 0.0001;   /* much cheaper 
*/
+               pp->path.locus = parallel_locus;
+               pp->flags = 0;
+               /* Parallel-aware child so it splits blocks across workers. */
+               pp->custom_paths =
+                       list_make1(create_seqscan_path(root, rel, NULL,
+                                                                               
   parallel_locus.parallel_workers));
+               pp->custom_private = NIL;
+               pp->methods = &pcs_path_methods;
+               add_partial_path(rel, &pp->path);
+       }
+}
+
+Datum
+pcs_get_hook_calls(PG_FUNCTION_ARGS)
+{
+       TupleDesc       tupdesc;
+       HeapTuple       tuple;
+       Datum           values[5];
+       bool            nulls[5] = {false, false, false, false, false};
+
+       if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+               ereport(ERROR,
+                               (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                errmsg("function returning record called in 
context that cannot accept type record")));
+
+       tupdesc = BlessTupleDesc(tupdesc);
+
+       values[0] = Int64GetDatum(pcs_n_estimate);
+       values[1] = Int64GetDatum(pcs_n_init_dsm);
+       values[2] = Int64GetDatum(pcs_n_reinit_dsm);
+       values[3] = Int64GetDatum(pcs_n_init_worker);
+       values[4] = Int64GetDatum(pcs_n_shutdown);
+
+       tuple = heap_form_tuple(tupdesc, values, nulls);
+       PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
+}
+
+void
+_PG_init(void)
+{
+       DefineCustomBoolVariable("parallel_customscan.enabled",
+                                                        "Replace seqscan paths 
with TestParallelCustomScan.",
+                                                        NULL,
+                                                        &pcs_enabled,
+                                                        false,
+                                                        PGC_USERSET,
+                                                        0,
+                                                        NULL, NULL, NULL);
+
+       RegisterCustomScanMethods(&pcs_scan_methods);
+
+       prev_pathlist_hook = set_rel_pathlist_hook;
+       set_rel_pathlist_hook = pcs_set_rel_pathlist;
+}
+
+void
+_PG_fini(void)
+{
+       set_rel_pathlist_hook = prev_pathlist_hook;
+}
diff --git a/src/test/modules/parallel_customscan/parallel_customscan.control 
b/src/test/modules/parallel_customscan/parallel_customscan.control
new file mode 100644
index 00000000000..d9657db2036
--- /dev/null
+++ b/src/test/modules/parallel_customscan/parallel_customscan.control
@@ -0,0 +1,5 @@
+# parallel_customscan extension
+comment = 'Exercise parallel-aware CustomScan dispatch (Cloudberry MPP 
walkers)'
+default_version = '1.0'
+module_pathname = '$libdir/parallel_customscan'
+relocatable = true
diff --git a/src/test/modules/parallel_customscan/sql/parallel_customscan.sql 
b/src/test/modules/parallel_customscan/sql/parallel_customscan.sql
new file mode 100644
index 00000000000..08821ccb8e0
--- /dev/null
+++ b/src/test/modules/parallel_customscan/sql/parallel_customscan.sql
@@ -0,0 +1,76 @@
+-- start_matchsubs
+-- m/\(actual rows=[^)]*\)/
+-- s/\(actual rows=[^)]*\)/(actual rows=...)/
+-- end_matchsubs
+-- start_matchignore
+-- m/^\s*Buckets: \d+  Batches: \d+  Memory Usage: \d+kB/
+-- end_matchignore
+
+CREATE EXTENSION parallel_customscan;
+
+-- Set up data BEFORE enabling our hook so ANALYZE doesn't traverse it.
+CREATE TABLE pcs_t (a int)
+    WITH (parallel_workers = 2)
+    DISTRIBUTED BY (a);
+INSERT INTO pcs_t SELECT generate_series(1, 1000);
+ANALYZE pcs_t;
+
+SET optimizer = off;
+SET enable_parallel = on;
+SET enable_seqscan = off;
+SET min_parallel_table_scan_size = 0;
+SET parallel_setup_cost = 0;
+SET parallel_tuple_cost = 0;
+SET max_parallel_workers_per_gather = 2;
+
+-- (1) Enabling the hook replaces the Parallel Seq Scan with a Custom Scan.
+EXPLAIN (COSTS OFF) SELECT count(*) FROM pcs_t;
+SET parallel_customscan.enabled = on;
+EXPLAIN (COSTS OFF) SELECT count(*) FROM pcs_t;
+
+-- (2) Correctness: results match only if every parallel worker runs our scan.
+SELECT count(*) FROM pcs_t;
+SELECT sum(a) FROM pcs_t;
+
+-- (3) Scan-level qualifier and projection through the custom scan.
+EXPLAIN (COSTS OFF) SELECT a FROM pcs_t WHERE a > 990;
+SELECT a FROM pcs_t WHERE a > 990 ORDER BY a;
+SELECT count(*) FROM pcs_t WHERE a % 2 = 0;
+
+-- (4) Join with both inputs scanned by the custom scan, plus a scan-level 
qual.
+CREATE TABLE pcs_t2 (a int)
+    WITH (parallel_workers = 2)
+    DISTRIBUTED BY (a);
+INSERT INTO pcs_t2 SELECT generate_series(1, 500);
+ANALYZE pcs_t2;
+EXPLAIN (COSTS OFF)
+    SELECT count(*) FROM pcs_t x JOIN pcs_t2 y ON x.a = y.a WHERE x.a <= 100;
+SELECT count(*) FROM pcs_t x JOIN pcs_t2 y ON x.a = y.a WHERE x.a <= 100;
+
+-- (5) Empty relation: the custom scan must handle an immediate end-of-scan.
+CREATE TABLE pcs_empty (a int)
+    WITH (parallel_workers = 2)
+    DISTRIBUTED BY (a);
+ANALYZE pcs_empty;
+EXPLAIN (COSTS OFF) SELECT count(*) FROM pcs_empty;
+SELECT count(*) FROM pcs_empty;
+SELECT * FROM pcs_empty;
+
+-- (6) Serial path: with no workers, a non-parallel Custom Scan is used.
+SET max_parallel_workers_per_gather = 0;
+EXPLAIN (COSTS OFF) SELECT count(*) FROM pcs_t;
+SELECT count(*) FROM pcs_t;
+SELECT a FROM pcs_t WHERE a > 995 ORDER BY a;
+SET max_parallel_workers_per_gather = 2;
+
+-- (7) EXPLAIN ANALYZE: exercises planstate_walk_kids' custom_ps recursion.
+EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
+    SELECT count(*) FROM pcs_t;
+EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)
+    SELECT count(*) FROM pcs_t x JOIN pcs_t2 y ON x.a = y.a;
+
+-- cleanup
+DROP TABLE pcs_t2;
+DROP TABLE pcs_empty;
+DROP TABLE pcs_t;
+DROP EXTENSION parallel_customscan;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to