From 2cd3edc0e5339434601e4c768dc853790d344b0f Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 24 Oct 2025 15:11:47 -0400
Subject: [PATCH v7 4/5] Allow for plugin control over path generation
 strategies.

Each RelOptInfo now has a pgs_mask member which is a mask of acceptable
strategies. For most rels, this is populated from PlannerGlobal's
default_pgs_mask, which is computed from the values of the enable_*
GUCs at the start of planning.

For baserels, get_relation_info_hook can be used to adjust pgs_mask for
each new RelOptInfo, at least for rels of type RTE_RELATION. Adjusting
pgs_mask is less useful for other types of rels, but if it proves to
be necessary, we can revisit the way this hook works or add a new one.

For joinrels, two new hooks are added. joinrel_setup_hook is called each
time a joinrel is created, and one thing that can be done from that hook
is to manipulate pgs_mask for the new joinrel. join_path_setup_hook is
called each time we're about to add paths to a joinrel by considering
some particular combination of an outer rel, an inner rel, and a join
type. It can modify the pgs_mask propagated into JoinPathExtraData to
restrict strategy choice for that paricular combination of rels.

To make joinrel_setup_hook work as intended, the existing calls to
build_joinrel_partition_info are moved later in the calling functions;
this is because that function checks whether the rel's pgs_mask includes
PGS_CONSIDER_PARTITIONWISE, so we want it to only be called after
plugins have had a chance to alter pgs_mask.

Upper rels currently inherit pgs_mask from the input relation. It's
unclear that this is the most useful behavior, but at the moment there
are no hooks to allow the mask to be set in any other way.
---
 src/backend/optimizer/path/allpaths.c   |   2 +-
 src/backend/optimizer/path/costsize.c   | 222 ++++++++++++++++++------
 src/backend/optimizer/path/indxpath.c   |   4 +-
 src/backend/optimizer/path/joinpath.c   |  89 +++++++---
 src/backend/optimizer/path/tidpath.c    |   7 +-
 src/backend/optimizer/plan/createplan.c |   4 +-
 src/backend/optimizer/plan/planner.c    |  54 ++++++
 src/backend/optimizer/util/pathnode.c   |  19 +-
 src/backend/optimizer/util/plancat.c    |   3 +
 src/backend/optimizer/util/relnode.c    |  43 ++++-
 src/include/nodes/pathnodes.h           |  82 ++++++++-
 src/include/optimizer/cost.h            |   4 +-
 src/include/optimizer/pathnode.h        |  11 +-
 src/include/optimizer/paths.h           |   9 +-
 14 files changed, 455 insertions(+), 98 deletions(-)

diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 928b8d84ad8..8e9dde3d195 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -954,7 +954,7 @@ set_tablesample_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *
 		 bms_membership(root->all_query_rels) != BMS_SINGLETON) &&
 		!(GetTsmRoutine(rte->tablesample->tsmhandler)->repeatable_across_scans))
 	{
-		path = (Path *) create_material_path(rel, path);
+		path = (Path *) create_material_path(rel, path, true);
 	}
 
 	add_path(rel, path);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index a39cc793b4d..51940aec820 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -275,6 +275,7 @@ cost_seqscan(Path *path, PlannerInfo *root,
 	double		spc_seq_page_cost;
 	QualCost	qpqual_cost;
 	Cost		cpu_per_tuple;
+	uint64		enable_mask = PGS_SEQSCAN;
 
 	/* Should only be applied to base relations */
 	Assert(baserel->relid > 0);
@@ -327,8 +328,11 @@ cost_seqscan(Path *path, PlannerInfo *root,
 		 */
 		path->rows = clamp_row_est(path->rows / parallel_divisor);
 	}
+	else
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
 
-	path->disabled_nodes = enable_seqscan ? 0 : 1;
+	path->disabled_nodes =
+		(baserel->pgs_mask & enable_mask) == enable_mask ? 0 : 1;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + cpu_run_cost + disk_run_cost;
 }
