diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 70709e5..d663118 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -183,6 +183,11 @@ static char *ExecBuildSlotPartitionKeyDescription(Relation rel,
 									 bool *isnull,
 									 int maxfieldlen);
 static List *adjust_partition_tlist(List *tlist, TupleConversionMap *map);
+static void ExecInitPruningContext(PartitionPruneContext *context,
+					   List *pruning_steps,
+					   PartitionDesc partdesc,
+					   PartitionKey partkey,
+					   PlanState *planstate);
 static void find_matching_subplans_recurse(PartitionPruningData *prunedata,
 							   PartitionedRelPruningData *pprune,
 							   bool initial_prune,
@@ -1614,13 +1619,9 @@ ExecCreatePartitionPruneState(PlanState *planstate,
 		{
 			PartitionedRelPruneInfo *pinfo = lfirst_node(PartitionedRelPruneInfo, lc2);
 			PartitionedRelPruningData *pprune = &prunedata->partrelprunedata[j];
-			PartitionPruneContext *context = &pprune->context;
 			Relation	partrel;
 			PartitionDesc partdesc;
 			PartitionKey partkey;
-			int			partnatts;
-			int			n_steps;
-			ListCell   *lc3;
 
 			/* present_parts is also subject to later modification */
 			pprune->present_parts = bms_copy(pinfo->present_parts);
@@ -1700,66 +1701,36 @@ ExecCreatePartitionPruneState(PlanState *planstate,
 				Assert(pd_idx == pinfo->nparts);
 			}
 
-			n_steps = list_length(pinfo->pruning_steps);
-
-			context->strategy = partkey->strategy;
-			context->partnatts = partnatts = partkey->partnatts;
-			context->nparts = pinfo->nparts;
-			context->boundinfo = partdesc->boundinfo;
-			context->partcollation = partkey->partcollation;
-			context->partsupfunc = partkey->partsupfunc;
-
-			/* We'll look up type-specific support functions as needed */
-			context->stepcmpfuncs = (FmgrInfo *)
-				palloc0(sizeof(FmgrInfo) * n_steps * partnatts);
-
-			context->ppccontext = CurrentMemoryContext;
-			context->planstate = planstate;
-
-			/* Initialize expression state for each expression we need */
-			context->exprstates = (ExprState **)
-				palloc0(sizeof(ExprState *) * n_steps * partnatts);
-			foreach(lc3, pinfo->pruning_steps)
+			/*
+			 * Initialize pruning contexts as needed.
+			 */
+			pprune->initial_pruning_steps = pinfo->initial_pruning_steps;
+			if (pinfo->initial_pruning_steps)
 			{
-				PartitionPruneStepOp *step = (PartitionPruneStepOp *) lfirst(lc3);
-				ListCell   *lc4;
-				int			keyno;
-
-				/* not needed for other step kinds */
-				if (!IsA(step, PartitionPruneStepOp))
-					continue;
-
-				Assert(list_length(step->exprs) <= partnatts);
-
-				keyno = 0;
-				foreach(lc4, step->exprs)
-				{
-					Expr	   *expr = (Expr *) lfirst(lc4);
-
-					/* not needed for Consts */
-					if (!IsA(expr, Const))
-					{
-						int			stateidx = PruneCxtStateIdx(partnatts,
-																step->step.step_id,
-																keyno);
-
-						context->exprstates[stateidx] =
-							ExecInitExpr(expr, context->planstate);
-					}
-					keyno++;
-				}
+				ExecInitPruningContext(&pprune->initial_context,
+									   pinfo->initial_pruning_steps,
+									   partdesc, partkey, planstate);
+				/* Record whether initial pruning is needed at any level */
+				prunestate->do_initial_prune = true;
+			}
+			else
+			{
+				/*
+				 * Hack: ExecFindInitialMatchingSubPlans requires this field
+				 * to be valid whether pruning or not.
+				 */
+				pprune->initial_context.nparts = partdesc->nparts;
 			}
 
-			/* Array is not modified at runtime, so just point to plan's copy */
-			context->exprhasexecparam = pinfo->hasexecparam;
-
-			pprune->pruning_steps = pinfo->pruning_steps;
-			pprune->do_initial_prune = pinfo->do_initial_prune;
-			pprune->do_exec_prune = pinfo->do_exec_prune;
-
-			/* Record if pruning would be useful at any level */
-			prunestate->do_initial_prune |= pinfo->do_initial_prune;
-			prunestate->do_exec_prune |= pinfo->do_exec_prune;
+			pprune->exec_pruning_steps = pinfo->exec_pruning_steps;
+			if (pinfo->exec_pruning_steps)
+			{
+				ExecInitPruningContext(&pprune->exec_context,
+									   pinfo->exec_pruning_steps,
+									   partdesc, partkey, planstate);
+				/* Record whether exec pruning is needed at any level */
+				prunestate->do_exec_prune = true;
+			}
 
 			/*
 			 * Accumulate the IDs of all PARAM_EXEC Params affecting the
@@ -1777,6 +1748,71 @@ ExecCreatePartitionPruneState(PlanState *planstate,
 }
 
 /*
+ * Initialize a PartitionPruneContext for the given list of pruning steps.
+ */
+static void
+ExecInitPruningContext(PartitionPruneContext *context,
+					   List *pruning_steps,
+					   PartitionDesc partdesc,
+					   PartitionKey partkey,
+					   PlanState *planstate)
+{
+	int			n_steps;
+	int			partnatts;
+	ListCell   *lc;
+
+	n_steps = list_length(pruning_steps);
+
+	context->strategy = partkey->strategy;
+	context->partnatts = partnatts = partkey->partnatts;
+	context->nparts = partdesc->nparts;
+	context->boundinfo = partdesc->boundinfo;
+	context->partcollation = partkey->partcollation;
+	context->partsupfunc = partkey->partsupfunc;
+
+	/* We'll look up type-specific support functions as needed */
+	context->stepcmpfuncs = (FmgrInfo *)
+		palloc0(sizeof(FmgrInfo) * n_steps * partnatts);
+
+	context->ppccontext = CurrentMemoryContext;
+	context->planstate = planstate;
+
+	/* Initialize expression state for each expression we need */
+	context->exprstates = (ExprState **)
+		palloc0(sizeof(ExprState *) * n_steps * partnatts);
+	foreach(lc, pruning_steps)
+	{
+		PartitionPruneStepOp *step = (PartitionPruneStepOp *) lfirst(lc);
+		ListCell   *lc2;
+		int			keyno;
+
+		/* not needed for other step kinds */
+		if (!IsA(step, PartitionPruneStepOp))
+			continue;
+
+		Assert(list_length(step->exprs) <= partnatts);
+
+		keyno = 0;
+		foreach(lc2, step->exprs)
+		{
+			Expr	   *expr = (Expr *) lfirst(lc2);
+
+			/* not needed for Consts */
+			if (!IsA(expr, Const))
+			{
+				int			stateidx = PruneCxtStateIdx(partnatts,
+														step->step.step_id,
+														keyno);
+
+				context->exprstates[stateidx] =
+					ExecInitExpr(expr, context->planstate);
+			}
+			keyno++;
+		}
+	}
+}
+
+/*
  * ExecFindInitialMatchingSubPlans
  *		Identify the set of subplans that cannot be eliminated by initial
  *		pruning, disregarding any pruning constraints involving PARAM_EXEC
@@ -1824,7 +1860,8 @@ ExecFindInitialMatchingSubPlans(PartitionPruneState *prunestate, int nsubplans)
 		find_matching_subplans_recurse(prunedata, pprune, true, &result);
 
 		/* Expression eval may have used space in node's ps_ExprContext too */
-		ResetExprContext(pprune->context.planstate->ps_ExprContext);
+		if (pprune->initial_pruning_steps)
+			ResetExprContext(pprune->initial_context.planstate->ps_ExprContext);
 	}
 
 	/* Add in any subplans that partition pruning didn't account for */
@@ -1888,7 +1925,7 @@ ExecFindInitialMatchingSubPlans(PartitionPruneState *prunestate, int nsubplans)
 			for (j = prunedata->num_partrelprunedata - 1; j >= 0; j--)
 			{
 				PartitionedRelPruningData *pprune = &prunedata->partrelprunedata[j];
-				int			nparts = pprune->context.nparts;
+				int			nparts = pprune->initial_context.nparts;
 				int			k;
 
 				/* We just rebuild present_parts from scratch */
@@ -1993,7 +2030,8 @@ ExecFindMatchingSubPlans(PartitionPruneState *prunestate)
 		find_matching_subplans_recurse(prunedata, pprune, false, &result);
 
 		/* Expression eval may have used space in node's ps_ExprContext too */
-		ResetExprContext(pprune->context.planstate->ps_ExprContext);
+		if (pprune->exec_pruning_steps)
+			ResetExprContext(pprune->exec_context.planstate->ps_ExprContext);
 	}
 
 	/* Add in any subplans that partition pruning didn't account for */
@@ -2029,15 +2067,15 @@ find_matching_subplans_recurse(PartitionPruningData *prunedata,
 	check_stack_depth();
 
 	/* Only prune if pruning would be useful at this level. */
-	if (initial_prune ? pprune->do_initial_prune : pprune->do_exec_prune)
+	if (initial_prune && pprune->initial_pruning_steps)
 	{
-		PartitionPruneContext *context = &pprune->context;
-
-		/* Set whether we can evaluate PARAM_EXEC Params or not */
-		context->evalexecparams = !initial_prune;
-
-		partset = get_matching_partitions(context,
-										  pprune->pruning_steps);
+		partset = get_matching_partitions(&pprune->initial_context,
+										  pprune->initial_pruning_steps);
+	}
+	else if (!initial_prune && pprune->exec_pruning_steps)
+	{
+		partset = get_matching_partitions(&pprune->exec_context,
+										  pprune->exec_pruning_steps);
 	}
 	else
 	{
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 780d7ab..78deade 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1198,16 +1198,13 @@ _copyPartitionedRelPruneInfo(const PartitionedRelPruneInfo *from)
 	PartitionedRelPruneInfo *newnode = makeNode(PartitionedRelPruneInfo);
 
 	COPY_SCALAR_FIELD(rtindex);
-	COPY_NODE_FIELD(pruning_steps);
 	COPY_BITMAPSET_FIELD(present_parts);
 	COPY_SCALAR_FIELD(nparts);
-	COPY_SCALAR_FIELD(nexprs);
 	COPY_POINTER_FIELD(subplan_map, from->nparts * sizeof(int));
 	COPY_POINTER_FIELD(subpart_map, from->nparts * sizeof(int));
 	COPY_POINTER_FIELD(relid_map, from->nparts * sizeof(Oid));
-	COPY_POINTER_FIELD(hasexecparam, from->nexprs * sizeof(bool));
-	COPY_SCALAR_FIELD(do_initial_prune);
-	COPY_SCALAR_FIELD(do_exec_prune);
+	COPY_NODE_FIELD(initial_pruning_steps);
+	COPY_NODE_FIELD(exec_pruning_steps);
 	COPY_BITMAPSET_FIELD(execparamids);
 
 	return newnode;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 387e4b9..237598e 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -948,16 +948,13 @@ _outPartitionedRelPruneInfo(StringInfo str, const PartitionedRelPruneInfo *node)
 	WRITE_NODE_TYPE("PARTITIONEDRELPRUNEINFO");
 
 	WRITE_UINT_FIELD(rtindex);
-	WRITE_NODE_FIELD(pruning_steps);
 	WRITE_BITMAPSET_FIELD(present_parts);
 	WRITE_INT_FIELD(nparts);
-	WRITE_INT_FIELD(nexprs);
 	WRITE_INT_ARRAY(subplan_map, node->nparts);
 	WRITE_INT_ARRAY(subpart_map, node->nparts);
 	WRITE_OID_ARRAY(relid_map, node->nparts);
-	WRITE_BOOL_ARRAY(hasexecparam, node->nexprs);
-	WRITE_BOOL_FIELD(do_initial_prune);
-	WRITE_BOOL_FIELD(do_exec_prune);
+	WRITE_NODE_FIELD(initial_pruning_steps);
+	WRITE_NODE_FIELD(exec_pruning_steps);
 	WRITE_BITMAPSET_FIELD(execparamids);
 }
 
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3b96492..6c2626e 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -2388,16 +2388,13 @@ _readPartitionedRelPruneInfo(void)
 	READ_LOCALS(PartitionedRelPruneInfo);
 
 	READ_UINT_FIELD(rtindex);
-	READ_NODE_FIELD(pruning_steps);
 	READ_BITMAPSET_FIELD(present_parts);
 	READ_INT_FIELD(nparts);
-	READ_INT_FIELD(nexprs);
 	READ_INT_ARRAY(subplan_map, local_node->nparts);
 	READ_INT_ARRAY(subpart_map, local_node->nparts);
 	READ_OID_ARRAY(relid_map, local_node->nparts);
-	READ_BOOL_ARRAY(hasexecparam, local_node->nexprs);
-	READ_BOOL_FIELD(do_initial_prune);
-	READ_BOOL_FIELD(do_exec_prune);
+	READ_NODE_FIELD(initial_pruning_steps);
+	READ_NODE_FIELD(exec_pruning_steps);
 	READ_BITMAPSET_FIELD(execparamids);
 
 	READ_DONE();
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index ff3caf1..53f814f 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -85,21 +85,42 @@ typedef enum PartClauseMatchStatus
 } PartClauseMatchStatus;
 
 /*
+ * PartClauseTarget
+ *		Identifies which qual clauses we can use for generating pruning steps
+ */
+typedef enum PartClauseTarget
+{
+	PARTTARGET_PLANNER,			/* want to prune during planning */
+	PARTTARGET_INITIAL,			/* want to prune during executor startup */
+	PARTTARGET_EXEC				/* want to prune during each plan node scan */
+} PartClauseTarget;
+
+/*
  * GeneratePruningStepsContext
  *		Information about the current state of generation of "pruning steps"
  *		for a given set of clauses
  *
- * gen_partprune_steps() initializes an instance of this struct, which is used
- * throughout the step generation process.
+ * gen_partprune_steps() initializes and returns an instance of this struct.
+ *
+ * Note that has_mutable_op, has_mutable_arg, and has_exec_param are set if
+ * we found any potentially-useful-for-pruning clause having those properties,
+ * whether or not we actually used the clause in the steps list.  This
+ * definition allows us to skip the PARTTARGET_EXEC pass in some cases.
  */
 typedef struct GeneratePruningStepsContext
 {
-	/* Input data: */
-	bool		forplanner;		/* true when generating steps to be used
-								 * during query planning */
-	/* Working state and result data: */
+	/* Copies of input arguments for gen_partprune_steps: */
+	RelOptInfo *rel;			/* the partitioned relation */
+	PartClauseTarget target;	/* use-case we're generating steps for */
+	/* Result data: */
+	List	   *steps;			/* list of PartitionPruneSteps */
+	bool		has_mutable_op; /* clauses include any stable operators */
+	bool		has_mutable_arg;	/* clauses include any mutable comparison
+									 * values, *other than* exec params */
+	bool		has_exec_param; /* clauses include any PARAM_EXEC params */
+	bool		contradictory;	/* clauses were proven self-contradictory */
+	/* Working state: */
 	int			next_step_id;
-	List	   *steps;			/* output, list of PartitionPruneSteps */
 } GeneratePruningStepsContext;
 
 /* The result of performing one PartitionPruneStep */
@@ -121,22 +142,20 @@ static List *make_partitionedrel_pruneinfo(PlannerInfo *root,
 							  int *relid_subplan_map,
 							  List *partitioned_rels, List *prunequal,
 							  Bitmapset **matchedsubplans);
-static List *gen_partprune_steps(RelOptInfo *rel, List *clauses,
-					bool forplanner, bool *contradictory);
+static void gen_partprune_steps(RelOptInfo *rel, List *clauses,
+					PartClauseTarget target,
+					GeneratePruningStepsContext *context);
 static List *gen_partprune_steps_internal(GeneratePruningStepsContext *context,
-							 RelOptInfo *rel, List *clauses,
-							 bool *contradictory);
+							 List *clauses);
 static PartitionPruneStep *gen_prune_step_op(GeneratePruningStepsContext *context,
 				  StrategyNumber opstrategy, bool op_is_ne,
 				  List *exprs, List *cmpfns, Bitmapset *nullkeys);
 static PartitionPruneStep *gen_prune_step_combine(GeneratePruningStepsContext *context,
 					   List *source_stepids,
 					   PartitionPruneCombineOp combineOp);
-static PartitionPruneStep *gen_prune_steps_from_opexps(PartitionScheme part_scheme,
-							GeneratePruningStepsContext *context,
+static PartitionPruneStep *gen_prune_steps_from_opexps(GeneratePruningStepsContext *context,
 							List **keyclauses, Bitmapset *nullkeys);
-static PartClauseMatchStatus match_clause_to_partition_key(RelOptInfo *rel,
-							  GeneratePruningStepsContext *context,
+static PartClauseMatchStatus match_clause_to_partition_key(GeneratePruningStepsContext *context,
 							  Expr *clause, Expr *partkey, int partkeyidx,
 							  bool *clause_is_not_null,
 							  PartClauseInfo **pc, List **clause_steps);
@@ -169,8 +188,7 @@ static PruneStepResult *get_matching_range_bounds(PartitionPruneContext *context
 						  FmgrInfo *partsupfunc, Bitmapset *nullkeys);
 static Bitmapset *pull_exec_paramids(Expr *expr);
 static bool pull_exec_paramids_walker(Node *node, Bitmapset **context);
-static bool analyze_partkey_exprs(PartitionedRelPruneInfo *pinfo, List *steps,
-					  int partnatts);
+static Bitmapset *get_partkey_exec_paramids(List *steps);
 static PruneStepResult *perform_pruning_base_step(PartitionPruneContext *context,
 						  PartitionPruneStepOp *opstep);
 static PruneStepResult *perform_pruning_combine_step(PartitionPruneContext *context,
@@ -178,7 +196,7 @@ static PruneStepResult *perform_pruning_combine_step(PartitionPruneContext *cont
 							 PruneStepResult **step_results);
 static bool match_boolean_partition_clause(Oid partopfamily, Expr *clause,
 							   Expr *partkey, Expr **outconst);
-static bool partkey_datum_from_expr(PartitionPruneContext *context,
+static void partkey_datum_from_expr(PartitionPruneContext *context,
 						Expr *expr, int stateidx,
 						Datum *value, bool *isnull);
 
@@ -347,10 +365,11 @@ make_partitionedrel_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 		Index		rti = lfirst_int(lc);
 		RelOptInfo *subpart = find_base_rel(root, rti);
 		PartitionedRelPruneInfo *pinfo;
-		int			partnatts = subpart->part_scheme->partnatts;
 		List	   *partprunequal;
-		List	   *pruning_steps;
-		bool		contradictory;
+		List	   *initial_pruning_steps;
+		List	   *exec_pruning_steps;
+		Bitmapset  *execparamids;
+		GeneratePruningStepsContext context;
 
 		/*
 		 * Fill the mapping array.
@@ -415,15 +434,16 @@ make_partitionedrel_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 		}
 
 		/*
-		 * Convert pruning qual to pruning steps.  Since these steps will be
-		 * used in the executor, we can pass 'forplanner' as false to allow
-		 * steps to be generated that are unsafe for evaluation during
-		 * planning, e.g. evaluation of stable functions.
+		 * Convert pruning qual to pruning steps.  We may need to do this
+		 * twice, once to obtain executor startup pruning steps, and once for
+		 * executor per-scan pruning steps.  This first pass creates startup
+		 * pruning steps and detects whether there's any possibly-useful quals
+		 * that would require per-scan pruning.
 		 */
-		pruning_steps = gen_partprune_steps(subpart, partprunequal, false,
-											&contradictory);
+		gen_partprune_steps(subpart, partprunequal, PARTTARGET_INITIAL,
+							&context);
 
-		if (contradictory)
+		if (context.contradictory)
 		{
 			/*
 			 * This shouldn't happen as the planner should have detected this
@@ -437,20 +457,63 @@ make_partitionedrel_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 			return NIL;
 		}
 
+		/*
+		 * If no mutable operators or expressions appear in usable pruning
+		 * clauses, then there's no point in running startup pruning, because
+		 * plan-time pruning should have pruned everything prunable.
+		 */
+		if (context.has_mutable_op || context.has_mutable_arg)
+			initial_pruning_steps = context.steps;
+		else
+			initial_pruning_steps = NIL;
+
+		/*
+		 * If no exec Params appear in potentially-usable pruning clauses,
+		 * then there's no point in even thinking about per-scan pruning.
+		 */
+		if (context.has_exec_param)
+		{
+			/* ... OK, we'd better think about it */
+			gen_partprune_steps(subpart, partprunequal, PARTTARGET_EXEC,
+								&context);
+
+			if (context.contradictory)
+			{
+				/* As above, skip run-time pruning if anything fishy happens */
+				return NIL;
+			}
+
+			exec_pruning_steps = context.steps;
+
+			/*
+			 * Detect which exec Params actually got used; the fact that some
+			 * were in available clauses doesn't mean we actually used them.
+			 * Skip per-scan pruning if there are none.
+			 */
+			execparamids = get_partkey_exec_paramids(exec_pruning_steps);
+
+			if (bms_is_empty(execparamids))
+				exec_pruning_steps = NIL;
+		}
+		else
+		{
+			/* No exec Params anywhere, so forget about scan-time pruning */
+			exec_pruning_steps = NIL;
+			execparamids = NULL;
+		}
+
+		if (initial_pruning_steps || exec_pruning_steps)
+			doruntimeprune = true;
+
 		/* Begin constructing the PartitionedRelPruneInfo for this rel */
 		pinfo = makeNode(PartitionedRelPruneInfo);
 		pinfo->rtindex = rti;
-		pinfo->pruning_steps = pruning_steps;
+		pinfo->initial_pruning_steps = initial_pruning_steps;
+		pinfo->exec_pruning_steps = exec_pruning_steps;
+		pinfo->execparamids = execparamids;
 		/* Remaining fields will be filled in the next loop */
 
 		pinfolist = lappend(pinfolist, pinfo);
-
-		/*
-		 * Determine which pruning types should be enabled at this level. This
-		 * also records paramids relevant to pruning steps in 'pinfo'.
-		 */
-		doruntimeprune |= analyze_partkey_exprs(pinfo, pruning_steps,
-												partnatts);
 	}
 
 	if (!doruntimeprune)
@@ -532,37 +595,25 @@ make_partitionedrel_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 
 /*
  * gen_partprune_steps
- *		Process 'clauses' (a rel's baserestrictinfo list of clauses) and return
- *		a list of "partition pruning steps".
+ *		Process 'clauses' (typically a rel's baserestrictinfo list of clauses)
+ *		and create a list of "partition pruning steps".
  *
- * 'forplanner' must be true when generating steps to be evaluated during
- * query planning, false when generating steps to be used at run-time.
+ * 'target' tells whether to generate pruning steps for planning (use
+ * immutable clauses only), or for executor startup (use any allowable
+ * clause except ones containing PARAM_EXEC Params), or for executor
+ * per-scan pruning (use any allowable clause).
  *
- * The result generated with forplanner=false includes all clauses that
- * are selected with forplanner=true, because in some cases we need a
- * combination of clauses to prune successfully.  For example, if we
- * are partitioning on a hash of columns A and B, and we have clauses
- * "WHERE A=constant AND B=nonconstant", we can't do anything at plan
- * time even though the first clause would be evaluable then.  And we
- * must include the first clause when called with forplanner=false,
- * or we'll fail to prune at run-time either.  This does mean that when
- * called with forplanner=false, we may return steps that don't actually
- * need to be executed at runtime; it's left to analyze_partkey_exprs()
- * to (re)discover that.
- *
- * If the clauses in the input list are contradictory or there is a
- * pseudo-constant "false", *contradictory is set to true upon return,
- * else it's set false.
+ * 'context' is an output argument that receives the steps list as well as
+ * some subsidiary flags; see the GeneratePruningStepsContext typedef.
  */
-static List *
-gen_partprune_steps(RelOptInfo *rel, List *clauses, bool forplanner,
-					bool *contradictory)
+static void
+gen_partprune_steps(RelOptInfo *rel, List *clauses, PartClauseTarget target,
+					GeneratePruningStepsContext *context)
 {
-	GeneratePruningStepsContext context;
-
-	context.forplanner = forplanner;
-	context.next_step_id = 0;
-	context.steps = NIL;
+	/* Initialize all output values to zero/false/NULL */
+	memset(context, 0, sizeof(GeneratePruningStepsContext));
+	context->rel = rel;
+	context->target = target;
 
 	/*
 	 * For sub-partitioned tables there's a corner case where if the
@@ -593,9 +644,7 @@ gen_partprune_steps(RelOptInfo *rel, List *clauses, bool forplanner,
 	}
 
 	/* Down into the rabbit-hole. */
-	(void) gen_partprune_steps_internal(&context, rel, clauses, contradictory);
-
-	return context.steps;
+	(void) gen_partprune_steps_internal(context, clauses);
 }
 
 /*
@@ -613,7 +662,7 @@ prune_append_rel_partitions(RelOptInfo *rel)
 {
 	List	   *clauses = rel->baserestrictinfo;
 	List	   *pruning_steps;
-	bool		contradictory;
+	GeneratePruningStepsContext gcontext;
 	PartitionPruneContext context;
 
 	Assert(rel->part_scheme != NULL);
@@ -630,15 +679,19 @@ prune_append_rel_partitions(RelOptInfo *rel)
 		return bms_add_range(NULL, 0, rel->nparts - 1);
 
 	/*
-	 * Process clauses.  If the clauses are found to be contradictory, we can
-	 * return the empty set.  Pass 'forplanner' as true to indicate to the
-	 * pruning code that we only want pruning steps that can be evaluated
-	 * during planning.
+	 * Process clauses to extract pruning steps that are usable at plan time.
+	 * If the clauses are found to be contradictory, we can return the empty
+	 * set.
 	 */
-	pruning_steps = gen_partprune_steps(rel, clauses, true,
-										&contradictory);
-	if (contradictory)
+	gen_partprune_steps(rel, clauses, PARTTARGET_PLANNER,
+						&gcontext);
+	if (gcontext.contradictory)
 		return NULL;
+	pruning_steps = gcontext.steps;
+
+	/* If there's nothing usable, return all partitions */
+	if (pruning_steps == NIL)
+		return bms_add_range(NULL, 0, rel->nparts - 1);
 
 	/* Set up PartitionPruneContext */
 	context.strategy = rel->part_scheme->strategy;
@@ -655,8 +708,6 @@ prune_append_rel_partitions(RelOptInfo *rel)
 	/* These are not valid when being called from the planner */
 	context.planstate = NULL;
 	context.exprstates = NULL;
-	context.exprhasexecparam = NULL;
-	context.evalexecparams = false;
 
 	/* Actual pruning happens here. */
 	return get_matching_partitions(&context, pruning_steps);
@@ -667,7 +718,7 @@ prune_append_rel_partitions(RelOptInfo *rel)
  *		Determine partitions that survive partition pruning
  *
  * Note: context->planstate must be set to a valid PlanState when the
- * pruning_steps were generated with 'forplanner' = false.
+ * pruning_steps were generated with a target other than PARTTARGET_PLANNER.
  *
  * Returns a Bitmapset of the RelOptInfo->part_rels indexes of the surviving
  * partitions.
@@ -786,16 +837,15 @@ get_matching_partitions(PartitionPruneContext *context, List *pruning_steps)
  * even across recursive calls.
  *
  * If we find clauses that are mutually contradictory, or a pseudoconstant
- * clause that contains false, we set *contradictory to true and return NIL
- * (that is, no pruning steps).  Caller should consider all partitions as
- * pruned in that case.  Otherwise, *contradictory is set to false.
+ * clause that contains false, we set context->contradictory to true and
+ * return NIL (that is, no pruning steps).  Caller should consider all
+ * partitions as pruned in that case.
  */
 static List *
 gen_partprune_steps_internal(GeneratePruningStepsContext *context,
-							 RelOptInfo *rel, List *clauses,
-							 bool *contradictory)
+							 List *clauses)
 {
-	PartitionScheme part_scheme = rel->part_scheme;
+	PartitionScheme part_scheme = context->rel->part_scheme;
 	List	   *keyclauses[PARTITION_MAX_KEYS];
 	Bitmapset  *nullkeys = NULL,
 			   *notnullkeys = NULL;
@@ -803,8 +853,6 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 	List	   *result = NIL;
 	ListCell   *lc;
 
-	*contradictory = false;
-
 	memset(keyclauses, 0, sizeof(keyclauses));
 	foreach(lc, clauses)
 	{
@@ -820,7 +868,7 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 			(((Const *) clause)->constisnull ||
 			 !DatumGetBool(((Const *) clause)->constvalue)))
 		{
-			*contradictory = true;
+			context->contradictory = true;
 			return NIL;
 		}
 
@@ -842,6 +890,12 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 				ListCell   *lc1;
 
 				/*
+				 * We can share the outer context area with the recursive
+				 * call, but contradictory had better not be true yet.
+				 */
+				Assert(!context->contradictory);
+
+				/*
 				 * Get pruning step for each arg.  If we get contradictory for
 				 * all args, it means the OR expression is false as a whole.
 				 */
@@ -851,11 +905,18 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 					bool		arg_contradictory;
 					List	   *argsteps;
 
-					argsteps =
-						gen_partprune_steps_internal(context, rel,
-													 list_make1(arg),
-													 &arg_contradictory);
-					if (!arg_contradictory)
+					argsteps = gen_partprune_steps_internal(context,
+															list_make1(arg));
+					arg_contradictory = context->contradictory;
+					/* Keep context->contradictory clear till we're done */
+					context->contradictory = false;
+
+					if (arg_contradictory)
+					{
+						/* Just ignore self-contradictory arguments. */
+						continue;
+					}
+					else
 						all_args_contradictory = false;
 
 					if (argsteps != NIL)
@@ -869,34 +930,28 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 					else
 					{
 						/*
-						 * No steps either means that arg_contradictory is
-						 * true or the arg didn't contain a clause matching
-						 * this partition key.
+						 * The arg didn't contain a clause matching this
+						 * partition key.  We cannot prune using such an arg.
+						 * To indicate that to the pruning code, we must
+						 * construct a dummy PartitionPruneStepCombine whose
+						 * source_stepids is set to an empty List.
 						 *
-						 * In case of the latter, we cannot prune using such
-						 * an arg.  To indicate that to the pruning code, we
-						 * must construct a dummy PartitionPruneStepCombine
-						 * whose source_stepids is set to an empty List.
 						 * However, if we can prove using constraint exclusion
 						 * that the clause refutes the table's partition
 						 * constraint (if it's sub-partitioned), we need not
 						 * bother with that.  That is, we effectively ignore
 						 * this OR arm.
 						 */
-						List	   *partconstr = rel->partition_qual;
+						List	   *partconstr = context->rel->partition_qual;
 						PartitionPruneStep *orstep;
 
-						/* Just ignore this argument. */
-						if (arg_contradictory)
-							continue;
-
 						if (partconstr)
 						{
 							partconstr = (List *)
 								expression_planner((Expr *) partconstr);
-							if (rel->relid != 1)
+							if (context->rel->relid != 1)
 								ChangeVarNodes((Node *) partconstr, 1,
-											   rel->relid, 0);
+											   context->rel->relid, 0);
 							if (predicate_refuted_by(partconstr,
 													 list_make1(arg),
 													 false))
@@ -909,11 +964,12 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 					}
 				}
 
-				*contradictory = all_args_contradictory;
-
-				/* Check if any contradicting clauses were found */
-				if (*contradictory)
+				/* If all the OR arms are contradictory, we can stop */
+				if (all_args_contradictory)
+				{
+					context->contradictory = true;
 					return NIL;
+				}
 
 				if (arg_stepids != NIL)
 				{
@@ -937,9 +993,10 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 				 * recurse and later combine the component partitions sets
 				 * using a combine step.
 				 */
-				argsteps = gen_partprune_steps_internal(context, rel, args,
-														contradictory);
-				if (*contradictory)
+				argsteps = gen_partprune_steps_internal(context, args);
+
+				/* If any AND arm is contradictory, we can stop immediately */
+				if (context->contradictory)
 					return NIL;
 
 				foreach(lc1, argsteps)
@@ -969,17 +1026,16 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 		}
 
 		/*
-		 * Must be a clause for which we can check if one of its args matches
-		 * the partition key.
+		 * See if we can match this clause to any of the partition keys.
 		 */
 		for (i = 0; i < part_scheme->partnatts; i++)
 		{
-			Expr	   *partkey = linitial(rel->partexprs[i]);
+			Expr	   *partkey = linitial(context->rel->partexprs[i]);
 			bool		clause_is_not_null = false;
 			PartClauseInfo *pc = NULL;
 			List	   *clause_steps = NIL;
 
-			switch (match_clause_to_partition_key(rel, context,
+			switch (match_clause_to_partition_key(context,
 												  clause, partkey, i,
 												  &clause_is_not_null,
 												  &pc, &clause_steps))
@@ -993,7 +1049,7 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 					 */
 					if (bms_is_member(i, nullkeys))
 					{
-						*contradictory = true;
+						context->contradictory = true;
 						return NIL;
 					}
 					generate_opsteps = true;
@@ -1006,7 +1062,7 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 						/* check for conflicting IS NOT NULL */
 						if (bms_is_member(i, notnullkeys))
 						{
-							*contradictory = true;
+							context->contradictory = true;
 							return NIL;
 						}
 						nullkeys = bms_add_member(nullkeys, i);
@@ -1016,7 +1072,7 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 						/* check for conflicting IS NULL */
 						if (bms_is_member(i, nullkeys))
 						{
-							*contradictory = true;
+							context->contradictory = true;
 							return NIL;
 						}
 						notnullkeys = bms_add_member(notnullkeys, i);
@@ -1030,7 +1086,7 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 
 				case PARTCLAUSE_MATCH_CONTRADICT:
 					/* We've nothing more to do if a contradiction was found. */
-					*contradictory = true;
+					context->contradictory = true;
 					return NIL;
 
 				case PARTCLAUSE_NOMATCH:
@@ -1091,8 +1147,7 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 		PartitionPruneStep *step;
 
 		/* Strategy 2 */
-		step = gen_prune_steps_from_opexps(part_scheme, context,
-										   keyclauses, nullkeys);
+		step = gen_prune_steps_from_opexps(context, keyclauses, nullkeys);
 		if (step != NULL)
 			result = lappend(result, step);
 	}
@@ -1201,15 +1256,15 @@ gen_prune_step_combine(GeneratePruningStepsContext *context,
  * found for any subsequent keys; see specific notes below.
  */
 static PartitionPruneStep *
-gen_prune_steps_from_opexps(PartitionScheme part_scheme,
-							GeneratePruningStepsContext *context,
+gen_prune_steps_from_opexps(GeneratePruningStepsContext *context,
 							List **keyclauses, Bitmapset *nullkeys)
 {
-	ListCell   *lc;
+	PartitionScheme part_scheme = context->rel->part_scheme;
 	List	   *opsteps = NIL;
 	List	   *btree_clauses[BTMaxStrategyNumber + 1],
 			   *hash_clauses[HTMaxStrategyNumber + 1];
 	int			i;
+	ListCell   *lc;
 
 	memset(btree_clauses, 0, sizeof(btree_clauses));
 	memset(hash_clauses, 0, sizeof(hash_clauses));
@@ -1563,13 +1618,12 @@ gen_prune_steps_from_opexps(PartitionScheme part_scheme,
  *   Output arguments: none set.
  */
 static PartClauseMatchStatus
-match_clause_to_partition_key(RelOptInfo *rel,
-							  GeneratePruningStepsContext *context,
+match_clause_to_partition_key(GeneratePruningStepsContext *context,
 							  Expr *clause, Expr *partkey, int partkeyidx,
 							  bool *clause_is_not_null, PartClauseInfo **pc,
 							  List **clause_steps)
 {
-	PartitionScheme part_scheme = rel->part_scheme;
+	PartitionScheme part_scheme = context->rel->part_scheme;
 	Oid			partopfamily = part_scheme->partopfamily[partkeyidx],
 				partcoll = part_scheme->partcollation[partkeyidx];
 	Expr	   *expr;
@@ -1588,7 +1642,7 @@ match_clause_to_partition_key(RelOptInfo *rel,
 		partclause->op_is_ne = false;
 		partclause->expr = expr;
 		/* We know that expr is of Boolean type. */
-		partclause->cmpfn = rel->part_scheme->partsupfunc[partkeyidx].fn_oid;
+		partclause->cmpfn = part_scheme->partsupfunc[partkeyidx].fn_oid;
 		partclause->op_strategy = InvalidStrategy;
 
 		*pc = partclause;
@@ -1647,59 +1701,6 @@ match_clause_to_partition_key(RelOptInfo *rel,
 			return PARTCLAUSE_NOMATCH;
 
 		/*
-		 * Matched with this key.  Now check various properties of the clause
-		 * to see if it's sane to use it for pruning.  In most of these cases,
-		 * we can return UNSUPPORTED because the same failure would occur no
-		 * matter which partkey it's matched to.  (In particular, now that
-		 * we've successfully matched one side of the opclause to a partkey,
-		 * there is no chance that matching the other side to another partkey
-		 * will produce a usable result, since that'd mean there are Vars on
-		 * both sides.)
-		 */
-		if (context->forplanner)
-		{
-			/*
-			 * When pruning in the planner, we only support pruning using
-			 * comparisons to constants.  Immutable subexpressions will have
-			 * been folded to constants already, and we cannot prune on the
-			 * basis of anything that's not immutable.
-			 */
-			if (!IsA(expr, Const))
-				return PARTCLAUSE_UNSUPPORTED;
-
-			/*
-			 * Also, the comparison operator itself must be immutable.
-			 */
-			if (op_volatile(opno) != PROVOLATILE_IMMUTABLE)
-				return PARTCLAUSE_UNSUPPORTED;
-		}
-		else
-		{
-			/*
-			 * Otherwise, non-consts are allowed, but we can't prune using an
-			 * expression that contains Vars.
-			 */
-			if (contain_var_clause((Node *) expr))
-				return PARTCLAUSE_UNSUPPORTED;
-
-			/*
-			 * And we must reject anything containing a volatile function.
-			 * Stable functions are OK though.  (We need not check this for
-			 * the comparison operator itself: anything that belongs to a
-			 * partitioning operator family must be at least stable.)
-			 */
-			if (contain_volatile_functions((Node *) expr))
-				return PARTCLAUSE_UNSUPPORTED;
-		}
-
-		/*
-		 * Only allow strict operators.  This will guarantee nulls are
-		 * filtered.
-		 */
-		if (!op_strict(opno))
-			return PARTCLAUSE_UNSUPPORTED;
-
-		/*
 		 * See if the operator is relevant to the partitioning opfamily.
 		 *
 		 * Normally we only care about operators that are listed as being part
@@ -1740,6 +1741,95 @@ match_clause_to_partition_key(RelOptInfo *rel,
 		}
 
 		/*
+		 * Only allow strict operators.  This will guarantee nulls are
+		 * filtered.  (This test is likely useless, since btree and hash
+		 * comparison operators are generally strict.)
+		 */
+		if (!op_strict(opno))
+			return PARTCLAUSE_UNSUPPORTED;
+
+		/*
+		 * OK, we have a match to the partition key and a suitable operator.
+		 * Examine the other argument to see if it's usable for pruning.
+		 *
+		 * In most of these cases, we can return UNSUPPORTED because the same
+		 * failure would occur no matter which partkey it's matched to.  (In
+		 * particular, now that we've successfully matched one side of the
+		 * opclause to a partkey, there is no chance that matching the other
+		 * side to another partkey will produce a usable result, since that'd
+		 * mean there are Vars on both sides.)
+		 *
+		 * Also, if we reject an argument for a target-dependent reason, set
+		 * appropriate fields of *context to report that.  We postpone these
+		 * tests until after matching the partkey and the operator, so as to
+		 * reduce the odds of setting the context fields for clauses that do
+		 * not end up contributing to pruning steps.
+		 *
+		 * First, check for non-Const argument.  (We assume that any immutable
+		 * subexpression will have been folded to a Const already.)
+		 */
+		if (!IsA(expr, Const))
+		{
+			Bitmapset  *paramids;
+
+			/*
+			 * When pruning in the planner, we only support pruning using
+			 * comparisons to constants.  We cannot prune on the basis of
+			 * anything that's not immutable.  (Note that has_mutable_arg and
+			 * has_exec_param do not get set for this target value.)
+			 */
+			if (context->target == PARTTARGET_PLANNER)
+				return PARTCLAUSE_UNSUPPORTED;
+
+			/*
+			 * We can never prune using an expression that contains Vars.
+			 */
+			if (contain_var_clause((Node *) expr))
+				return PARTCLAUSE_UNSUPPORTED;
+
+			/*
+			 * And we must reject anything containing a volatile function.
+			 * Stable functions are OK though.
+			 */
+			if (contain_volatile_functions((Node *) expr))
+				return PARTCLAUSE_UNSUPPORTED;
+
+			/*
+			 * See if there are any exec Params.  If so, we can only use this
+			 * expression during per-scan pruning.
+			 */
+			paramids = pull_exec_paramids(expr);
+			if (!bms_is_empty(paramids))
+			{
+				context->has_exec_param = true;
+				if (context->target != PARTTARGET_EXEC)
+					return PARTCLAUSE_UNSUPPORTED;
+			}
+			else
+			{
+				/* It's potentially usable, but mutable */
+				context->has_mutable_arg = true;
+			}
+		}
+
+		/*
+		 * Check whether the comparison operator itself is immutable.  (We
+		 * assume anything that's in a btree or hash opclass is at least
+		 * stable, but we need to check for immutability.)
+		 */
+		if (op_volatile(opno) != PROVOLATILE_IMMUTABLE)
+		{
+			context->has_mutable_op = true;
+
+			/*
+			 * When pruning in the planner, we cannot prune with mutable
+			 * operators.
+			 */
+			if (context->target == PARTTARGET_PLANNER)
+				return PARTCLAUSE_UNSUPPORTED;
+		}
+
+		/*
 		 * Now find the procedure to use, based on the types.  If the clause's
 		 * other argument is of the same type as the partitioning opclass's
 		 * declared input type, we can use the procedure cached in
@@ -1822,7 +1912,6 @@ match_clause_to_partition_key(RelOptInfo *rel,
 		List	   *elem_exprs,
 				   *elem_clauses;
 		ListCell   *lc1;
-		bool		contradictory;
 
 		if (IsA(leftop, RelabelType))
 			leftop = ((RelabelType *) leftop)->arg;
@@ -1833,54 +1922,8 @@ match_clause_to_partition_key(RelOptInfo *rel,
 			return PARTCLAUSE_NOMATCH;
 
 		/*
-		 * Matched with this key.  Check various properties of the clause to
-		 * see if it can sanely be used for partition pruning (this is
-		 * identical to the logic for a plain OpExpr).
-		 */
-		if (context->forplanner)
-		{
-			/*
-			 * When pruning in the planner, we only support pruning using
-			 * comparisons to constants.  Immutable subexpressions will have
-			 * been folded to constants already, and we cannot prune on the
-			 * basis of anything that's not immutable.
-			 */
-			if (!IsA(rightop, Const))
-				return PARTCLAUSE_UNSUPPORTED;
-
-			/*
-			 * Also, the comparison operator itself must be immutable.
-			 */
-			if (op_volatile(saop_op) != PROVOLATILE_IMMUTABLE)
-				return PARTCLAUSE_UNSUPPORTED;
-		}
-		else
-		{
-			/*
-			 * Otherwise, non-consts are allowed, but we can't prune using an
-			 * expression that contains Vars.
-			 */
-			if (contain_var_clause((Node *) rightop))
-				return PARTCLAUSE_UNSUPPORTED;
-
-			/*
-			 * And we must reject anything containing a volatile function.
-			 * Stable functions are OK though.  (We need not check this for
-			 * the comparison operator itself: anything that belongs to a
-			 * partitioning operator family must be at least stable.)
-			 */
-			if (contain_volatile_functions((Node *) rightop))
-				return PARTCLAUSE_UNSUPPORTED;
-		}
-
-		/*
-		 * Only allow strict operators.  This will guarantee nulls are
-		 * filtered.
-		 */
-		if (!op_strict(saop_op))
-			return PARTCLAUSE_UNSUPPORTED;
-
-		/*
+		 * See if the operator is relevant to the partitioning opfamily.
+		 *
 		 * In case of NOT IN (..), we get a '<>', which we handle if list
 		 * partitioning is in use and we're able to confirm that it's negator
 		 * is a btree equality operator belonging to the partitioning operator
@@ -1911,12 +1954,89 @@ match_clause_to_partition_key(RelOptInfo *rel,
 		}
 
 		/*
-		 * First generate a list of Const nodes, one for each array element
-		 * (excepting nulls).
+		 * Only allow strict operators.  This will guarantee nulls are
+		 * filtered.  (This test is likely useless, since btree and hash
+		 * comparison operators are generally strict.)
+		 */
+		if (!op_strict(saop_op))
+			return PARTCLAUSE_UNSUPPORTED;
+
+		/*
+		 * OK, we have a match to the partition key and a suitable operator.
+		 * Examine the array argument to see if it's usable for pruning.  This
+		 * is identical to the logic for a plain OpExpr.
+		 */
+		if (!IsA(rightop, Const))
+		{
+			Bitmapset  *paramids;
+
+			/*
+			 * When pruning in the planner, we only support pruning using
+			 * comparisons to constants.  We cannot prune on the basis of
+			 * anything that's not immutable.  (Note that has_mutable_arg and
+			 * has_exec_param do not get set for this target value.)
+			 */
+			if (context->target == PARTTARGET_PLANNER)
+				return PARTCLAUSE_UNSUPPORTED;
+
+			/*
+			 * We can never prune using an expression that contains Vars.
+			 */
+			if (contain_var_clause((Node *) rightop))
+				return PARTCLAUSE_UNSUPPORTED;
+
+			/*
+			 * And we must reject anything containing a volatile function.
+			 * Stable functions are OK though.
+			 */
+			if (contain_volatile_functions((Node *) rightop))
+				return PARTCLAUSE_UNSUPPORTED;
+
+			/*
+			 * See if there are any exec Params.  If so, we can only use this
+			 * expression during per-scan pruning.
+			 */
+			paramids = pull_exec_paramids(rightop);
+			if (!bms_is_empty(paramids))
+			{
+				context->has_exec_param = true;
+				if (context->target != PARTTARGET_EXEC)
+					return PARTCLAUSE_UNSUPPORTED;
+			}
+			else
+			{
+				/* It's potentially usable, but mutable */
+				context->has_mutable_arg = true;
+			}
+		}
+
+		/*
+		 * Check whether the comparison operator itself is immutable.  (We
+		 * assume anything that's in a btree or hash opclass is at least
+		 * stable, but we need to check for immutability.)
+		 */
+		if (op_volatile(saop_op) != PROVOLATILE_IMMUTABLE)
+		{
+			context->has_mutable_op = true;
+
+			/*
+			 * When pruning in the planner, we cannot prune with mutable
+			 * operators.
+			 */
+			if (context->target == PARTTARGET_PLANNER)
+				return PARTCLAUSE_UNSUPPORTED;
+		}
+
+		/*
+		 * Examine the contents of the array argument.
 		 */
 		elem_exprs = NIL;
 		if (IsA(rightop, Const))
 		{
+			/*
+			 * For a constant array, convert the elements to a list of Const
+			 * nodes, one for each array element (excepting nulls).
+			 */
 			Const	   *arr = (Const *) rightop;
 			ArrayType  *arrval = DatumGetArrayTypeP(arr->constvalue);
 			int16		elemlen;
@@ -1968,6 +2088,9 @@ match_clause_to_partition_key(RelOptInfo *rel,
 			if (arrexpr->multidims)
 				return PARTCLAUSE_UNSUPPORTED;
 
+			/*
+			 * Otherwise, we can just use the list of element values.
+			 */
 			elem_exprs = arrexpr->elements;
 		}
 		else
@@ -2000,10 +2123,8 @@ match_clause_to_partition_key(RelOptInfo *rel,
 			elem_clauses = list_make1(makeBoolExpr(OR_EXPR, elem_clauses, -1));
 
 		/* Finally, generate steps */
-		*clause_steps =
-			gen_partprune_steps_internal(context, rel, elem_clauses,
-										 &contradictory);
-		if (contradictory)
+		*clause_steps = gen_partprune_steps_internal(context, elem_clauses);
+		if (context->contradictory)
 			return PARTCLAUSE_MATCH_CONTRADICT;
 		else if (*clause_steps == NIL)
 			return PARTCLAUSE_UNSUPPORTED;	/* step generation failed */
@@ -2985,89 +3106,38 @@ pull_exec_paramids_walker(Node *node, Bitmapset **context)
 }
 
 /*
- * analyze_partkey_exprs
- *		Loop through all pruning steps and identify which ones require
- *		executor startup-time or executor run-time pruning.
- *
- * Returns true if any executor partition pruning should be attempted at this
- * level.  Also fills fields of *pinfo to record how to process each step.
+ * get_partkey_exec_paramids
+ *		Loop through given pruning steps and find out which exec Params
+ *		are used.
  *
- * Note: when this is called, not much of *pinfo is valid; but that's OK
- * since we only use it as an output area.
+ * Returns a Bitmapset of Param IDs.
  */
-static bool
-analyze_partkey_exprs(PartitionedRelPruneInfo *pinfo, List *steps,
-					  int partnatts)
+static Bitmapset *
+get_partkey_exec_paramids(List *steps)
 {
-	bool		doruntimeprune = false;
+	Bitmapset  *execparamids = NULL;
 	ListCell   *lc;
 
-	/*
-	 * Steps require run-time pruning if they contain EXEC_PARAM Params.
-	 * Otherwise, if their expressions aren't simple Consts or they involve
-	 * non-immutable comparison operators, they require startup-time pruning.
-	 * (Otherwise, the pruning would have been done at plan time.)
-	 *
-	 * Notice that what we actually check for mutability is the comparison
-	 * functions, not the original operators.  This relies on the support
-	 * functions of the btree or hash opfamily being marked consistently with
-	 * the operators.
-	 */
-	pinfo->nexprs = list_length(steps) * partnatts;
-	pinfo->hasexecparam = (bool *) palloc0(sizeof(bool) * pinfo->nexprs);
-	pinfo->do_initial_prune = false;
-	pinfo->do_exec_prune = false;
-	pinfo->execparamids = NULL;
-
 	foreach(lc, steps)
 	{
 		PartitionPruneStepOp *step = (PartitionPruneStepOp *) lfirst(lc);
 		ListCell   *lc2;
-		ListCell   *lc3;
-		int			keyno;
 
 		if (!IsA(step, PartitionPruneStepOp))
 			continue;
 
-		keyno = 0;
-		Assert(list_length(step->exprs) == list_length(step->cmpfns));
-		forboth(lc2, step->exprs, lc3, step->cmpfns)
+		foreach(lc2, step->exprs)
 		{
 			Expr	   *expr = lfirst(lc2);
-			Oid			fnoid = lfirst_oid(lc3);
 
+			/* We can be quick for plain Consts */
 			if (!IsA(expr, Const))
-			{
-				Bitmapset  *execparamids = pull_exec_paramids(expr);
-				bool		hasexecparams;
-				int			stateidx = PruneCxtStateIdx(partnatts,
-														step->step.step_id,
-														keyno);
-
-				Assert(stateidx < pinfo->nexprs);
-				hasexecparams = !bms_is_empty(execparamids);
-				pinfo->hasexecparam[stateidx] = hasexecparams;
-				pinfo->execparamids = bms_join(pinfo->execparamids,
-											   execparamids);
-
-				if (hasexecparams)
-					pinfo->do_exec_prune = true;
-				else
-					pinfo->do_initial_prune = true;
-
-				doruntimeprune = true;
-			}
-			else if (func_volatile(fnoid) != PROVOLATILE_IMMUTABLE)
-			{
-				/* No exec params here, but must do initial pruning */
-				pinfo->do_initial_prune = true;
-				doruntimeprune = true;
-			}
-			keyno++;
+				execparamids = bms_join(execparamids,
+										pull_exec_paramids(expr));
 		}
 	}
 
-	return doruntimeprune;
+	return execparamids;
 }
 
 /*
@@ -3125,56 +3195,54 @@ perform_pruning_base_step(PartitionPruneContext *context,
 			Expr	   *expr;
 			Datum		datum;
 			bool		isnull;
+			Oid			cmpfn;
 
 			expr = lfirst(lc1);
 			stateidx = PruneCxtStateIdx(context->partnatts,
 										opstep->step.step_id, keyno);
-			if (partkey_datum_from_expr(context, expr, stateidx,
-										&datum, &isnull))
-			{
-				Oid			cmpfn;
+			partkey_datum_from_expr(context, expr, stateidx,
+									&datum, &isnull);
 
-				/*
-				 * Since we only allow strict operators in pruning steps, any
-				 * null-valued comparison value must cause the comparison to
-				 * fail, so that no partitions could match.
-				 */
-				if (isnull)
-				{
-					PruneStepResult *result;
-
-					result = (PruneStepResult *) palloc(sizeof(PruneStepResult));
-					result->bound_offsets = NULL;
-					result->scan_default = false;
-					result->scan_null = false;
+			/*
+			 * Since we only allow strict operators in pruning steps, any
+			 * null-valued comparison value must cause the comparison to fail,
+			 * so that no partitions could match.
+			 */
+			if (isnull)
+			{
+				PruneStepResult *result;
 
-					return result;
-				}
+				result = (PruneStepResult *) palloc(sizeof(PruneStepResult));
+				result->bound_offsets = NULL;
+				result->scan_default = false;
+				result->scan_null = false;
 
-				/* Set up the stepcmpfuncs entry, unless we already did */
-				cmpfn = lfirst_oid(lc2);
-				Assert(OidIsValid(cmpfn));
-				if (cmpfn != context->stepcmpfuncs[stateidx].fn_oid)
-				{
-					/*
-					 * If the needed support function is the same one cached
-					 * in the relation's partition key, copy the cached
-					 * FmgrInfo.  Otherwise (i.e., when we have a cross-type
-					 * comparison), an actual lookup is required.
-					 */
-					if (cmpfn == context->partsupfunc[keyno].fn_oid)
-						fmgr_info_copy(&context->stepcmpfuncs[stateidx],
-									   &context->partsupfunc[keyno],
-									   context->ppccontext);
-					else
-						fmgr_info_cxt(cmpfn, &context->stepcmpfuncs[stateidx],
-									  context->ppccontext);
-				}
+				return result;
+			}
 
-				values[keyno] = datum;
-				nvalues++;
+			/* Set up the stepcmpfuncs entry, unless we already did */
+			cmpfn = lfirst_oid(lc2);
+			Assert(OidIsValid(cmpfn));
+			if (cmpfn != context->stepcmpfuncs[stateidx].fn_oid)
+			{
+				/*
+				 * If the needed support function is the same one cached in
+				 * the relation's partition key, copy the cached FmgrInfo.
+				 * Otherwise (i.e., when we have a cross-type comparison), an
+				 * actual lookup is required.
+				 */
+				if (cmpfn == context->partsupfunc[keyno].fn_oid)
+					fmgr_info_copy(&context->stepcmpfuncs[stateidx],
+								   &context->partsupfunc[keyno],
+								   context->ppccontext);
+				else
+					fmgr_info_cxt(cmpfn, &context->stepcmpfuncs[stateidx],
+								  context->ppccontext);
 			}
 
+			values[keyno] = datum;
+			nvalues++;
+
 			lc1 = lnext(lc1);
 			lc2 = lnext(lc2);
 		}
@@ -3392,16 +3460,17 @@ match_boolean_partition_clause(Oid partopfamily, Expr *clause, Expr *partkey,
  * partkey_datum_from_expr
  *		Evaluate expression for potential partition pruning
  *
- * Evaluate 'expr', whose ExprState is stateidx of the context exprstate
- * array; set *value and *isnull to the resulting Datum and nullflag.
- * Return true if evaluation was possible, otherwise false.
+ * Evaluate 'expr'; set *value and *isnull to the resulting Datum and nullflag.
+ *
+ * If expr isn't a Const, its ExprState is in stateidx of the context
+ * exprstate array.
  *
  * Note that the evaluated result may be in the per-tuple memory context of
  * context->planstate->ps_ExprContext, and we may have leaked other memory
  * there too.  This memory must be recovered by resetting that ExprContext
  * after we're done with the pruning operation (see execPartition.c).
  */
-static bool
+static void
 partkey_datum_from_expr(PartitionPruneContext *context,
 						Expr *expr, int stateidx,
 						Datum *value, bool *isnull)
@@ -3413,35 +3482,20 @@ partkey_datum_from_expr(PartitionPruneContext *context,
 
 		*value = con->constvalue;
 		*isnull = con->constisnull;
-		return true;
 	}
 	else
 	{
+		ExprState  *exprstate;
+		ExprContext *ectx;
+
 		/*
 		 * We should never see a non-Const in a step unless we're running in
 		 * the executor.
 		 */
 		Assert(context->planstate != NULL);
 
-		/*
-		 * When called from the executor we'll have a valid planstate so we
-		 * may be able to evaluate an expression which could not be folded to
-		 * a Const during planning.  Since run-time pruning can occur both
-		 * during initialization of the executor or while it's running, we
-		 * must be careful here to evaluate expressions containing PARAM_EXEC
-		 * Params only when told it's OK.
-		 */
-		if (context->evalexecparams || !context->exprhasexecparam[stateidx])
-		{
-			ExprState  *exprstate;
-			ExprContext *ectx;
-
-			exprstate = context->exprstates[stateidx];
-			ectx = context->planstate->ps_ExprContext;
-			*value = ExecEvalExprSwitchContext(exprstate, ectx, isnull);
-			return true;
-		}
+		exprstate = context->exprstates[stateidx];
+		ectx = context->planstate->ps_ExprContext;
+		*value = ExecEvalExprSwitchContext(exprstate, ectx, isnull);
 	}
-
-	return false;
 }
diff --git a/src/include/executor/execPartition.h b/src/include/executor/execPartition.h
index b363aba..465a975 100644
--- a/src/include/executor/execPartition.h
+++ b/src/include/executor/execPartition.h
@@ -62,24 +62,24 @@ typedef struct PartitionRoutingInfo
  * subpart_map					Subpart index by partition index, or -1.
  * present_parts				A Bitmapset of the partition indexes that we
  *								have subplans or subparts for.
- * context						Contains the context details required to call
- *								the partition pruning code.
- * pruning_steps				List of PartitionPruneSteps used to
- *								perform the actual pruning.
- * do_initial_prune				true if pruning should be performed during
- *								executor startup (for this partitioning level).
- * do_exec_prune				true if pruning should be performed during
- *								executor run (for this partitioning level).
+ * initial_pruning_steps		List of PartitionPruneSteps used to
+ *								perform executor startup pruning.
+ * exec_pruning_steps			List of PartitionPruneSteps used to
+ *								perform per-scan pruning.
+ * initial_context				If initial_pruning_steps isn't NIL, contains
+ *								the details needed to execute those steps.
+ * exec_context					If exec_pruning_steps isn't NIL, contains
+ *								the details needed to execute those steps.
  */
 typedef struct PartitionedRelPruningData
 {
 	int		   *subplan_map;
 	int		   *subpart_map;
 	Bitmapset  *present_parts;
-	PartitionPruneContext context;
-	List	   *pruning_steps;
-	bool		do_initial_prune;
-	bool		do_exec_prune;
+	List	   *initial_pruning_steps;
+	List	   *exec_pruning_steps;
+	PartitionPruneContext initial_context;
+	PartitionPruneContext exec_context;
 } PartitionedRelPruningData;
 
 /*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 1cce762..1241245 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1109,21 +1109,23 @@ typedef struct PartitionedRelPruneInfo
 {
 	NodeTag		type;
 	Index		rtindex;		/* RT index of partition rel for this level */
-	List	   *pruning_steps;	/* List of PartitionPruneStep, see below */
 	Bitmapset  *present_parts;	/* Indexes of all partitions which subplans or
-								 * subparts are present for. */
-	int			nparts;			/* Length of subplan_map[] and subpart_map[] */
-	int			nexprs;			/* Length of hasexecparam[] */
+								 * subparts are present for */
+	int			nparts;			/* Length of the following arrays: */
 	int		   *subplan_map;	/* subplan index by partition index, or -1 */
 	int		   *subpart_map;	/* subpart index by partition index, or -1 */
 	Oid		   *relid_map;		/* relation OID by partition index, or 0 */
-	bool	   *hasexecparam;	/* true if corresponding pruning_step contains
-								 * any PARAM_EXEC Params. */
-	bool		do_initial_prune;	/* true if pruning should be performed
-									 * during executor startup. */
-	bool		do_exec_prune;	/* true if pruning should be performed during
-								 * executor run. */
-	Bitmapset  *execparamids;	/* All PARAM_EXEC Param IDs in pruning_steps */
+
+	/*
+	 * initial_pruning_steps shows how to prune during executor startup (i.e.,
+	 * without use of any PARAM_EXEC Params); it is NIL if no startup pruning
+	 * is required.  exec_pruning_steps shows how to prune with PARAM_EXEC
+	 * Params; it is NIL if no per-scan pruning is required.
+	 */
+	List	   *initial_pruning_steps;	/* List of PartitionPruneStep */
+	List	   *exec_pruning_steps; /* List of PartitionPruneStep */
+	Bitmapset  *execparamids;	/* All PARAM_EXEC Param IDs in
+								 * exec_pruning_steps */
 } PartitionedRelPruneInfo;
 
 /*
diff --git a/src/include/partitioning/partprune.h b/src/include/partitioning/partprune.h
index 2f75717..b906ae1 100644
--- a/src/include/partitioning/partprune.h
+++ b/src/include/partitioning/partprune.h
@@ -44,10 +44,6 @@ struct RelOptInfo;
  * exprstates		Array of ExprStates, indexed as per PruneCtxStateIdx; one
  *					for each partition key in each pruning step.  Allocated if
  *					planstate is non-NULL, otherwise NULL.
- * exprhasexecparam	Array of bools, each true if corresponding 'exprstate'
- *					expression contains any PARAM_EXEC Params.  (Can be NULL
- *					if planstate is NULL.)
- * evalexecparams	True if it's safe to evaluate PARAM_EXEC Params.
  */
 typedef struct PartitionPruneContext
 {
@@ -61,8 +57,6 @@ typedef struct PartitionPruneContext
 	MemoryContext ppccontext;
 	PlanState  *planstate;
 	ExprState **exprstates;
-	bool	   *exprhasexecparam;
-	bool		evalexecparams;
 } PartitionPruneContext;
 
 /*
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 723fc70..841bd8b 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -3149,6 +3149,45 @@ select * from mc3p where a < 3 and abs(b) = 1;
          Filter: ((a < 3) AND (abs(b) = 1))
 (7 rows)
 
+--
+-- Check that pruning with composite range partitioning works correctly when
+-- a combination of runtime parameters is specified, not all of whose values
+-- are available at the same time
+--
+set plan_cache_mode = force_generic_plan;
+prepare ps1 as
+  select * from mc3p where a = $1 and abs(b) < (select 3);
+explain (analyze, costs off, summary off, timing off)
+execute ps1(1);
+                   QUERY PLAN                    
+-------------------------------------------------
+ Append (actual rows=1 loops=1)
+   InitPlan 1 (returns $0)
+     ->  Result (actual rows=1 loops=1)
+   Subplans Removed: 2
+   ->  Seq Scan on mc3p1 (actual rows=1 loops=1)
+         Filter: ((a = $1) AND (abs(b) < $0))
+(6 rows)
+
+deallocate ps1;
+prepare ps2 as
+  select * from mc3p where a <= $1 and abs(b) < (select 3);
+explain (analyze, costs off, summary off, timing off)
+execute ps2(1);
+                   QUERY PLAN                    
+-------------------------------------------------
+ Append (actual rows=2 loops=1)
+   InitPlan 1 (returns $0)
+     ->  Result (actual rows=1 loops=1)
+   Subplans Removed: 1
+   ->  Seq Scan on mc3p0 (actual rows=1 loops=1)
+         Filter: ((a <= $1) AND (abs(b) < $0))
+   ->  Seq Scan on mc3p1 (actual rows=1 loops=1)
+         Filter: ((a <= $1) AND (abs(b) < $0))
+(8 rows)
+
+deallocate ps2;
+reset plan_cache_mode;
 drop table mc3p;
 -- Ensure runtime pruning works with initplans params with boolean types
 create table boolvalues (value bool not null);
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 2373bd8..071e28d 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -809,6 +809,24 @@ insert into mc3p values (0, 1, 1), (1, 1, 1), (2, 1, 1);
 explain (analyze, costs off, summary off, timing off)
 select * from mc3p where a < 3 and abs(b) = 1;
 
+--
+-- Check that pruning with composite range partitioning works correctly when
+-- a combination of runtime parameters is specified, not all of whose values
+-- are available at the same time
+--
+set plan_cache_mode = force_generic_plan;
+prepare ps1 as
+  select * from mc3p where a = $1 and abs(b) < (select 3);
+explain (analyze, costs off, summary off, timing off)
+execute ps1(1);
+deallocate ps1;
+prepare ps2 as
+  select * from mc3p where a <= $1 and abs(b) < (select 3);
+explain (analyze, costs off, summary off, timing off)
+execute ps2(1);
+deallocate ps2;
+reset plan_cache_mode;
+
 drop table mc3p;
 
 -- Ensure runtime pruning works with initplans params with boolean types