@@ -354,6 +358,7 @@ cost_samplescan(Path *path, PlannerInfo *root,
 				spc_page_cost;
 	QualCost	qpqual_cost;
 	Cost		cpu_per_tuple;
+	uint64		enable_mask = 0;
 
 	/* Should only be applied to base relations with tablesample clauses */
 	Assert(baserel->relid > 0);
@@ -401,7 +406,11 @@ cost_samplescan(Path *path, PlannerInfo *root,
 	startup_cost += path->pathtarget->cost.startup;
 	run_cost += path->pathtarget->cost.per_tuple * path->rows;
 
-	path->disabled_nodes = 0;
+	if (path->parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
+
+	path->disabled_nodes =
+		(baserel->pgs_mask & enable_mask) == enable_mask ? 0 : 1;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + run_cost;
 }
@@ -440,7 +449,8 @@ cost_gather(GatherPath *path, PlannerInfo *root,
 	startup_cost += parallel_setup_cost;
 	run_cost += parallel_tuple_cost * path->path.rows;
 
-	path->path.disabled_nodes = path->subpath->disabled_nodes;
+	path->path.disabled_nodes = path->subpath->disabled_nodes
+		+ ((rel->pgs_mask & PGS_GATHER) != 0 ? 0 : 1);
 	path->path.startup_cost = startup_cost;
 	path->path.total_cost = (startup_cost + run_cost);
 }
@@ -506,8 +516,8 @@ cost_gather_merge(GatherMergePath *path, PlannerInfo *root,
 	startup_cost += parallel_setup_cost;
 	run_cost += parallel_tuple_cost * path->path.rows * 1.05;
 
-	path->path.disabled_nodes = input_disabled_nodes
-		+ (enable_gathermerge ? 0 : 1);
+	path->path.disabled_nodes = path->subpath->disabled_nodes
+		+ ((rel->pgs_mask & PGS_GATHER_MERGE) != 0 ? 0 : 1);
 	path->path.startup_cost = startup_cost + input_startup_cost;
 	path->path.total_cost = (startup_cost + run_cost + input_total_cost);
 }
@@ -557,6 +567,7 @@ cost_index(IndexPath *path, PlannerInfo *root, double loop_count,
 	double		pages_fetched;
 	double		rand_heap_pages;
 	double		index_pages;
+	uint64		enable_mask;
 
 	/* Should only be applied to base relations */
 	Assert(IsA(baserel, RelOptInfo) &&
@@ -588,8 +599,11 @@ cost_index(IndexPath *path, PlannerInfo *root, double loop_count,
 											  path->indexclauses);
 	}
 
-	/* we don't need to check enable_indexonlyscan; indxpath.c does that */
-	path->path.disabled_nodes = enable_indexscan ? 0 : 1;
+	/* is this scan type disabled? */
+	enable_mask = (indexonly ? PGS_INDEXONLYSCAN : PGS_INDEXSCAN)
+		| (path->path.parallel_workers == 0 ? PGS_CONSIDER_NONPARTIAL : 0);
+	path->path.disabled_nodes =
+		(baserel->pgs_mask & enable_mask) == enable_mask ? 0 : 1;
 
 	/*
 	 * Call index-access-method-specific code to estimate the processing cost
@@ -1010,6 +1024,7 @@ cost_bitmap_heap_scan(Path *path, PlannerInfo *root, RelOptInfo *baserel,
 	double		spc_seq_page_cost,
 				spc_random_page_cost;
 	double		T;
+	uint64		enable_mask = PGS_BITMAPSCAN;
 
 	/* Should only be applied to base relations */
 	Assert(IsA(baserel, RelOptInfo));
@@ -1075,6 +1090,8 @@ cost_bitmap_heap_scan(Path *path, PlannerInfo *root, RelOptInfo *baserel,
 
 		path->rows = clamp_row_est(path->rows / parallel_divisor);
 	}
+	else
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
 
 
 	run_cost += cpu_run_cost;
@@ -1083,7 +1100,8 @@ cost_bitmap_heap_scan(Path *path, PlannerInfo *root, RelOptInfo *baserel,
 	startup_cost += path->pathtarget->cost.startup;
 	run_cost += path->pathtarget->cost.per_tuple * path->rows;
 
-	path->disabled_nodes = enable_bitmapscan ? 0 : 1;
+	path->disabled_nodes =
+		(baserel->pgs_mask & enable_mask) == enable_mask ? 0 : 1;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + run_cost;
 }
@@ -1240,6 +1258,7 @@ cost_tidscan(Path *path, PlannerInfo *root,
 	double		ntuples;
 	ListCell   *l;
 	double		spc_random_page_cost;
+	uint64		enable_mask = 0;
 
 	/* Should only be applied to base relations */
 	Assert(baserel->relid > 0);
@@ -1261,10 +1280,10 @@ cost_tidscan(Path *path, PlannerInfo *root,
 
 		/*
 		 * We must use a TID scan for CurrentOfExpr; in any other case, we
-		 * should be generating a TID scan only if enable_tidscan=true. Also,
-		 * if CurrentOfExpr is the qual, there should be only one.
+		 * should be generating a TID scan only if TID scans are allowed.
+		 * Also, if CurrentOfExpr is the qual, there should be only one.
 		 */
-		Assert(enable_tidscan || IsA(qual, CurrentOfExpr));
+		Assert((baserel->pgs_mask & PGS_TIDSCAN) != 0 || IsA(qual, CurrentOfExpr));
 		Assert(list_length(tidquals) == 1 || !IsA(qual, CurrentOfExpr));
 
 		if (IsA(qual, ScalarArrayOpExpr))
@@ -1316,10 +1335,14 @@ cost_tidscan(Path *path, PlannerInfo *root,
 
 	/*
 	 * There are assertions above verifying that we only reach this function
-	 * either when enable_tidscan=true or when the TID scan is the only legal
-	 * path, so it's safe to set disabled_nodes to zero here.
+	 * either when baserel->pgs_mask includes PGS_TIDSCAN or when the TID scan
+	 * is the only legal path, so we only need to consider the effects of
+	 * PGS_CONSIDER_NONPARTIAL here.
 	 */
-	path->disabled_nodes = 0;
+	if (path->parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
+	path->disabled_nodes =
+		(baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + run_cost;
 }
@@ -1350,6 +1373,7 @@ cost_tidrangescan(Path *path, PlannerInfo *root,
 	double		nseqpages;
 	double		spc_random_page_cost;
 	double		spc_seq_page_cost;
+	uint64		enable_mask = PGS_TIDSCAN;
 
 	/* Should only be applied to base relations */
 	Assert(baserel->relid > 0);
@@ -1428,8 +1452,15 @@ cost_tidrangescan(Path *path, PlannerInfo *root,
 		path->rows = clamp_row_est(path->rows / parallel_divisor);
 	}
 
-	/* we should not generate this path type when enable_tidscan=false */
-	Assert(enable_tidscan);
+	/*
+	 * We should not generate this path type when PGS_TIDSCAN is unset, but we
+	 * might need to disable this path due to PGS_CONSIDER_NONPARTIAL.
+	 */
+	Assert((baserel->pgs_mask & PGS_TIDSCAN) != 0);
+	if (path->parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
+	path->disabled_nodes =
+		(baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0;
 	path->disabled_nodes = 0;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + cpu_run_cost + disk_run_cost;
@@ -1453,6 +1484,7 @@ cost_subqueryscan(SubqueryScanPath *path, PlannerInfo *root,
 	List	   *qpquals;
 	QualCost	qpqual_cost;
 	Cost		cpu_per_tuple;
+	uint64		enable_mask = 0;
 
 	/* Should only be applied to base relations that are subqueries */
 	Assert(baserel->relid > 0);
@@ -1483,7 +1515,10 @@ cost_subqueryscan(SubqueryScanPath *path, PlannerInfo *root,
 	 * SubqueryScan node, plus cpu_tuple_cost to account for selection and
 	 * projection overhead.
 	 */
-	path->path.disabled_nodes = path->subpath->disabled_nodes;
+	if (path->path.parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
+	path->path.disabled_nodes = path->subpath->disabled_nodes
+		+ (((baserel->pgs_mask & enable_mask) != enable_mask) ? 1 : 0);
 	path->path.startup_cost = path->subpath->startup_cost;
 	path->path.total_cost = path->subpath->total_cost;
 
@@ -1534,6 +1569,7 @@ cost_functionscan(Path *path, PlannerInfo *root,
 	Cost		cpu_per_tuple;
 	RangeTblEntry *rte;
 	QualCost	exprcost;
+	uint64		enable_mask = 0;
 
 	/* Should only be applied to base relations that are functions */
 	Assert(baserel->relid > 0);
@@ -1574,7 +1610,10 @@ cost_functionscan(Path *path, PlannerInfo *root,
 	startup_cost += path->pathtarget->cost.startup;
 	run_cost += path->pathtarget->cost.per_tuple * path->rows;
 
-	path->disabled_nodes = 0;
+	if (path->parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
+	path->disabled_nodes =
+		(baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + run_cost;
 }
@@ -1596,6 +1635,7 @@ cost_tablefuncscan(Path *path, PlannerInfo *root,
 	Cost		cpu_per_tuple;
 	RangeTblEntry *rte;
 	QualCost	exprcost;
+	uint64		enable_mask = 0;
 
 	/* Should only be applied to base relations that are functions */
 	Assert(baserel->relid > 0);
@@ -1631,7 +1671,10 @@ cost_tablefuncscan(Path *path, PlannerInfo *root,
 	startup_cost += path->pathtarget->cost.startup;
 	run_cost += path->pathtarget->cost.per_tuple * path->rows;
 
-	path->disabled_nodes = 0;
+	if (path->parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
+	path->disabled_nodes =
+		(baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + run_cost;
 }
@@ -1651,6 +1694,7 @@ cost_valuesscan(Path *path, PlannerInfo *root,
 	Cost		run_cost = 0;
 	QualCost	qpqual_cost;
 	Cost		cpu_per_tuple;
+	uint64		enable_mask = 0;
 
 	/* Should only be applied to base relations that are values lists */
 	Assert(baserel->relid > 0);
@@ -1679,7 +1723,10 @@ cost_valuesscan(Path *path, PlannerInfo *root,
 	startup_cost += path->pathtarget->cost.startup;
 	run_cost += path->pathtarget->cost.per_tuple * path->rows;
 
-	path->disabled_nodes = 0;
+	if (path->parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
+	path->disabled_nodes =
+		(baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + run_cost;
 }
@@ -1702,6 +1749,7 @@ cost_ctescan(Path *path, PlannerInfo *root,
 	Cost		run_cost = 0;
 	QualCost	qpqual_cost;
 	Cost		cpu_per_tuple;
+	uint64		enable_mask = 0;
 
 	/* Should only be applied to base relations that are CTEs */
 	Assert(baserel->relid > 0);
@@ -1727,7 +1775,10 @@ cost_ctescan(Path *path, PlannerInfo *root,
 	startup_cost += path->pathtarget->cost.startup;
 	run_cost += path->pathtarget->cost.per_tuple * path->rows;
 
-	path->disabled_nodes = 0;
+	if (path->parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
+	path->disabled_nodes =
+		(baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + run_cost;
 }
@@ -1744,6 +1795,7 @@ cost_namedtuplestorescan(Path *path, PlannerInfo *root,
 	Cost		run_cost = 0;
 	QualCost	qpqual_cost;
 	Cost		cpu_per_tuple;
+	uint64		enable_mask = 0;
 
 	/* Should only be applied to base relations that are Tuplestores */
 	Assert(baserel->relid > 0);
@@ -1765,7 +1817,10 @@ cost_namedtuplestorescan(Path *path, PlannerInfo *root,
 	cpu_per_tuple += cpu_tuple_cost + qpqual_cost.per_tuple;
 	run_cost += cpu_per_tuple * baserel->tuples;
 
-	path->disabled_nodes = 0;
+	if (path->parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
+	path->disabled_nodes =
+		(baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + run_cost;
 }
@@ -1782,6 +1837,7 @@ cost_resultscan(Path *path, PlannerInfo *root,
 	Cost		run_cost = 0;
 	QualCost	qpqual_cost;
 	Cost		cpu_per_tuple;
+	uint64		enable_mask = 0;
 
 	/* Should only be applied to RTE_RESULT base relations */
 	Assert(baserel->relid > 0);
@@ -1800,7 +1856,10 @@ cost_resultscan(Path *path, PlannerInfo *root,
 	cpu_per_tuple = cpu_tuple_cost + qpqual_cost.per_tuple;
 	run_cost += cpu_per_tuple * baserel->tuples;
 
-	path->disabled_nodes = 0;
+	if (path->parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
+	path->disabled_nodes =
+		(baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + run_cost;
 }
@@ -1818,6 +1877,7 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm)
 	Cost		startup_cost;
 	Cost		total_cost;
 	double		total_rows;
+	uint64		enable_mask = 0;
 
 	/* We probably have decent estimates for the non-recursive term */
 	startup_cost = nrterm->startup_cost;
@@ -1840,7 +1900,10 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm)
 	 */
 	total_cost += cpu_tuple_cost * total_rows;
 
-	runion->disabled_nodes = nrterm->disabled_nodes + rterm->disabled_nodes;
+	if (runion->parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
+	runion->disabled_nodes =
+		(runion->parent->pgs_mask & enable_mask) != enable_mask ? 1 : 0;
 	runion->startup_cost = startup_cost;
 	runion->total_cost = total_cost;
 	runion->rows = total_rows;
@@ -2110,7 +2173,11 @@ cost_incremental_sort(Path *path,
 
 	path->rows = input_tuples;
 
-	/* should not generate these paths when enable_incremental_sort=false */
+	/*
+	 * We should not generate these paths when enable_incremental_sort=false.
+	 * We can ignore PGS_CONSIDER_NONPARTIAL here, because if it's relevant,
+	 * it will have already affected the input path.
+	 */
 	Assert(enable_incremental_sort);
 	path->disabled_nodes = input_disabled_nodes;
 
@@ -2148,6 +2215,10 @@ cost_sort(Path *path, PlannerInfo *root,
 
 	startup_cost += input_cost;
 
+	/*
+	 * We can ignore PGS_CONSIDER_NONPARTIAL here, because if it's relevant,
+	 * it will have already affected the input path.
+	 */
 	path->rows = tuples;
 	path->disabled_nodes = input_disabled_nodes + (enable_sort ? 0 : 1);
 	path->startup_cost = startup_cost;
@@ -2239,9 +2310,15 @@ append_nonpartial_cost(List *subpaths, int numpaths, int parallel_workers)
 void
 cost_append(AppendPath *apath, PlannerInfo *root)
 {
+	RelOptInfo *rel = apath->path.parent;
 	ListCell   *l;
+	uint64		enable_mask = PGS_APPEND;
+
+	if (apath->path.parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
 
-	apath->path.disabled_nodes = 0;
+	apath->path.disabled_nodes =
+		(rel->pgs_mask & enable_mask) == enable_mask ? 0 : 1;
 	apath->path.startup_cost = 0;
 	apath->path.total_cost = 0;
 	apath->path.rows = 0;
@@ -2451,11 +2528,16 @@ cost_merge_append(Path *path, PlannerInfo *root,
 				  Cost input_startup_cost, Cost input_total_cost,
 				  double tuples)
 {
+	RelOptInfo *rel = path->parent;
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
 	Cost		comparison_cost;
 	double		N;
 	double		logN;
+	uint64		enable_mask = PGS_MERGE_APPEND;
+
+	if (path->parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
 
 	/*
 	 * Avoid log(0)...
@@ -2478,7 +2560,9 @@ cost_merge_append(Path *path, PlannerInfo *root,
 	 */
 	run_cost += cpu_tuple_cost * APPEND_CPU_COST_MULTIPLIER * tuples;
 
-	path->disabled_nodes = input_disabled_nodes;
+	path->disabled_nodes =
+		(rel->pgs_mask & enable_mask) == enable_mask ? 0 : 1;
+	path->disabled_nodes += input_disabled_nodes;
 	path->startup_cost = startup_cost + input_startup_cost;
 	path->total_cost = startup_cost + run_cost + input_total_cost;
 }
@@ -2497,7 +2581,7 @@ cost_merge_append(Path *path, PlannerInfo *root,
  */
 void
 cost_material(Path *path,
-			  int input_disabled_nodes,
+			  bool enabled, int input_disabled_nodes,
 			  Cost input_startup_cost, Cost input_total_cost,
 			  double tuples, int width)
 {
@@ -2506,6 +2590,11 @@ cost_material(Path *path,
 	double		nbytes = relation_byte_size(tuples, width);
 	double		work_mem_bytes = work_mem * (Size) 1024;
 
+	if (path->parallel_workers == 0 &&
+		path->parent != NULL &&
+		(path->parent->pgs_mask & PGS_CONSIDER_NONPARTIAL) == 0)
+		enabled = false;
+
 	path->rows = tuples;
 
 	/*
@@ -2535,7 +2624,7 @@ cost_material(Path *path,
 		run_cost += seq_page_cost * npages;
 	}
 
-	path->disabled_nodes = input_disabled_nodes + (enable_material ? 0 : 1);
+	path->disabled_nodes = input_disabled_nodes + (enabled ? 0 : 1);
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + run_cost;
 }
@@ -3287,7 +3376,7 @@ cost_group(Path *path, PlannerInfo *root,
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, uint64 enable_mask,
 					  Path *outer_path, Path *inner_path,
 					  JoinPathExtraData *extra)
 {
@@ -3301,7 +3390,7 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	Cost		inner_rescan_run_cost;
 
 	/* Count up disabled nodes. */
-	disabled_nodes = enable_nestloop ? 0 : 1;
+	disabled_nodes = (extra->pgs_mask & enable_mask) == enable_mask ? 0 : 1;
 	disabled_nodes += inner_path->disabled_nodes;
 	disabled_nodes += outer_path->disabled_nodes;
 
@@ -3701,7 +3790,19 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 	Assert(outerstartsel <= outerendsel);
 	Assert(innerstartsel <= innerendsel);
 
-	disabled_nodes = enable_mergejoin ? 0 : 1;
+	/*
+	 * We don't decide whether to materialize the inner path until we get to
+	 * final_cost_mergejoin(), so we don't know whether to check the pgs_mask
+	 * again PGS_MERGEJOIN_PLAIN or PGS_MERGEJOIN_MATERIALIZE. Instead, we
+	 * just account for any child nodes here and assume that this node is not
+	 * itslef disabled; we can sort out the details in final_cost_mergejoin().
+	 *
+	 * (We could be more precise here by setting disabled_nodes to 1 at this
+	 * stage if both PGS_MERGEJOIN_PLAIN and PGS_MERGEJOIN_MATERIALIZE are
+	 * disabled, but that seems to against the idea of making this function
+	 * produce a quick, optimistic approximation of the final cost.)
+	 */
+	disabled_nodes = 0;
 
 	/* cost of source data */
 
@@ -3880,9 +3981,7 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	double		mergejointuples,
 				rescannedtuples;
 	double		rescanratio;
-
-	/* Set the number of disabled nodes. */
-	path->jpath.path.disabled_nodes = workspace->disabled_nodes;
+	uint64		enable_mask = 0;
 
 	/* Protect some assumptions below that rowcounts aren't zero */
 	if (inner_path_rows <= 0)
@@ -4012,16 +4111,20 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 		path->materialize_inner = false;
 
 	/*
-	 * Prefer materializing if it looks cheaper, unless the user has asked to
-	 * suppress materialization.
+	 * If merge joins with materialization are enabled, then choose
+	 * materialization if either (a) it looks cheaper or (b) merge joins
+	 * without materialization are disabled.
 	 */
-	else if (enable_material && mat_inner_cost < bare_inner_cost)
+	else if ((extra->pgs_mask & PGS_MERGEJOIN_MATERIALIZE) != 0 &&
+			 (mat_inner_cost < bare_inner_cost ||
+			  (extra->pgs_mask & PGS_MERGEJOIN_PLAIN) == 0))
 		path->materialize_inner = true;
 
 	/*
-	 * Even if materializing doesn't look cheaper, we *must* do it if the
-	 * inner path is to be used directly (without sorting) and it doesn't
-	 * support mark/restore.
+	 * Regardless of what plan shapes are enabled and what the costs seem to
+	 * be, we *must* materialize it if the inner path is to be used directly
+	 * (without sorting) and it doesn't support mark/restore. Planner failure
+	 * is not an option!
 	 *
 	 * Since the inner side must be ordered, and only Sorts and IndexScans can
 	 * create order to begin with, and they both support mark/restore, you
@@ -4029,10 +4132,6 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * merge joins can *preserve* the order of their inputs, so they can be
 	 * selected as the input of a mergejoin, and they don't support
 	 * mark/restore at present.
-	 *
-	 * We don't test the value of enable_material here, because
-	 * materialization is required for correctness in this case, and turning
-	 * it off does not entitle us to deliver an invalid plan.
 	 */
 	else if (innersortkeys == NIL &&
 			 !ExecSupportsMarkRestore(inner_path))
@@ -4046,10 +4145,11 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * though.
 	 *
 	 * Since materialization is a performance optimization in this case,
-	 * rather than necessary for correctness, we skip it if enable_material is
-	 * off.
+	 * rather than necessary for correctness, we skip it if materialization is
+	 * switched off.
 	 */
-	else if (enable_material && innersortkeys != NIL &&
+	else if ((extra->pgs_mask & PGS_MERGEJOIN_MATERIALIZE) != 0 &&
+			 innersortkeys != NIL &&
 			 relation_byte_size(inner_path_rows,
 								inner_path->pathtarget->width) >
 			 work_mem * (Size) 1024)
@@ -4057,11 +4157,29 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	else
 		path->materialize_inner = false;
 
-	/* Charge the right incremental cost for the chosen case */
+	/* Get the number of disabled nodes, not yet including this one. */
+	path->jpath.path.disabled_nodes = workspace->disabled_nodes;
+
+	/*
+	 * Charge the right incremental cost for the chosen case, and update
+	 * enable_mask as appropriate.
+	 */
 	if (path->materialize_inner)
+	{
 		run_cost += mat_inner_cost;
+		enable_mask |= PGS_MERGEJOIN_MATERIALIZE;
+	}
 	else
+	{
 		run_cost += bare_inner_cost;
+		enable_mask |= PGS_MERGEJOIN_PLAIN;
+	}
+
+	/* Incremental count of disabled nodes if this node is disabled. */
+	if (path->jpath.path.parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
+	if ((extra->pgs_mask & enable_mask) != enable_mask)
+		++path->jpath.path.disabled_nodes;
 
 	/* CPU costs */
 
@@ -4199,9 +4317,13 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 	int			numbatches;
 	int			num_skew_mcvs;
 	size_t		space_allowed;	/* unused */
+	uint64		enable_mask = PGS_HASHJOIN;
+
+	if (outer_path->parallel_workers == 0)
+		enable_mask |= PGS_CONSIDER_NONPARTIAL;
 
 	/* Count up disabled nodes. */
-	disabled_nodes = enable_hashjoin ? 0 : 1;
+	disabled_nodes = (extra->pgs_mask & enable_mask) == enable_mask ? 0 : 1;
 	disabled_nodes += inner_path->disabled_nodes;
 	disabled_nodes += outer_path->disabled_nodes;
 
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index 5d4f81ee77e..8922b68033a 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -2232,8 +2232,8 @@ check_index_only(RelOptInfo *rel, IndexOptInfo *index)
 	ListCell   *lc;
 	int			i;
 
-	/* Index-only scans must be enabled */
-	if (!enable_indexonlyscan)
+	/* If we're not allowed to consider index-only scans, give up now */
+	if ((rel->pgs_mask & PGS_CONSIDER_INDEXONLY) == 0)
 		return false;
 
 	/*
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index ea5b6415186..82dab3d6004 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -29,8 +29,9 @@
 #include "utils/lsyscache.h"
 #include "utils/typcache.h"
 
-/* Hook for plugins to get control in add_paths_to_joinrel() */
+/* Hooks for plugins to get control in add_paths_to_joinrel() */
 set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
+join_path_setup_hook_type join_path_setup_hook = NULL;
 
 /*
  * Paths parameterized by a parent rel can be considered to be parameterized
@@ -151,6 +152,33 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.mergeclause_list = NIL;
 	extra.sjinfo = sjinfo;
 	extra.param_source_rels = NULL;
+	extra.pgs_mask = joinrel->pgs_mask;
+
+	/*
+	 * Give extensions a chance to take control. In particular, an extension
+	 * might want to modify extra.pgs_mask. It's possible to override pgs_mask
+	 * on a query-wide basis using join_search_hook, or for a particular
+	 * relation using joinrel_setup_hook, but extensions that want to provide
+	 * different advice for the same joinrel based on the choice of innerrel
+	 * and outerrel will need to use this hook.
+	 *
+	 * A very simple way for an extension to use this hook is to set
+	 * extra.pgs_mask &= ~PGS_JOIN_ANY, if it simply doesn't want any of the
+	 * paths generated by this call to add_paths_to_joinrel() to be selected.
+	 * An extension could use this technique to constrain the join order,
+	 * since it could thereby arrange to reject all paths from join orders
+	 * that it does not like. An extension can also selectively clear bits
+	 * from extra.pgs_mask to rule out specific techniques for specific joins,
+	 * or could even set additional bits to re-allow methods disabled at some
+	 * higher level.
+	 *
+	 * NB: Below this point, this function should be careful to reference
+	 * extra.pgs_mask rather than rel->pgs_mask to avoid disregarding any
+	 * changes made by the hook we're about to call.
+	 */
+	if (join_path_setup_hook)
+		join_path_setup_hook(root, joinrel, outerrel, innerrel,
+							 jointype, &extra);
 
 	/*
 	 * See if the inner relation is provably unique for this outer rel.
@@ -210,10 +238,10 @@ add_paths_to_joinrel(PlannerInfo *root,
 	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
-	 * way of implementing a full outer join, so override enable_mergejoin if
-	 * it's a full join.
+	 * way of implementing a full outer join, so in that case we don't care
+	 * whether mergejoins are disabled.
 	 */
-	if (enable_mergejoin || jointype == JOIN_FULL)
+	if ((extra.pgs_mask & PGS_MERGEJOIN_ANY) != 0 || jointype == JOIN_FULL)
 		extra.mergeclause_list = select_mergejoin_clauses(root,
 														  joinrel,
 														  outerrel,
@@ -321,10 +349,10 @@ add_paths_to_joinrel(PlannerInfo *root,
 
 	/*
 	 * 4. Consider paths where both outer and inner relations must be hashed
-	 * before being joined.  As above, disregard enable_hashjoin for full
-	 * joins, because there may be no other alternative.
+	 * before being joined.  As above, when it's a full join, we must try this
+	 * even when the path type is disabled, because it may be our only option.
 	 */
-	if (enable_hashjoin || jointype == JOIN_FULL)
+	if ((extra.pgs_mask & PGS_HASHJOIN) != 0 || jointype == JOIN_FULL)
 		hash_inner_and_outer(root, joinrel, outerrel, innerrel,
 							 jointype, &extra);
 
@@ -333,7 +361,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	 * to the same server and assigned to the same user to check access
 	 * permissions as, give the FDW a chance to push down joins.
 	 */
-	if (joinrel->fdwroutine &&
+	if ((extra.pgs_mask & PGS_FOREIGNJOIN) != 0 && joinrel->fdwroutine &&
 		joinrel->fdwroutine->GetForeignJoinPaths)
 		joinrel->fdwroutine->GetForeignJoinPaths(root, joinrel,
 												 outerrel, innerrel,
@@ -342,8 +370,13 @@ add_paths_to_joinrel(PlannerInfo *root,
 	/*
 	 * 6. Finally, give extensions a chance to manipulate the path list.  They
 	 * could add new paths (such as CustomPaths) by calling add_path(), or
-	 * add_partial_path() if parallel aware.  They could also delete or modify
-	 * paths added by the core code.
+	 * add_partial_path() if parallel aware.
+	 *
+	 * In theory, extensions could also use this hook to delete or modify
+	 * paths added by the core code, but in practice this is difficult to make
+	 * work, since it's too late to get back any paths that have already been
+	 * discarded by add_path() or add_partial_path(). If you're trying to
+	 * suppress paths, consider using join_path_setup_hook instead.
 	 */
 	if (set_join_pathlist_hook)
 		set_join_pathlist_hook(root, joinrel, outerrel, innerrel,
@@ -690,7 +723,7 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 	List	   *ph_lateral_vars;
 
 	/* Obviously not if it's disabled */
-	if (!enable_memoize)
+	if ((extra->pgs_mask & PGS_NESTLOOP_MEMOIZE) == 0)
 		return NULL;
 
 	/*
@@ -845,6 +878,7 @@ try_nestloop_path(PlannerInfo *root,
 				  Path *inner_path,
 				  List *pathkeys,
 				  JoinType jointype,
+				  uint64 nestloop_subtype,
 				  JoinPathExtraData *extra)
 {
 	Relids		required_outer;
@@ -927,6 +961,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * methodology worthwhile.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
+						  nestloop_subtype | PGS_CONSIDER_NONPARTIAL,
 						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel, workspace.disabled_nodes,
@@ -964,6 +999,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 						  Path *inner_path,
 						  List *pathkeys,
 						  JoinType jointype,
+						  uint64 nestloop_subtype,
 						  JoinPathExtraData *extra)
 {
 	JoinCostWorkspace workspace;
@@ -1011,7 +1047,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * Before creating a path, get a quick lower bound on what it is likely to
 	 * cost.  Bail out right away if it looks terrible.
 	 */
-	initial_cost_nestloop(root, &workspace, jointype,
+	initial_cost_nestloop(root, &workspace, jointype, nestloop_subtype,
 						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.disabled_nodes,
 								   workspace.total_cost, pathkeys))
@@ -1859,14 +1895,14 @@ match_unsorted_outer(PlannerInfo *root,
 	if (nestjoinOK)
 	{
 		/*
-		 * Consider materializing the cheapest inner path, unless
-		 * enable_material is off or the path in question materializes its
-		 * output anyway.
+		 * Consider materializing the cheapest inner path, unless that is
+		 * disabled or the path in question materializes its output anyway.
 		 */
-		if (enable_material && inner_cheapest_total != NULL &&
+		if ((extra->pgs_mask & PGS_NESTLOOP_MATERIALIZE) != 0 &&
+			inner_cheapest_total != NULL &&
 			!ExecMaterializesOutput(inner_cheapest_total->pathtype))
 			matpath = (Path *)
-				create_material_path(innerrel, inner_cheapest_total);
+				create_material_path(innerrel, inner_cheapest_total, true);
 	}
 
 	foreach(lc1, outerrel->pathlist)
@@ -1909,6 +1945,7 @@ match_unsorted_outer(PlannerInfo *root,
 								  innerpath,
 								  merge_pathkeys,
 								  jointype,
+								  PGS_NESTLOOP_PLAIN,
 								  extra);
 
 				/*
@@ -1925,6 +1962,7 @@ match_unsorted_outer(PlannerInfo *root,
 									  mpath,
 									  merge_pathkeys,
 									  jointype,
+									  PGS_NESTLOOP_MEMOIZE,
 									  extra);
 			}
 
@@ -1936,6 +1974,7 @@ match_unsorted_outer(PlannerInfo *root,
 								  matpath,
 								  merge_pathkeys,
 								  jointype,
+								  PGS_NESTLOOP_MATERIALIZE,
 								  extra);
 		}
 
@@ -2052,16 +2091,17 @@ consider_parallel_nestloop(PlannerInfo *root,
 
 	/*
 	 * Consider materializing the cheapest inner path, unless: 1)
-	 * enable_material is off, 2) the cheapest inner path is not
+	 * materialization is disabled here, 2) the cheapest inner path is not
 	 * parallel-safe, 3) the cheapest inner path is parameterized by the outer
 	 * rel, or 4) the cheapest inner path materializes its output anyway.
 	 */
-	if (enable_material && inner_cheapest_total->parallel_safe &&
+	if ((extra->pgs_mask & PGS_NESTLOOP_MATERIALIZE) != 0 &&
+		inner_cheapest_total->parallel_safe &&
 		!PATH_PARAM_BY_REL(inner_cheapest_total, outerrel) &&
 		!ExecMaterializesOutput(inner_cheapest_total->pathtype))
 	{
 		matpath = (Path *)
-			create_material_path(innerrel, inner_cheapest_total);
+			create_material_path(innerrel, inner_cheapest_total, true);
 		Assert(matpath->parallel_safe);
 	}
 
@@ -2091,7 +2131,8 @@ consider_parallel_nestloop(PlannerInfo *root,
 				continue;
 
 			try_partial_nestloop_path(root, joinrel, outerpath, innerpath,
-									  pathkeys, jointype, extra);
+									  pathkeys, jointype,
+									  PGS_NESTLOOP_PLAIN, extra);
 
 			/*
 			 * Try generating a memoize path and see if that makes the nested
@@ -2102,13 +2143,15 @@ consider_parallel_nestloop(PlannerInfo *root,
 									 extra);
 			if (mpath != NULL)
 				try_partial_nestloop_path(root, joinrel, outerpath, mpath,
-										  pathkeys, jointype, extra);
+										  pathkeys, jointype,
+										  PGS_NESTLOOP_MEMOIZE, extra);
 		}
 
 		/* Also consider materialized form of the cheapest inner path */
 		if (matpath != NULL)
 			try_partial_nestloop_path(root, joinrel, outerpath, matpath,
-									  pathkeys, jointype, extra);
+									  pathkeys, jointype,
+									  PGS_NESTLOOP_MATERIALIZE, extra);
 	}
 }
 
diff --git a/src/backend/optimizer/path/tidpath.c b/src/backend/optimizer/path/tidpath.c
index 3ddbc10bbdf..150115c293f 100644
--- a/src/backend/optimizer/path/tidpath.c
+++ b/src/backend/optimizer/path/tidpath.c
@@ -499,18 +499,19 @@ create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel)
 	List	   *tidquals;
 	List	   *tidrangequals;
 	bool		isCurrentOf;
+	bool		enabled = (rel->pgs_mask & PGS_TIDSCAN) != 0;
 
 	/*
 	 * If any suitable quals exist in the rel's baserestrict list, generate a
 	 * plain (unparameterized) TidPath with them.
 	 *
-	 * We skip this when enable_tidscan = false, except when the qual is
+	 * We skip this when TID scans are disabled, except when the qual is
 	 * CurrentOfExpr. In that case, a TID scan is the only correct path.
 	 */
 	tidquals = TidQualFromRestrictInfoList(root, rel->baserestrictinfo, rel,
 										   &isCurrentOf);
 
-	if (tidquals != NIL && (enable_tidscan || isCurrentOf))
+	if (tidquals != NIL && (enabled || isCurrentOf))
 	{
 		/*
 		 * This path uses no join clauses, but it could still have required
@@ -532,7 +533,7 @@ create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel)
 	}
 
 	/* Skip the rest if TID scans are disabled. */
-	if (!enable_tidscan)
+	if (!enabled)
 		return false;
 
 	/*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index e3f27a586ca..66d491ecb10 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -6503,7 +6503,7 @@ Plan *
 materialize_finished_plan(Plan *subplan)
 {
 	Plan	   *matplan;
-	Path		matpath;		/* dummy for result of cost_material */
+	Path		matpath;		/* dummy for cost_material */
 	Cost		initplan_cost;
 	bool		unsafe_initplans;
 
@@ -6525,7 +6525,9 @@ materialize_finished_plan(Plan *subplan)
 	subplan->total_cost -= initplan_cost;
 
 	/* Set cost data */
+	matpath.parent = NULL;
 	cost_material(&matpath,
+				  enable_material,
 				  subplan->disabled_nodes,
 				  subplan->startup_cost,
 				  subplan->total_cost,
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 94e1ac96ed9..36a29355104 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -462,6 +462,53 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 		tuple_fraction = 0.0;
 	}
 
+	/*
+	 * Compute the initial path generation strategy mask.
+	 *
+	 * Some strategies, such as PGS_FOREIGNJOIN, have no corresponding enable_*
+	 * GUC, and so the corresponding bits are always set in the default
+	 * strategy mask.
+	 *
+	 * It may seem surprising that enable_indexscan sets both PGS_INDEXSCAN
+	 * and PGS_INDEXONLYSCAN. However, the historical behavior of this GUC
+	 * corresponds to this exactly: enable_indexscan=off disables both
+	 * index-scan and index-only scan paths, whereas enable_indexonlyscan=off
+	 * converts the index-only scan paths that we would have considered into
+	 * index scan paths.
+	 */
+	glob->default_pgs_mask = PGS_APPEND | PGS_MERGE_APPEND | PGS_FOREIGNJOIN |
+		PGS_GATHER | PGS_CONSIDER_NONPARTIAL;
+	if (enable_tidscan)
+		glob->default_pgs_mask |= PGS_TIDSCAN;
+	if (enable_seqscan)
+		glob->default_pgs_mask |= PGS_SEQSCAN;
+	if (enable_indexscan)
+		glob->default_pgs_mask |= PGS_INDEXSCAN | PGS_INDEXONLYSCAN;
+	if (enable_indexonlyscan)
+		glob->default_pgs_mask |= PGS_CONSIDER_INDEXONLY;
+	if (enable_bitmapscan)
+		glob->default_pgs_mask |= PGS_BITMAPSCAN;
+	if (enable_mergejoin)
+	{
+		glob->default_pgs_mask |= PGS_MERGEJOIN_PLAIN;
+		if (enable_material)
+			glob->default_pgs_mask |= PGS_MERGEJOIN_MATERIALIZE;
+	}
+	if (enable_nestloop)
+	{
+		glob->default_pgs_mask |= PGS_NESTLOOP_PLAIN;
+		if (enable_material)
+			glob->default_pgs_mask |= PGS_NESTLOOP_MATERIALIZE;
+		if (enable_memoize)
+			glob->default_pgs_mask |= PGS_NESTLOOP_MEMOIZE;
+	}
+	if (enable_hashjoin)
+		glob->default_pgs_mask |= PGS_HASHJOIN;
+	if (enable_gathermerge)
+		glob->default_pgs_mask |= PGS_GATHER_MERGE;
+	if (enable_partitionwise_join)
+		glob->default_pgs_mask |= PGS_CONSIDER_PARTITIONWISE;
+
 	/* Allow plugins to take control after we've initialized "glob" */
 	if (planner_setup_hook)
 		(*planner_setup_hook) (glob, parse, query_string, &tuple_fraction, es);
@@ -3954,6 +4001,9 @@ make_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel,
 		is_parallel_safe(root, havingQual))
 		grouped_rel->consider_parallel = true;
 
+	/* Assume that the same path generation strategies are allowed */
+	grouped_rel->pgs_mask = input_rel->pgs_mask;
+
 	/*
 	 * If the input rel belongs to a single FDW, so does the grouped rel.
 	 */
@@ -5348,6 +5398,9 @@ create_ordered_paths(PlannerInfo *root,
 	if (input_rel->consider_parallel && target_parallel_safe)
 		ordered_rel->consider_parallel = true;
 
+	/* Assume that the same path generation strategies are allowed. */
+	ordered_rel->pgs_mask = input_rel->pgs_mask;
+
 	/*
 	 * If the input rel belongs to a single FDW, so does the ordered_rel.
 	 */
@@ -7428,6 +7481,7 @@ create_partial_grouping_paths(PlannerInfo *root,
 											grouped_rel->relids);
 	partially_grouped_rel->consider_parallel =
 		grouped_rel->consider_parallel;
+	partially_grouped_rel->pgs_mask = grouped_rel->pgs_mask;
 	partially_grouped_rel->reloptkind = grouped_rel->reloptkind;
 	partially_grouped_rel->serverid = grouped_rel->serverid;
 	partially_grouped_rel->userid = grouped_rel->userid;
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 33ce34f0088..7dd9a7c4609 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1659,7 +1659,7 @@ create_group_result_path(PlannerInfo *root, RelOptInfo *rel,
  *	  pathnode.
  */
 MaterialPath *
-create_material_path(RelOptInfo *rel, Path *subpath)
+create_material_path(RelOptInfo *rel, Path *subpath, bool enabled)
 {
 	MaterialPath *pathnode = makeNode(MaterialPath);
 
@@ -1678,6 +1678,7 @@ create_material_path(RelOptInfo *rel, Path *subpath)
 	pathnode->subpath = subpath;
 
 	cost_material(&pathnode->path,
+				  enabled,
 				  subpath->disabled_nodes,
 				  subpath->startup_cost,
 				  subpath->total_cost,
@@ -1730,8 +1731,15 @@ create_memoize_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
 	pathnode->est_unique_keys = 0.0;
 	pathnode->est_hit_ratio = 0.0;
 
-	/* we should not generate this path type when enable_memoize=false */
-	Assert(enable_memoize);
+	/*
+	 * We should not be asked to generate this path type when memoization is
+	 * disabled, so set our count of disabled nodes equal to the subpath's
+	 * count.
+	 *
+	 * It would be nice to also Assert that memoization is enabled, but the
+	 * value of enable_memoize is not controlling: what we would need to check
+	 * is that the JoinPathExtraData's pgs_mask included PGS_NESTLOOP_MEMOIZE.
+	 */
 	pathnode->path.disabled_nodes = subpath->disabled_nodes;
 
 	/*
@@ -3965,13 +3973,16 @@ reparameterize_path(PlannerInfo *root, Path *path,
 			{
 				MaterialPath *mpath = (MaterialPath *) path;
 				Path	   *spath = mpath->subpath;
+				bool		enabled;
 
 				spath = reparameterize_path(root, spath,
 											required_outer,
 											loop_count);
+				enabled =
+					(mpath->path.disabled_nodes <= spath->disabled_nodes);
 				if (spath == NULL)
 					return NULL;
-				return (Path *) create_material_path(rel, spath);
+				return (Path *) create_material_path(rel, spath, enabled);
 			}
 		case T_Memoize:
 			{
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index bf45c355b77..82a60c3ba6a 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -576,6 +576,9 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 	 * Allow a plugin to editorialize on the info we obtained from the
 	 * catalogs.  Actions might include altering the assumed relation size,
 	 * removing an index, or adding a hypothetical index to the indexlist.
+	 *
+	 * An extension can also modify rel->pgs_mask here to control path
+	 * generation.
 	 */
 	if (get_relation_info_hook)
 		(*get_relation_info_hook) (root, relationObjectId, inhparent, rel);
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 405f4dae109..1d2d7292fe6 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -47,6 +47,9 @@ typedef struct JoinHashEntry
 	RelOptInfo *join_rel;
 } JoinHashEntry;
 
+/* Hook for plugins to get control during joinrel setup */
+joinrel_setup_hook_type joinrel_setup_hook = NULL;
+
 static void build_joinrel_tlist(PlannerInfo *root, RelOptInfo *joinrel,
 								RelOptInfo *input_rel,
 								SpecialJoinInfo *sjinfo,
@@ -225,6 +228,7 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent)
 	rel->consider_startup = (root->tuple_fraction > 0);
 	rel->consider_param_startup = false;	/* might get changed later */
 	rel->consider_parallel = false; /* might get changed later */
+	rel->pgs_mask = root->glob->default_pgs_mask;
 	rel->reltarget = create_empty_pathtarget();
 	rel->pathlist = NIL;
 	rel->ppilist = NIL;
@@ -822,6 +826,7 @@ build_join_rel(PlannerInfo *root,
 	joinrel->consider_startup = (root->tuple_fraction > 0);
 	joinrel->consider_param_startup = false;
 	joinrel->consider_parallel = false;
+	joinrel->pgs_mask = root->glob->default_pgs_mask;
 	joinrel->reltarget = create_empty_pathtarget();
 	joinrel->pathlist = NIL;
 	joinrel->ppilist = NIL;
@@ -934,10 +939,6 @@ build_join_rel(PlannerInfo *root,
 	 */
 	joinrel->has_eclass_joins = has_relevant_eclass_joinclause(root, joinrel);
 
-	/* Store the partition information. */
-	build_joinrel_partition_info(root, joinrel, outer_rel, inner_rel, sjinfo,
-								 restrictlist);
-
 	/*
 	 * Set estimates of the joinrel's size.
 	 */
@@ -963,6 +964,18 @@ build_join_rel(PlannerInfo *root,
 		is_parallel_safe(root, (Node *) joinrel->reltarget->exprs))
 		joinrel->consider_parallel = true;
 
+	/*
+	 * Allow a plugin to editorialize on the new joinrel's properties. Actions
+	 * might include altering the size estimate or clearing consider_parallel.
+	 */
+	if (joinrel_setup_hook)
+		(*joinrel_setup_hook) (root, joinrel, outer_rel, inner_rel, sjinfo,
+							   restrictlist);
+
+	/* Store the partition information. */
+	build_joinrel_partition_info(root, joinrel, outer_rel, inner_rel, sjinfo,
+								 restrictlist);
+
 	/* Add the joinrel to the PlannerInfo. */
 	add_join_rel(root, joinrel);
 
@@ -1019,6 +1032,7 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
 	joinrel->consider_startup = (root->tuple_fraction > 0);
 	joinrel->consider_param_startup = false;
 	joinrel->consider_parallel = false;
+	joinrel->pgs_mask = root->glob->default_pgs_mask;
 	joinrel->reltarget = create_empty_pathtarget();
 	joinrel->pathlist = NIL;
 	joinrel->ppilist = NIL;
@@ -1102,10 +1116,6 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
 	 */
 	joinrel->has_eclass_joins = parent_joinrel->has_eclass_joins;
 
-	/* Is the join between partitions itself partitioned? */
-	build_joinrel_partition_info(root, joinrel, outer_rel, inner_rel, sjinfo,
-								 restrictlist);
-
 	/* Child joinrel is parallel safe if parent is parallel safe. */
 	joinrel->consider_parallel = parent_joinrel->consider_parallel;
 
@@ -1113,6 +1123,20 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
 	set_joinrel_size_estimates(root, joinrel, outer_rel, inner_rel,
 							   sjinfo, restrictlist);
 
+	/*
+	 * Allow a plugin to editorialize on the new joinrel's properties. Actions
+	 * might include altering the size estimate or clearing consider_parallel,
+	 * although the latter would be better done in the parent joinrel rather
+	 * than here.
+	 */
+	if (joinrel_setup_hook)
+		(*joinrel_setup_hook) (root, joinrel, outer_rel, inner_rel, sjinfo,
+							   restrictlist);
+
+	/* Is the join between partitions itself partitioned? */
+	build_joinrel_partition_info(root, joinrel, outer_rel, inner_rel, sjinfo,
+								 restrictlist);
+
 	/* We build the join only once. */
 	Assert(!find_join_rel(root, joinrel->relids));
 
@@ -1602,6 +1626,7 @@ fetch_upper_rel(PlannerInfo *root, UpperRelationKind kind, Relids relids)
 	upperrel = makeNode(RelOptInfo);
 	upperrel->reloptkind = RELOPT_UPPER_REL;
 	upperrel->relids = bms_copy(relids);
+	upperrel->pgs_mask = root->glob->default_pgs_mask;
 
 	/* cheap startup cost is interesting iff not all tuples to be retrieved */
 	upperrel->consider_startup = (root->tuple_fraction > 0);
@@ -2118,7 +2143,7 @@ build_joinrel_partition_info(PlannerInfo *root,
 	PartitionScheme part_scheme;
 
 	/* Nothing to do if partitionwise join technique is disabled. */
-	if (!enable_partitionwise_join)
+	if ((joinrel->pgs_mask & PGS_CONSIDER_PARTITIONWISE) == 0)
 	{
 		Assert(!IS_PARTITIONED_REL(joinrel));
 		return;
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index eacae16b827..7dbc979da12 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -22,6 +22,79 @@
 #include "nodes/parsenodes.h"
 #include "storage/block.h"
 
+/*
+ * Path generation strategies.
+ *
+ * These constants are used to specify the set of strategies that the planner
+ * should use, either for the query as a whole or for a specific baserel or
+ * joinrel. The various planner-related enable_* GUCs are used to set the
+ * PlannerGlobal's default_pgs_mask, and that in turn is used to set each
+ * RelOptInfo's pgs_mask. In both cases, extensions can use hooks to modify the
+ * default value.  Not every strategy listed here has a corresponding enable_*
+ * GUC; those that don't are always allowed unless disabled by an extension.
+ * Not all strategies are relevant for every RelOptInfo; e.g. PGS_SEQSCAN
+ * doesn't affect joinrels one way or the other.
+ *
+ * In most cases, disabling a path generation strategy merely means that any
+ * paths generated using that strategy are marked as disabled, but in some
+ * cases, path generation is skipped altogether. The latter strategy is only
+ * permissible when it can't result in planner failure -- for instance, we
+ * couldn't do this for sequential scans on a plain rel, because there might
+ * not be any other possible path. Nevertheless, the behaviors in each
+ * individual case are to some extent the result of historical accident,
+ * chosen to match the preexisting behaviors of the enable_* GUCs.
+ *
+ * In a few cases, we have more than one bit for the same strategy, controlling
+ * different aspects of the planner behavior. When PGS_CONSIDER_INDEXONLY is
+ * unset, we don't even consider index-only scans, and any such scans that
+ * would have been generated become index scans instead. On the other hand,
+ * unsetting PGS_INDEXSCAN or PGS_INDEXONLYSCAN causes generated paths of the
+ * corresponding types to be marked as disabled. Similarly, unsetting
+ * PGS_CONSIDER_PARTITIONWISE prevents any sort of thinking about partitionwise
+ * joins for the current rel, which incidentally will preclude higher-level
+ * joinrels from building parititonwise paths using paths taken from the
+ * current rel's children. On the other hand, unsetting PGS_APPEND or
+ * PGS_MERGE_APPEND will only arrange to disable paths of the corresponding
+ * types if they are generated at the level of the current rel.
+ *
+ * Finally, unsetting PGS_CONSIDER_NONPARTIAL disables all non-partial paths
+ * except those that use Gather or Gather Merge. In most other cases, a
+ * plugin can nudge the planner toward a particular strategy by disabling
+ * all of the others, but that doesn't work here: unsetting PGS_SEQSCAN,
+ * for instance, would disable both partial and non-partial sequential scans.
+ */
+#define PGS_SEQSCAN					0x00000001
+#define PGS_INDEXSCAN				0x00000002
+#define PGS_INDEXONLYSCAN			0x00000004
+#define PGS_BITMAPSCAN				0x00000008
+#define PGS_TIDSCAN					0x00000010
+#define PGS_FOREIGNJOIN				0x00000020
+#define PGS_MERGEJOIN_PLAIN			0x00000040
+#define PGS_MERGEJOIN_MATERIALIZE	0x00000080
+#define PGS_NESTLOOP_PLAIN			0x00000100
+#define PGS_NESTLOOP_MATERIALIZE	0x00000200
+#define PGS_NESTLOOP_MEMOIZE		0x00000400
+#define PGS_HASHJOIN				0x00000800
+#define PGS_APPEND					0x00001000
+#define PGS_MERGE_APPEND			0x00002000
+#define PGS_GATHER					0x00004000
+#define PGS_GATHER_MERGE			0x00008000
+#define PGS_CONSIDER_INDEXONLY		0x00010000
+#define PGS_CONSIDER_PARTITIONWISE	0x00020000
+#define PGS_CONSIDER_NONPARTIAL		0x00040000
+
+/*
+ * Convenience macros for useful combination of the bits defined above.
+ */
+#define PGS_SCAN_ANY		\
+	(PGS_SEQSCAN | PGS_INDEXSCAN | PGS_INDEXONLYSCAN | PGS_BITMAPSCAN | \
+	 PGS_TIDSCAN)
+#define PGS_MERGEJOIN_ANY	\
+	(PGS_MERGEJOIN_PLAIN | PGS_MERGEJOIN_MATERIALIZE)
+#define PGS_NESTLOOP_ANY	\
+	(PGS_NESTLOOP_PLAIN | PGS_NESTLOOP_MATERIALIZE | PGS_NESTLOOP_MEMOIZE)
+#define PGS_JOIN_ANY		\
+	(PGS_FOREIGNJOIN | PGS_MERGEJOIN_ANY | PGS_NESTLOOP_ANY | PGS_HASHJOIN)
 
 /*
  * Relids
@@ -186,6 +259,9 @@ typedef struct PlannerGlobal
 	/* worst PROPARALLEL hazard level */
 	char		maxParallelHazard;
 
+	/* mask of allowed path generation strategies */
+	uint64		default_pgs_mask;
+
 	/* partition descriptors */
 	PartitionDirectory partition_directory pg_node_attr(read_write_ignore);
 
@@ -939,7 +1015,7 @@ typedef struct RelOptInfo
 	Cardinality rows;
 
 	/*
-	 * per-relation planner control flags
+	 * per-relation planner control
 	 */
 	/* keep cheap-startup-cost paths? */
 	bool		consider_startup;
@@ -947,6 +1023,8 @@ typedef struct RelOptInfo
 	bool		consider_param_startup;
 	/* consider parallel paths? */
 	bool		consider_parallel;
+	/* path generation strategy mask */
+	uint64		pgs_mask;
 
 	/*
 	 * default result targetlist for Paths scanning this relation; list of
@@ -3506,6 +3584,7 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI/ANTI/inner_unique joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * pgs_mask is a bitmask of PGS_* constants to limit the join strategy
  */
 typedef struct JoinPathExtraData
 {
@@ -3515,6 +3594,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	uint64		pgs_mask;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index b523bcda8f3..2d80462bece 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -125,7 +125,7 @@ extern void cost_merge_append(Path *path, PlannerInfo *root,
 							  Cost input_startup_cost, Cost input_total_cost,
 							  double tuples);
 extern void cost_material(Path *path,
-						  int input_disabled_nodes,
+						  bool enabled, int input_disabled_nodes,
 						  Cost input_startup_cost, Cost input_total_cost,
 						  double tuples, int width);
 extern void cost_agg(Path *path, PlannerInfo *root,
@@ -148,7 +148,7 @@ extern void cost_group(Path *path, PlannerInfo *root,
 					   double input_tuples);
 extern void initial_cost_nestloop(PlannerInfo *root,
 								  JoinCostWorkspace *workspace,
-								  JoinType jointype,
+								  JoinType jointype, uint64 enable_mask,
 								  Path *outer_path, Path *inner_path,
 								  JoinPathExtraData *extra);
 extern void final_cost_nestloop(PlannerInfo *root, NestPath *path,
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index dbf4702acc9..123b78cbf11 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -17,6 +17,14 @@
 #include "nodes/bitmapset.h"
 #include "nodes/pathnodes.h"
 
+/* Hook for plugins to get control during joinrel setup */
+typedef void (*joinrel_setup_hook_type) (PlannerInfo *root,
+										 RelOptInfo *joinrel,
+										 RelOptInfo *outer_rel,
+										 RelOptInfo *inner_rel,
+										 SpecialJoinInfo *sjinfo,
+										 List *restrictlist);
+extern PGDLLIMPORT joinrel_setup_hook_type joinrel_setup_hook;
 
 /*
  * prototypes for pathnode.c
@@ -85,7 +93,8 @@ extern GroupResultPath *create_group_result_path(PlannerInfo *root,
 												 RelOptInfo *rel,
 												 PathTarget *target,
 												 List *havingqual);
-extern MaterialPath *create_material_path(RelOptInfo *rel, Path *subpath);
+extern MaterialPath *create_material_path(RelOptInfo *rel, Path *subpath,
+										  bool enabled);
 extern MemoizePath *create_memoize_path(PlannerInfo *root,
 										RelOptInfo *rel,
 										Path *subpath,
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index f6a62df0b43..61c1607f872 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -28,7 +28,14 @@ extern PGDLLIMPORT int min_parallel_table_scan_size;
 extern PGDLLIMPORT int min_parallel_index_scan_size;
 extern PGDLLIMPORT bool enable_group_by_reordering;
 
-/* Hook for plugins to get control in set_rel_pathlist() */
+/* Hooks for plugins to get control in set_rel_pathlist() */
+typedef void (*join_path_setup_hook_type) (PlannerInfo *root,
+										   RelOptInfo *joinrel,
+										   RelOptInfo *outerrel,
+										   RelOptInfo *innerrel,
+										   JoinType jointype,
+										   JoinPathExtraData *extra);
+extern PGDLLIMPORT join_path_setup_hook_type join_path_setup_hook;
 typedef void (*set_rel_pathlist_hook_type) (PlannerInfo *root,
 											RelOptInfo *rel,
 											Index rti,
-- 
2.51.0

