From 926b7bef8c2aa05380849e7da1802dd7de743772 Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Wed, 29 Jan 2025 17:51:29 -0500
Subject: [PATCH v1 1/8] Replace CaseTestExpr with a Param during planning.

For some time we've wanted to get rid of CaseTestExpr and replace it
with a less-spongily-defined node type, because we have had bugs
caused by the lack of a clear connection between the CaseTestExpr and
the expression node providing its value.  See for example commits
f0c7b789a and 14a158f9b.  This patch series implements a partial fix:
replace CaseTestExpr with a Param early in planning.

I tried to generate Params in parse analysis and not use CaseTestExpr
ever, but it turns out to be far too hard to keep the Params' IDs
distinct during rule rewriting, subquery pullup, etc.  It doesn't
seem really necessary either: in practice, the transformations that
risk confusion between different CaseTestExpr-using expressions don't
occur before eval_const_expressions.  So as long as we lock down the
association between a CaseExpr and its controlled nodes when
eval_const_expressions first sees the controlled nodes, we'll be okay.

It turns out to be a good idea to invent a new paramkind, here
called PARAM_EXPR, for this purpose.  I first intended to just use
PARAM_EXEC Params, but it's valuable to instead define PARAM_EXEC
as being for values passed between plan nodes while PARAM_EXPR is
for values passed around within a single expression.  In particular
this allows easily treating PARAM_EXPR Params as parallel-safe,
which they should be since we'll never break a single expression
across worker nodes.  The machinations around setParam signaling
should ignore these Params too.

This first patch installs the infrastructure for PARAM_EXPR Params
and causes CaseTestExpr nodes used for CASE to be replaced properly.
The various places that abuse CaseTestExpr for other purposes will
be dealt with in subsequent patches, with the end goal of removing
executor support for CaseTestExpr.
---
 contrib/postgres_fdw/deparse.c        | 191 +++++++++++++++-----------
 src/backend/executor/execExpr.c       |  53 ++++---
 src/backend/executor/execExprInterp.c |  84 ++++++++++-
 src/backend/executor/execMain.c       |  23 +++-
 src/backend/executor/execParallel.c   |   1 +
 src/backend/executor/execUtils.c      |   6 +
 src/backend/jit/llvm/llvmjit_expr.c   |  16 ++-
 src/backend/jit/llvm/llvmjit_types.c  |   4 +-
 src/backend/optimizer/plan/planner.c  |   2 +
 src/backend/optimizer/util/clauses.c  | 144 +++++++++++++++++--
 src/backend/parser/parse_expr.c       |   5 +
 src/backend/utils/adt/ruleutils.c     |  36 +++--
 src/include/executor/execExpr.h       |  27 +++-
 src/include/nodes/execnodes.h         |  20 +--
 src/include/nodes/params.h            |  27 +++-
 src/include/nodes/pathnodes.h         |   3 +
 src/include/nodes/plannodes.h         |   2 +
 src/include/nodes/primnodes.h         |  26 +++-
 src/tools/pgindent/typedefs.list      |   2 +
 19 files changed, 505 insertions(+), 167 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 6e7dc3d2df..7299fbc273 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -91,6 +91,18 @@ typedef struct foreign_loc_cxt
 	FDWCollateState state;		/* state of current collation choice */
 } foreign_loc_cxt;
 
+/*
+ * To locate collation info for the source of a PARAM_EXPR Param, we use a
+ * linked list of these structs.  The sources and Params are strictly nested
+ * in expression trees, so it suffices to keep these on the stack.
+ */
+typedef struct foreign_param_cxt
+{
+	int			paramid;		/* paramid of the Param(s) this matches */
+	foreign_loc_cxt src_cxt;	/* collation info for Param's source expr */
+	struct foreign_param_cxt *next; /* link to next innermost item, if any */
+} foreign_param_cxt;
+
 /*
  * Context for deparseExpr
  */
@@ -119,7 +131,7 @@ typedef struct deparse_expr_cxt
 static bool foreign_expr_walker(Node *node,
 								foreign_glob_cxt *glob_cxt,
 								foreign_loc_cxt *outer_cxt,
-								foreign_loc_cxt *case_arg_cxt);
+								foreign_param_cxt *param_cxt);
 static char *deparse_type_name(Oid type_oid, int32 typemod);
 
 /*
@@ -294,9 +306,8 @@ is_foreign_expr(PlannerInfo *root,
  *
  * In addition, *outer_cxt is updated with collation information.
  *
- * case_arg_cxt is NULL if this subexpression is not inside a CASE-with-arg.
- * Otherwise, it points to the collation info derived from the arg expression,
- * which must be consulted by any CaseTestExpr.
+ * param_cxt points to a chain (initially empty) of foreign_param_cxt nodes
+ * that describe the sources of PARAM_EXPR Params, such as a CASE-with-arg.
  *
  * We must check that the expression contains only node types we can deparse,
  * that all types/functions/operators are safe to send (they are "shippable"),
@@ -310,7 +321,7 @@ static bool
 foreign_expr_walker(Node *node,
 					foreign_glob_cxt *glob_cxt,
 					foreign_loc_cxt *outer_cxt,
-					foreign_loc_cxt *case_arg_cxt)
+					foreign_param_cxt *param_cxt)
 {
 	bool		check_type = true;
 	PgFdwRelationInfo *fpinfo;
@@ -476,6 +487,56 @@ foreign_expr_walker(Node *node,
 			{
 				Param	   *p = (Param *) node;
 
+				collation = p->paramcollid;
+
+				/*
+				 * If it's a PARAM_EXPR Param, try to find the source.  Punt
+				 * if we can't.  (That probably shouldn't happen, since we'd
+				 * not have descended into the current subexpression if we
+				 * didn't recognize the outer node that supplies the Param.)
+				 */
+				if (p->paramkind == PARAM_EXPR)
+				{
+					foreign_param_cxt *pcxt;
+
+					for (pcxt = param_cxt; pcxt != NULL; pcxt = pcxt->next)
+					{
+						if (pcxt->paramid == p->paramid)
+							break;
+					}
+					if (pcxt == NULL)
+						return false;
+
+					/*
+					 * Otherwise, any nondefault collation attached to the
+					 * Param node must be derived from foreign Var(s) in the
+					 * source expression.
+					 */
+					if (collation == InvalidOid)
+						state = FDW_COLLATE_NONE;
+					else if (pcxt->src_cxt.state == FDW_COLLATE_SAFE &&
+							 collation == pcxt->src_cxt.collation)
+						state = FDW_COLLATE_SAFE;
+					else if (collation == DEFAULT_COLLATION_OID)
+						state = FDW_COLLATE_NONE;
+					else
+						state = FDW_COLLATE_UNSAFE;
+				}
+				else if (p->paramkind == PARAM_EXTERN ||
+						 p->paramkind == PARAM_EXEC)
+				{
+					/*
+					 * Param supplied from outside the current plan node.
+					 * Collation rule is same as for Consts and non-foreign
+					 * Vars.
+					 */
+					if (collation == InvalidOid ||
+						collation == DEFAULT_COLLATION_OID)
+						state = FDW_COLLATE_NONE;
+					else
+						state = FDW_COLLATE_UNSAFE;
+				}
+
 				/*
 				 * If it's a MULTIEXPR Param, punt.  We can't tell from here
 				 * whether the referenced sublink/subplan contains any remote
@@ -488,19 +549,11 @@ foreign_expr_walker(Node *node,
 				 * references a sub-select elsewhere in the same targetlist,
 				 * so we'd be on the hook to evaluate it somehow if we wanted
 				 * to handle such cases as direct foreign updates.)
+				 *
+				 * We punt for any unrecognized paramkind, too.
 				 */
-				if (p->paramkind == PARAM_MULTIEXPR)
-					return false;
-
-				/*
-				 * Collation rule is same as for Consts and non-foreign Vars.
-				 */
-				collation = p->paramcollid;
-				if (collation == InvalidOid ||
-					collation == DEFAULT_COLLATION_OID)
-					state = FDW_COLLATE_NONE;
 				else
-					state = FDW_COLLATE_UNSAFE;
+					return false;
 			}
 			break;
 		case T_SubscriptingRef:
@@ -517,17 +570,17 @@ foreign_expr_walker(Node *node,
 				 * result, so do those first and reset inner_cxt afterwards.
 				 */
 				if (!foreign_expr_walker((Node *) sr->refupperindexpr,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
+										 glob_cxt, &inner_cxt, param_cxt))
 					return false;
 				inner_cxt.collation = InvalidOid;
 				inner_cxt.state = FDW_COLLATE_NONE;
 				if (!foreign_expr_walker((Node *) sr->reflowerindexpr,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
+										 glob_cxt, &inner_cxt, param_cxt))
 					return false;
 				inner_cxt.collation = InvalidOid;
 				inner_cxt.state = FDW_COLLATE_NONE;
 				if (!foreign_expr_walker((Node *) sr->refexpr,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
+										 glob_cxt, &inner_cxt, param_cxt))
 					return false;
 
 				/*
@@ -563,7 +616,7 @@ foreign_expr_walker(Node *node,
 				 * Recurse to input subexpressions.
 				 */
 				if (!foreign_expr_walker((Node *) fe->args,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
+										 glob_cxt, &inner_cxt, param_cxt))
 					return false;
 
 				/*
@@ -611,7 +664,7 @@ foreign_expr_walker(Node *node,
 				 * Recurse to input subexpressions.
 				 */
 				if (!foreign_expr_walker((Node *) oe->args,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
+										 glob_cxt, &inner_cxt, param_cxt))
 					return false;
 
 				/*
@@ -651,7 +704,7 @@ foreign_expr_walker(Node *node,
 				 * Recurse to input subexpressions.
 				 */
 				if (!foreign_expr_walker((Node *) oe->args,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
+										 glob_cxt, &inner_cxt, param_cxt))
 					return false;
 
 				/*
@@ -677,7 +730,7 @@ foreign_expr_walker(Node *node,
 				 * Recurse to input subexpression.
 				 */
 				if (!foreign_expr_walker((Node *) r->arg,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
+										 glob_cxt, &inner_cxt, param_cxt))
 					return false;
 
 				/*
@@ -704,7 +757,7 @@ foreign_expr_walker(Node *node,
 				 * Recurse to input subexpressions.
 				 */
 				if (!foreign_expr_walker((Node *) b->args,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
+										 glob_cxt, &inner_cxt, param_cxt))
 					return false;
 
 				/* Output is always boolean and so noncollatable. */
@@ -720,7 +773,7 @@ foreign_expr_walker(Node *node,
 				 * Recurse to input subexpressions.
 				 */
 				if (!foreign_expr_walker((Node *) nt->arg,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
+										 glob_cxt, &inner_cxt, param_cxt))
 					return false;
 
 				/* Output is always boolean and so noncollatable. */
@@ -731,23 +784,29 @@ foreign_expr_walker(Node *node,
 		case T_CaseExpr:
 			{
 				CaseExpr   *ce = (CaseExpr *) node;
-				foreign_loc_cxt arg_cxt;
+				foreign_param_cxt arg_cxt;
+				foreign_param_cxt *when_cxt;
 				foreign_loc_cxt tmp_cxt;
 				ListCell   *lc;
 
 				/*
 				 * Recurse to CASE's arg expression, if any.  Its collation
-				 * has to be saved aside for use while examining CaseTestExprs
-				 * within the WHEN expressions.
+				 * has to be saved aside for use while examining PARAM_EXPR
+				 * Params within the WHEN expressions.
 				 */
-				arg_cxt.collation = InvalidOid;
-				arg_cxt.state = FDW_COLLATE_NONE;
 				if (ce->arg)
 				{
-					if (!foreign_expr_walker((Node *) ce->arg,
-											 glob_cxt, &arg_cxt, case_arg_cxt))
+					arg_cxt.src_cxt.collation = InvalidOid;
+					arg_cxt.src_cxt.state = FDW_COLLATE_NONE;
+					if (!foreign_expr_walker((Node *) ce->arg, glob_cxt,
+											 &arg_cxt.src_cxt, param_cxt))
 						return false;
+					arg_cxt.paramid = ce->caseparam;
+					arg_cxt.next = param_cxt;
+					when_cxt = &arg_cxt;
 				}
+				else
+					when_cxt = param_cxt;
 
 				/* Examine the CaseWhen subexpressions. */
 				foreach(lc, ce->args)
@@ -757,26 +816,29 @@ foreign_expr_walker(Node *node,
 					if (ce->arg)
 					{
 						/*
-						 * In a CASE-with-arg, the parser should have produced
-						 * WHEN clauses of the form "CaseTestExpr = RHS",
-						 * possibly with an implicit coercion inserted above
-						 * the CaseTestExpr.  However in an expression that's
-						 * been through the optimizer, the WHEN clause could
-						 * be almost anything (since the equality operator
-						 * could have been expanded into an inline function).
-						 * In such cases forbid pushdown, because
+						 * In a CASE-with-arg, the planner should have
+						 * produced WHEN clauses of the form "PARAM_EXPR Param
+						 * = RHS", possibly with an implicit coercion inserted
+						 * above the Param.  But after optimization, the WHEN
+						 * clause could be almost anything (since the equality
+						 * operator could have been expanded into an inline
+						 * function).  In such cases forbid pushdown, because
 						 * deparseCaseExpr can't handle it.
 						 */
 						Node	   *whenExpr = (Node *) cw->expr;
 						List	   *opArgs;
+						Node	   *larg;
 
 						if (!IsA(whenExpr, OpExpr))
 							return false;
-
 						opArgs = ((OpExpr *) whenExpr)->args;
-						if (list_length(opArgs) != 2 ||
-							!IsA(strip_implicit_coercions(linitial(opArgs)),
-								 CaseTestExpr))
+						if (list_length(opArgs) != 2)
+							return false;
+						larg = strip_implicit_coercions(linitial(opArgs));
+						if (!IsA(larg, Param))
+							return false;
+						if (((Param *) larg)->paramkind != PARAM_EXPR ||
+							((Param *) larg)->paramid != ce->caseparam)
 							return false;
 					}
 
@@ -788,18 +850,18 @@ foreign_expr_walker(Node *node,
 					tmp_cxt.collation = InvalidOid;
 					tmp_cxt.state = FDW_COLLATE_NONE;
 					if (!foreign_expr_walker((Node *) cw->expr,
-											 glob_cxt, &tmp_cxt, &arg_cxt))
+											 glob_cxt, &tmp_cxt, when_cxt))
 						return false;
 
 					/* Recurse to THEN expression. */
 					if (!foreign_expr_walker((Node *) cw->result,
-											 glob_cxt, &inner_cxt, case_arg_cxt))
+											 glob_cxt, &inner_cxt, param_cxt))
 						return false;
 				}
 
 				/* Recurse to ELSE expression. */
 				if (!foreign_expr_walker((Node *) ce->defresult,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
+										 glob_cxt, &inner_cxt, param_cxt))
 					return false;
 
 				/*
@@ -822,31 +884,6 @@ foreign_expr_walker(Node *node,
 					state = FDW_COLLATE_UNSAFE;
 			}
 			break;
-		case T_CaseTestExpr:
-			{
-				CaseTestExpr *c = (CaseTestExpr *) node;
-
-				/* Punt if we seem not to be inside a CASE arg WHEN. */
-				if (!case_arg_cxt)
-					return false;
-
-				/*
-				 * Otherwise, any nondefault collation attached to the
-				 * CaseTestExpr node must be derived from foreign Var(s) in
-				 * the CASE arg.
-				 */
-				collation = c->collation;
-				if (collation == InvalidOid)
-					state = FDW_COLLATE_NONE;
-				else if (case_arg_cxt->state == FDW_COLLATE_SAFE &&
-						 collation == case_arg_cxt->collation)
-					state = FDW_COLLATE_SAFE;
-				else if (collation == DEFAULT_COLLATION_OID)
-					state = FDW_COLLATE_NONE;
-				else
-					state = FDW_COLLATE_UNSAFE;
-			}
-			break;
 		case T_ArrayExpr:
 			{
 				ArrayExpr  *a = (ArrayExpr *) node;
@@ -855,7 +892,7 @@ foreign_expr_walker(Node *node,
 				 * Recurse to input subexpressions.
 				 */
 				if (!foreign_expr_walker((Node *) a->elements,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
+										 glob_cxt, &inner_cxt, param_cxt))
 					return false;
 
 				/*
@@ -885,7 +922,7 @@ foreign_expr_walker(Node *node,
 				foreach(lc, l)
 				{
 					if (!foreign_expr_walker((Node *) lfirst(lc),
-											 glob_cxt, &inner_cxt, case_arg_cxt))
+											 glob_cxt, &inner_cxt, param_cxt))
 						return false;
 				}
 
@@ -935,7 +972,7 @@ foreign_expr_walker(Node *node,
 					}
 
 					if (!foreign_expr_walker(n,
-											 glob_cxt, &inner_cxt, case_arg_cxt))
+											 glob_cxt, &inner_cxt, param_cxt))
 						return false;
 				}
 
@@ -968,7 +1005,7 @@ foreign_expr_walker(Node *node,
 
 				/* Check aggregate filter */
 				if (!foreign_expr_walker((Node *) agg->aggfilter,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
+										 glob_cxt, &inner_cxt, param_cxt))
 					return false;
 
 				/*
@@ -3601,7 +3638,7 @@ deparseCaseExpr(CaseExpr *node, deparse_expr_cxt *context)
 			deparseExpr(whenclause->expr, context);
 		else					/* CASE arg WHEN */
 		{
-			/* Ignore the CaseTestExpr and equality operator. */
+			/* Ignore the Param and equality operator. */
 			deparseExpr(lsecond(castNode(OpExpr, whenclause->expr)->args),
 						context);
 		}
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 8f28da4bf9..2a66363502 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1041,6 +1041,12 @@ ExecInitExprRec(Expr *node, ExprState *state,
 						scratch.d.param.paramtype = param->paramtype;
 						ExprEvalPushStep(state, &scratch);
 						break;
+					case PARAM_EXPR:
+						scratch.opcode = EEOP_PARAM_EXPR;
+						scratch.d.param.paramid = param->paramid;
+						scratch.d.param.paramtype = param->paramtype;
+						ExprEvalPushStep(state, &scratch);
+						break;
 					case PARAM_EXTERN:
 
 						/*
@@ -1771,23 +1777,17 @@ ExecInitExprRec(Expr *node, ExprState *state,
 			{
 				CaseExpr   *caseExpr = (CaseExpr *) node;
 				List	   *adjust_jumps = NIL;
-				Datum	   *caseval = NULL;
-				bool	   *casenull = NULL;
 				ListCell   *lc;
 
 				/*
 				 * If there's a test expression, we have to evaluate it and
-				 * save the value where the CaseTestExpr placeholders can find
-				 * it.
+				 * save the value where the PARAM_EXPR Params can find it.
 				 */
 				if (caseExpr->arg != NULL)
 				{
-					/* Evaluate testexpr into caseval/casenull workspace */
-					caseval = palloc(sizeof(Datum));
-					casenull = palloc(sizeof(bool));
-
+					/* Evaluate testexpr into CASE's result variables */
 					ExecInitExprRec(caseExpr->arg, state,
-									caseval, casenull);
+									resv, resnull);
 
 					/*
 					 * Since value might be read multiple times, force to R/O
@@ -1795,17 +1795,20 @@ ExecInitExprRec(Expr *node, ExprState *state,
 					 */
 					if (get_typlen(exprType((Node *) caseExpr->arg)) == -1)
 					{
-						/* change caseval in-place */
+						/* change testexpr value in-place */
 						scratch.opcode = EEOP_MAKE_READONLY;
-						scratch.resvalue = caseval;
-						scratch.resnull = casenull;
-						scratch.d.make_readonly.value = caseval;
-						scratch.d.make_readonly.isnull = casenull;
+						scratch.d.make_readonly.value = resv;
+						scratch.d.make_readonly.isnull = resnull;
 						ExprEvalPushStep(state, &scratch);
-						/* restore normal settings of scratch fields */
-						scratch.resvalue = resv;
-						scratch.resnull = resnull;
 					}
+
+					/* Now copy value into the appropriate Param slot */
+					Assert(caseExpr->caseparam > 0);
+					scratch.opcode = EEOP_PARAM_SET_EXPR;
+					scratch.d.paramset.paramid = caseExpr->caseparam;
+					scratch.d.paramset.setvalue = resv;
+					scratch.d.paramset.setnull = resnull;
+					ExprEvalPushStep(state, &scratch);
 				}
 
 				/*
@@ -1822,19 +1825,13 @@ ExecInitExprRec(Expr *node, ExprState *state,
 					int			whenstep;
 
 					/*
-					 * Make testexpr result available to CaseTestExpr nodes
-					 * within the condition.  We must save and restore prior
-					 * setting of innermost_caseval fields, in case this node
-					 * is itself within a larger CASE.
-					 *
-					 * If there's no test expression, we don't actually need
-					 * to save and restore these fields; but it's less code to
-					 * just do so unconditionally.
+					 * XXX innermost_caseval/casenull will go away, but for
+					 * now just make them NULL.
 					 */
 					save_innermost_caseval = state->innermost_caseval;
 					save_innermost_casenull = state->innermost_casenull;
-					state->innermost_caseval = caseval;
-					state->innermost_casenull = casenull;
+					state->innermost_caseval = NULL;
+					state->innermost_casenull = NULL;
 
 					/* evaluate condition into CASE's result variables */
 					ExecInitExprRec(when->expr, state, resv, resnull);
@@ -2836,7 +2833,7 @@ ExecInitSubPlanExpr(SubPlan *subplan,
 		ExecInitExprRec(arg, state,
 						&state->resvalue, &state->resnull);
 
-		scratch.opcode = EEOP_PARAM_SET;
+		scratch.opcode = EEOP_PARAM_SET_EXEC;
 		scratch.d.param.paramid = paramid;
 		/* paramtype's not actually used, but we might as well fill it */
 		scratch.d.param.paramtype = exprType((Node *) arg);
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 1127e6f11e..fa3e76eccd 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -520,9 +520,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 		&&CASE_EEOP_BOOLTEST_IS_FALSE,
 		&&CASE_EEOP_BOOLTEST_IS_NOT_FALSE,
 		&&CASE_EEOP_PARAM_EXEC,
+		&&CASE_EEOP_PARAM_EXPR,
 		&&CASE_EEOP_PARAM_EXTERN,
 		&&CASE_EEOP_PARAM_CALLBACK,
-		&&CASE_EEOP_PARAM_SET,
+		&&CASE_EEOP_PARAM_SET_EXEC,
+		&&CASE_EEOP_PARAM_SET_EXPR,
 		&&CASE_EEOP_CASE_TESTVAL,
 		&&CASE_EEOP_MAKE_READONLY,
 		&&CASE_EEOP_IOCOERCE,
@@ -1250,6 +1252,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();
 		}
 
+		EEO_CASE(EEOP_PARAM_EXPR)
+		{
+			/* out of line implementation (XXX should make it inline) */
+			ExecEvalParamExpr(state, op, econtext);
+
+			EEO_NEXT();
+		}
+
 		EEO_CASE(EEOP_PARAM_EXTERN)
 		{
 			/* out of line implementation: too large */
@@ -1264,10 +1274,17 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();
 		}
 
-		EEO_CASE(EEOP_PARAM_SET)
+		EEO_CASE(EEOP_PARAM_SET_EXEC)
 		{
 			/* out of line, unlikely to matter performance-wise */
-			ExecEvalParamSet(state, op, econtext);
+			ExecEvalParamSetExec(state, op, econtext);
+			EEO_NEXT();
+		}
+
+		EEO_CASE(EEOP_PARAM_SET_EXPR)
+		{
+			/* out of line implementation: too large */
+			ExecEvalParamSetExpr(state, op, econtext);
 			EEO_NEXT();
 		}
 
@@ -2975,6 +2992,7 @@ ExecEvalParamExec(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 {
 	ParamExecData *prm;
 
+	/* paramid is zero-based */
 	prm = &(econtext->ecxt_param_exec_vals[op->d.param.paramid]);
 	if (unlikely(prm->execPlan != NULL))
 	{
@@ -2987,6 +3005,24 @@ ExecEvalParamExec(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 	*op->resnull = prm->isnull;
 }
 
+/*
+ * Evaluate a PARAM_EXPR parameter.
+ *
+ * PARAM_EXPR params (internal executor parameters) are stored in the
+ * ecxt_param_expr_vals array, and can be accessed by array index.
+ */
+void
+ExecEvalParamExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
+{
+	ParamExprData *prm;
+
+	/* paramid is one-based */
+	Assert(op->d.param.paramid <= econtext->ecxt_num_param_expr_vals);
+	prm = &(econtext->ecxt_param_expr_vals[op->d.param.paramid - 1]);
+	*op->resvalue = prm->value;
+	*op->resnull = prm->isnull;
+}
+
 /*
  * Evaluate a PARAM_EXTERN parameter.
  *
@@ -3032,11 +3068,10 @@ ExecEvalParamExtern(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 }
 
 /*
- * Set value of a param (currently always PARAM_EXEC) from
- * state->res{value,null}.
+ * Set value of a PARAM_EXEC Param from state->res{value,null}.
  */
 void
-ExecEvalParamSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
+ExecEvalParamSetExec(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 {
 	ParamExecData *prm;
 
@@ -3049,6 +3084,43 @@ ExecEvalParamSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 	prm->isnull = state->resnull;
 }
 
+/*
+ * Set value of a PARAM_EXPR Param from *op->paramset.set{value,null}.
+ */
+void
+ExecEvalParamSetExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
+{
+	ParamExprData *prm;
+
+	/*
+	 * In normal query execution, the ecxt_param_expr_vals array should be
+	 * large enough already.  However, in a standalone expression we may have
+	 * to resize it larger, since there is no good place to initialize the
+	 * array for that case.
+	 */
+	if (unlikely(op->d.paramset.paramid > econtext->ecxt_num_param_expr_vals))
+	{
+		MemoryContext oldcontext;
+
+		/* Be sure to put it in the same context as econtext itself */
+		oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
+		if (econtext->ecxt_param_expr_vals == NULL)
+			econtext->ecxt_param_expr_vals =
+				palloc0_array(ParamExprData, op->d.paramset.paramid);
+		else
+			econtext->ecxt_param_expr_vals =
+				repalloc0_array(econtext->ecxt_param_expr_vals,
+								ParamExprData,
+								econtext->ecxt_num_param_expr_vals,
+								op->d.paramset.paramid);
+		econtext->ecxt_num_param_expr_vals = op->d.paramset.paramid;
+		MemoryContextSwitchTo(oldcontext);
+	}
+	prm = &(econtext->ecxt_param_expr_vals[op->d.paramset.paramid - 1]);
+	prm->value = *op->d.paramset.setvalue;
+	prm->isnull = *op->d.paramset.setnull;
+}
+
 /*
  * Evaluate a CoerceViaIO node in soft-error mode.
  *
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index fb8dba3ab2..552a852e7e 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -185,10 +185,14 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 		int			nParamExec;
 
 		nParamExec = list_length(queryDesc->plannedstmt->paramExecTypes);
-		estate->es_param_exec_vals = (ParamExecData *)
-			palloc0(nParamExec * sizeof(ParamExecData));
+		estate->es_param_exec_vals = palloc0_array(ParamExecData, nParamExec);
 	}
 
+	estate->es_num_param_expr_vals = queryDesc->plannedstmt->numExprParams;
+	if (estate->es_num_param_expr_vals > 0)
+		estate->es_param_expr_vals =
+			palloc0_array(ParamExprData, estate->es_num_param_expr_vals);
+
 	/* We now require all callers to provide sourceText */
 	Assert(queryDesc->sourceText != NULL);
 	estate->es_sourceText = queryDesc->sourceText;
@@ -2875,9 +2879,10 @@ EvalPlanQualStart(EPQState *epqstate, Plan *planTree)
 
 	/*
 	 * The external param list is simply shared from parent.  The internal
-	 * param workspace has to be local state, but we copy the initial values
-	 * from the parent, so as to have access to any param values that were
-	 * already set from other parts of the parent's plan tree.
+	 * param workspaces have to be local state, but we copy the initial values
+	 * for PARAM_EXEC Params from the parent, so as to have access to any
+	 * param values that were already set from other parts of the parent's
+	 * plan tree.
 	 */
 	rcestate->es_param_list_info = parentestate->es_param_list_info;
 	if (parentestate->es_plannedstmt->paramExecTypes != NIL)
@@ -2907,8 +2912,7 @@ EvalPlanQualStart(EPQState *epqstate, Plan *planTree)
 
 		/* now make the internal param workspace ... */
 		i = list_length(parentestate->es_plannedstmt->paramExecTypes);
-		rcestate->es_param_exec_vals = (ParamExecData *)
-			palloc0(i * sizeof(ParamExecData));
+		rcestate->es_param_exec_vals = palloc0_array(ParamExecData, i);
 		/* ... and copy down all values, whether really needed or not */
 		while (--i >= 0)
 		{
@@ -2920,6 +2924,11 @@ EvalPlanQualStart(EPQState *epqstate, Plan *planTree)
 		}
 	}
 
+	rcestate->es_num_param_expr_vals = parentestate->es_num_param_expr_vals;
+	if (rcestate->es_num_param_expr_vals > 0)
+		rcestate->es_param_expr_vals =
+			palloc0_array(ParamExprData, rcestate->es_num_param_expr_vals);
+
 	/*
 	 * Initialize private state information for each SubPlan.  We must do this
 	 * before running ExecInitNode on the main query tree, since
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index ff4d9dd1bb..7335cafc0a 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -208,6 +208,7 @@ ExecSerializePlan(Plan *plan, EState *estate)
 	pstmt->relationOids = NIL;
 	pstmt->invalItems = NIL;	/* workers can't replan anyway... */
 	pstmt->paramExecTypes = estate->es_plannedstmt->paramExecTypes;
+	pstmt->numExprParams = estate->es_plannedstmt->numExprParams;
 	pstmt->utilityStmt = NULL;
 	pstmt->stmt_location = -1;
 	pstmt->stmt_len = -1;
diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index 7c539de5cf..9a236c3789 100644
--- a/src/backend/executor/execUtils.c
+++ b/src/backend/executor/execUtils.c
@@ -133,6 +133,8 @@ CreateExecutorState(void)
 
 	estate->es_param_list_info = NULL;
 	estate->es_param_exec_vals = NULL;
+	estate->es_param_expr_vals = NULL;
+	estate->es_num_param_expr_vals = 0;
 
 	estate->es_queryEnv = NULL;
 
@@ -262,6 +264,8 @@ CreateExprContextInternal(EState *estate, Size minContextSize,
 							  maxBlockSize);
 
 	econtext->ecxt_param_exec_vals = estate->es_param_exec_vals;
+	econtext->ecxt_param_expr_vals = estate->es_param_expr_vals;
+	econtext->ecxt_num_param_expr_vals = estate->es_num_param_expr_vals;
 	econtext->ecxt_param_list_info = estate->es_param_list_info;
 
 	econtext->ecxt_aggvalues = NULL;
@@ -377,6 +381,8 @@ CreateStandaloneExprContext(void)
 							  ALLOCSET_DEFAULT_SIZES);
 
 	econtext->ecxt_param_exec_vals = NULL;
+	econtext->ecxt_param_expr_vals = NULL;
+	econtext->ecxt_num_param_expr_vals = 0;
 	econtext->ecxt_param_list_info = NULL;
 
 	econtext->ecxt_aggvalues = NULL;
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index c1cf34f103..1b59c30b6f 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1182,6 +1182,12 @@ llvm_compile_expr(ExprState *state)
 				LLVMBuildBr(b, opblocks[opno + 1]);
 				break;
 
+			case EEOP_PARAM_EXPR:
+				build_EvalXFunc(b, mod, "ExecEvalParamExpr",
+								v_state, op, v_econtext);
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
+
 			case EEOP_PARAM_EXTERN:
 				build_EvalXFunc(b, mod, "ExecEvalParamExtern",
 								v_state, op, v_econtext);
@@ -1208,8 +1214,14 @@ llvm_compile_expr(ExprState *state)
 					break;
 				}
 
-			case EEOP_PARAM_SET:
-				build_EvalXFunc(b, mod, "ExecEvalParamSet",
+			case EEOP_PARAM_SET_EXEC:
+				build_EvalXFunc(b, mod, "ExecEvalParamSetExec",
+								v_state, op, v_econtext);
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
+
+			case EEOP_PARAM_SET_EXPR:
+				build_EvalXFunc(b, mod, "ExecEvalParamSetExpr",
 								v_state, op, v_econtext);
 				LLVMBuildBr(b, opblocks[opno + 1]);
 				break;
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
index dbe0282e98..7c2705efa4 100644
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -157,8 +157,10 @@ void	   *referenced_functions[] =
 	ExecEvalMinMax,
 	ExecEvalNextValueExpr,
 	ExecEvalParamExec,
+	ExecEvalParamExpr,
 	ExecEvalParamExtern,
-	ExecEvalParamSet,
+	ExecEvalParamSetExec,
+	ExecEvalParamSetExpr,
 	ExecEvalRow,
 	ExecEvalRowNotNull,
 	ExecEvalRowNull,
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 6803edd085..85a98a065d 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -328,6 +328,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->relationOids = NIL;
 	glob->invalItems = NIL;
 	glob->paramExecTypes = NIL;
+	glob->numExprParams = 0;
 	glob->lastPHId = 0;
 	glob->lastRowMarkId = 0;
 	glob->lastPlanNodeId = 0;
@@ -565,6 +566,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->relationOids = glob->relationOids;
 	result->invalItems = glob->invalItems;
 	result->paramExecTypes = glob->paramExecTypes;
+	result->numExprParams = glob->numExprParams;
 	/* utilityStmt should be null, but we might as well copy it */
 	result->utilityStmt = parse->utilityStmt;
 	result->stmt_location = parse->stmt_location;
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 43dfecfb47..b1ffe3c4f7 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -62,6 +62,7 @@ typedef struct
 	PlannerInfo *root;
 	List	   *active_fns;
 	Node	   *case_val;
+	int			numExprParams;
 	bool		estimate;
 } eval_const_expressions_context;
 
@@ -149,6 +150,9 @@ static Node *substitute_actual_parameters(Node *expr, int nargs, List *args,
 static Node *substitute_actual_parameters_mutator(Node *node,
 												  substitute_actual_parameters_context *context);
 static void sql_inline_error_callback(void *arg);
+static Param *generate_new_expr_param(eval_const_expressions_context *context,
+									  Oid paramtype, int32 paramtypmod,
+									  Oid paramcollation);
 static Query *substitute_actual_srf_parameters(Query *expr,
 											   int nargs, List *args);
 static Node *substitute_actual_srf_parameters_mutator(Node *node,
@@ -921,17 +925,25 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
 	}
 
 	/*
-	 * We can't pass Params to workers at the moment either, so they are also
-	 * parallel-restricted, unless they are PARAM_EXTERN Params or are
-	 * PARAM_EXEC Params listed in safe_param_ids, meaning they could be
-	 * either generated within workers or can be computed by the leader and
-	 * then their value can be passed to workers.
+	 * Params are parallel-restricted except in these cases:
+	 *
+	 * PARAM_EXTERN Params are made available to workers by copying the
+	 * executor's ParamListInfo to workers, so they're parallel-safe.
+	 *
+	 * PARAM_EXPR Params are assumed to be supplied from somewhere higher in
+	 * the same expression.  Since we don't parallelize at the subexpression
+	 * level, these can be considered parallel-safe.
+	 *
+	 * PARAM_EXEC Params are safe if listed in safe_param_ids, meaning they
+	 * could be either generated within workers or can be computed by the
+	 * leader and then their value can be passed to workers.
 	 */
 	else if (IsA(node, Param))
 	{
 		Param	   *param = (Param *) node;
 
-		if (param->paramkind == PARAM_EXTERN)
+		if (param->paramkind == PARAM_EXTERN ||
+			param->paramkind == PARAM_EXPR)
 			return false;
 
 		if (param->paramkind != PARAM_EXEC ||
@@ -2236,6 +2248,15 @@ rowtype_field_matches(Oid rowtypeid, int fieldnum,
  * root->glob->invalItems.  This ensures the plan is known to depend on
  * such functions, even though they aren't referenced anymore.
  *
+ * This function also performs certain essential pre-processing on
+ * expression trees, including insertion of function default arguments,
+ * conversion of named-argument calls to positional notation, and
+ * replacement of CaseTestExpr nodes with suitable Params.  Because of
+ * that, this *must* be invoked on all expressions, even when there
+ * might not be much value in the constant-folding aspect.  It'd be
+ * cleaner to do those things separately, but the cost of an additional
+ * mutation pass seems unattractive.
+ *
  * We assume that the tree has already been type-checked and contains
  * only operators and functions that are reasonable to try to execute.
  *
@@ -2244,10 +2265,6 @@ rowtype_field_matches(Oid rowtypeid, int fieldnum,
  *
  * NOTE: the planner assumes that this will always flatten nested AND and
  * OR clauses into N-argument form.  See comments in prepqual.c.
- *
- * NOTE: another critical effect is that any function calls that require
- * default arguments will be expanded, and named-argument calls will be
- * converted to positional notation.  The executor won't handle either.
  *--------------------
  */
 Node *
@@ -2262,6 +2279,7 @@ eval_const_expressions(PlannerInfo *root, Node *node)
 	context.root = root;		/* for inlined-function dependencies */
 	context.active_fns = NIL;	/* nothing being recursively simplified */
 	context.case_val = NULL;	/* no CASE being examined */
+	context.numExprParams = 0;	/* used only if no root */
 	context.estimate = false;	/* safe transformations only */
 	return eval_const_expressions_mutator(node, &context);
 }
@@ -2389,6 +2407,10 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context)
  *	  value of the Param.
  * 2. Fold stable, as well as immutable, functions to constants.
  * 3. Reduce PlaceHolderVar nodes to their contained expressions.
+ *
+ * A step that is *not* taken in this mode is replacement of CaseTestExpr
+ * nodes with Params.  Doing so would just result in useless consumption
+ * of PARAM_EXPR slots, since the result will never be executed.
  *--------------------
  */
 Node *
@@ -2401,6 +2423,7 @@ estimate_expression_value(PlannerInfo *root, Node *node)
 	context.root = NULL;
 	context.active_fns = NIL;	/* nothing being recursively simplified */
 	context.case_val = NULL;	/* no CASE being examined */
+	context.numExprParams = 0;	/* not used in this path */
 	context.estimate = true;	/* unsafe transformations OK */
 	return eval_const_expressions_mutator(node, &context);
 }
@@ -3146,6 +3169,13 @@ eval_const_expressions_mutator(Node *node,
 				 * expression when executing the CASE, since any contained
 				 * CaseTestExprs that might have referred to it will have been
 				 * replaced by the constant.
+				 *
+				 * If the test expression doesn't reduce to a constant, then
+				 * contained CaseTestExpr placeholders need to be replaced
+				 * with PARAM_EXPR Params instead.  This ensures they will
+				 * continue to be correctly associated with this CaseExpr
+				 * even in the face of subsequent slicing-and-dicing of the
+				 * expression tree.
 				 *----------
 				 */
 				CaseExpr   *caseexpr = (CaseExpr *) node;
@@ -3155,19 +3185,54 @@ eval_const_expressions_mutator(Node *node,
 				List	   *newargs;
 				bool		const_true_cond;
 				Node	   *defresult = NULL;
+				int			case_param = 0;
 				ListCell   *arg;
 
 				/* Simplify the test expression, if any */
 				newarg = eval_const_expressions_mutator((Node *) caseexpr->arg,
 														context);
 
-				/* Set up for contained CaseTestExpr nodes */
+				/*
+				 * Set up the node to substitute for contained CaseTestExpr
+				 * nodes.  If we reduced the arg to a Const we can always
+				 * replace CaseTestExprs with that.  Otherwise, generate a
+				 * PARAM_EXPR Param; or if we are just estimating, leave the
+				 * CaseTestExprs alone.
+				 *
+				 * We must save and restore context->case_val so as not to
+				 * affect surrounding constructs.
+				 */
 				save_case_val = context->case_val;
-				if (newarg && IsA(newarg, Const))
+
+				if (caseexpr->caseparam > 0)
+				{
+					/*
+					 * We get here if asked to re-simplify an
+					 * already-simplified CASE.  We must not generate a new
+					 * Param, or we'd be out of sync with the already-replaced
+					 * CaseTestExprs.
+					 */
+					case_param = caseexpr->caseparam;
+					context->case_val = NULL;	/* no CaseTestExprs expected */
+				}
+				else if (newarg == NULL)
+					context->case_val = NULL;	/* no CaseTestExprs expected */
+				else if (IsA(newarg, Const))
 				{
 					context->case_val = newarg;
 					newarg = NULL;	/* not needed anymore, see above */
 				}
+				else if (!context->estimate)
+				{
+					Param	   *p;
+
+					p = generate_new_expr_param(context,
+												exprType(newarg),
+												exprTypmod(newarg),
+												exprCollation(newarg));
+					context->case_val = (Node *) p;
+					case_param = p->paramid;
+				}
 				else
 					context->case_val = NULL;
 
@@ -3245,15 +3310,25 @@ eval_const_expressions_mutator(Node *node,
 				newcase->arg = (Expr *) newarg;
 				newcase->args = newargs;
 				newcase->defresult = (Expr *) defresult;
+				newcase->caseparam = case_param;
 				newcase->location = caseexpr->location;
 				return (Node *) newcase;
 			}
 		case T_CaseTestExpr:
 			{
 				/*
-				 * If we know a constant test value for the current CASE
+				 * If we have a replacement node for the current CASE
 				 * construct, substitute it for the placeholder.  Else just
 				 * return the placeholder as-is.
+				 *
+				 * Note: this behavior is subtler than it looks.  We can rely
+				 * on context->case_val to be from the matching CASE construct
+				 * because we are dealing with an expression as it was created
+				 * by the parser.  Subsequent processing, such as inlining of
+				 * SQL-language functions, might result in insertion of CASE
+				 * or other CaseTestExpr-abusing constructs between here and
+				 * there --- but it will no longer matter once we've replaced
+				 * the CaseTestExpr with a numbered Param.
 				 */
 				if (context->case_val)
 					return copyObject(context->case_val);
@@ -4928,6 +5003,7 @@ substitute_actual_parameters_mutator(Node *node,
 	{
 		Param	   *param = (Param *) node;
 
+		/* Only EXTERN Params should appear in the tree as yet */
 		if (param->paramkind != PARAM_EXTERN)
 			elog(ERROR, "unexpected paramkind: %d", (int) param->paramkind);
 		if (param->paramid <= 0 || param->paramid > context->nargs)
@@ -4964,6 +5040,48 @@ sql_inline_error_callback(void *arg)
 	errcontext("SQL function \"%s\" during inlining", callback_arg->proname);
 }
 
+/*
+ * Generate a new PARAM_EXPR Param node that will not conflict with any other.
+ *
+ * Possibly this should be in paramassign.c, but then it couldn't rely on
+ * struct eval_const_expressions_context, which would make for a much more
+ * awkward API.  Currently there seems no need to allocate PARAM_EXPR Params
+ * anywhere except within eval_const_expressions.
+ */
+static Param *
+generate_new_expr_param(eval_const_expressions_context *context,
+						Oid paramtype, int32 paramtypmod, Oid paramcollation)
+{
+	Param	   *retval;
+	int			paramid;
+
+	/*
+	 * If we're doing normal planning, use root->glob->numExprParams to count
+	 * PARAM_EXPR Params, so that we allocate them uniquely across the entire
+	 * plan tree.  This is extremely conservative, as in most cases Params in
+	 * different subexpressions won't have overlapping lifetimes and could
+	 * share a ParamExprData slot.  But the slots are small, so trying to
+	 * re-use them seems more dangerous than it's worth.
+	 *
+	 * When processing a standalone expression, use context->numExprParams,
+	 * meaning that the assignments are unique only within the expression.
+	 */
+	if (context->root)
+		paramid = ++context->root->glob->numExprParams;
+	else
+		paramid = ++context->numExprParams;
+
+	retval = makeNode(Param);
+	retval->paramkind = PARAM_EXPR;
+	retval->paramid = paramid;
+	retval->paramtype = paramtype;
+	retval->paramtypmod = paramtypmod;
+	retval->paramcollid = paramcollation;
+	retval->location = -1;
+
+	return retval;
+}
+
 /*
  * evaluate_expr: pre-evaluate a constant expression
  *
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index bad1df732e..a17b8a5797 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1667,6 +1667,11 @@ transformCaseExpr(ParseState *pstate, CaseExpr *c)
 		placeholder->typeId = exprType(arg);
 		placeholder->typeMod = exprTypmod(arg);
 		placeholder->collation = exprCollation(arg);
+
+		/*
+		 * Note: the planner will replace the CaseTestExpr(s) with PARAM_EXPR
+		 * Params, and will fill the CaseExpr's caseparam field at that time.
+		 */
 	}
 	else
 		placeholder = NULL;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 54dad97555..59a4cde467 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -8744,10 +8744,11 @@ get_parameter(Param *param, deparse_context *context)
 	/*
 	 * Not PARAM_EXEC, or couldn't find referent: just print $N.
 	 *
-	 * It's a bug if we get here for anything except PARAM_EXTERN Params, but
-	 * in production builds printing $N seems more useful than failing.
+	 * It's a bug if we get here for anything except PARAM_EXTERN or
+	 * PARAM_EXPR Params, but in production builds printing $N seems more
+	 * useful than failing.
 	 */
-	Assert(param->paramkind == PARAM_EXTERN);
+	Assert(param->paramkind == PARAM_EXTERN || param->paramkind == PARAM_EXPR);
 
 	appendStringInfo(context->buf, "$%d", param->paramid);
 }
@@ -9748,21 +9749,30 @@ get_rule_expr(Node *node, deparse_context *context,
 						 * form "CaseTestExpr = RHS", possibly with an
 						 * implicit coercion inserted above the CaseTestExpr.
 						 * For accurate decompilation of rules it's essential
-						 * that we show just the RHS.  However in an
-						 * expression that's been through the optimizer, the
-						 * WHEN clause could be almost anything (since the
-						 * equality operator could have been expanded into an
-						 * inline function).  If we don't recognize the form
-						 * of the WHEN clause, just punt and display it as-is.
+						 * that we show just the RHS.  In an expression that's
+						 * been through the optimizer, the WHEN clause would
+						 * typically look like "PARAM_EXPR Param = RHS", and
+						 * we prefer to show just the RHS.  However, an
+						 * optimized WHEN clause could be almost anything
+						 * (since the equality operator could have been
+						 * expanded into an inline function).  If we don't
+						 * recognize the form of the WHEN clause, just punt
+						 * and display it as-is.
 						 */
 						if (IsA(w, OpExpr))
 						{
 							List	   *args = ((OpExpr *) w)->args;
 
-							if (list_length(args) == 2 &&
-								IsA(strip_implicit_coercions(linitial(args)),
-									CaseTestExpr))
-								w = (Node *) lsecond(args);
+							if (list_length(args) == 2)
+							{
+								Node	   *larg = strip_implicit_coercions(linitial(args));
+
+								if (IsA(larg, CaseTestExpr) ||
+									(IsA(larg, Param) &&
+									 ((Param *) larg)->paramkind == PARAM_EXPR &&
+									 ((Param *) larg)->paramid == caseexpr->caseparam))
+									w = (Node *) lsecond(args);
+							}
 						}
 					}
 
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index 5371e344ec..e03045de92 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -164,12 +164,15 @@ typedef enum ExprEvalOp
 	EEOP_BOOLTEST_IS_FALSE,
 	EEOP_BOOLTEST_IS_NOT_FALSE,
 
-	/* evaluate PARAM_EXEC/EXTERN parameters */
+	/* evaluate PARAM_EXEC/EXPR/EXTERN parameters */
 	EEOP_PARAM_EXEC,
+	EEOP_PARAM_EXPR,
 	EEOP_PARAM_EXTERN,
 	EEOP_PARAM_CALLBACK,
-	/* set PARAM_EXEC value */
-	EEOP_PARAM_SET,
+
+	/* set PARAM_EXEC/EXPR values */
+	EEOP_PARAM_SET_EXEC,
+	EEOP_PARAM_SET_EXPR,
 
 	/* return CaseTestExpr value */
 	EEOP_CASE_TESTVAL,
@@ -411,13 +414,21 @@ typedef struct ExprEvalStep
 			ExprEvalRowtypeCache rowcache;
 		}			nulltest_row;
 
-		/* for EEOP_PARAM_EXEC/EXTERN and EEOP_PARAM_SET */
+		/* for EEOP_PARAM_EXEC/EXPR/EXTERN and EEOP_PARAM_SET_EXEC */
 		struct
 		{
 			int			paramid;	/* numeric ID for parameter */
 			Oid			paramtype;	/* OID of parameter's datatype */
 		}			param;
 
+		/* for EEOP_PARAM_SET_EXPR */
+		struct
+		{
+			int			paramid;	/* numeric ID for parameter */
+			Datum	   *setvalue;	/* source of value */
+			bool	   *setnull;
+		}			paramset;
+
 		/* for EEOP_PARAM_CALLBACK */
 		struct
 		{
@@ -845,10 +856,14 @@ extern void ExecEvalFuncExprStrictFusage(ExprState *state, ExprEvalStep *op,
 										 ExprContext *econtext);
 extern void ExecEvalParamExec(ExprState *state, ExprEvalStep *op,
 							  ExprContext *econtext);
-extern void ExecEvalParamSet(ExprState *state, ExprEvalStep *op,
-							 ExprContext *econtext);
+extern void ExecEvalParamExpr(ExprState *state, ExprEvalStep *op,
+							  ExprContext *econtext);
 extern void ExecEvalParamExtern(ExprState *state, ExprEvalStep *op,
 								ExprContext *econtext);
+extern void ExecEvalParamSetExec(ExprState *state, ExprEvalStep *op,
+								 ExprContext *econtext);
+extern void ExecEvalParamSetExpr(ExprState *state, ExprEvalStep *op,
+								 ExprContext *econtext);
 extern void ExecEvalCoerceViaIOSafe(ExprState *state, ExprEvalStep *op);
 extern void ExecEvalSQLValueFunction(ExprState *state, ExprEvalStep *op);
 extern void ExecEvalCurrentOfExpr(ExprState *state, ExprEvalStep *op);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index d0f2dca592..366534c0d0 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -276,33 +276,35 @@ typedef struct ExprContext
 
 	/* Values to substitute for Param nodes in expression */
 	ParamExecData *ecxt_param_exec_vals;	/* for PARAM_EXEC params */
+	ParamExprData *ecxt_param_expr_vals;	/* for PARAM_EXPR params */
+	int			ecxt_num_param_expr_vals;
 	ParamListInfo ecxt_param_list_info; /* for other param types */
 
 	/*
 	 * Values to substitute for Aggref nodes in the expressions of an Agg
 	 * node, or for WindowFunc nodes within a WindowAgg node.
 	 */
-#define FIELDNO_EXPRCONTEXT_AGGVALUES 8
+#define FIELDNO_EXPRCONTEXT_AGGVALUES 10
 	Datum	   *ecxt_aggvalues; /* precomputed values for aggs/windowfuncs */
-#define FIELDNO_EXPRCONTEXT_AGGNULLS 9
+#define FIELDNO_EXPRCONTEXT_AGGNULLS 11
 	bool	   *ecxt_aggnulls;	/* null flags for aggs/windowfuncs */
 
 	/* Value to substitute for CaseTestExpr nodes in expression */
-#define FIELDNO_EXPRCONTEXT_CASEDATUM 10
+#define FIELDNO_EXPRCONTEXT_CASEDATUM 12
 	Datum		caseValue_datum;
-#define FIELDNO_EXPRCONTEXT_CASENULL 11
+#define FIELDNO_EXPRCONTEXT_CASENULL 13
 	bool		caseValue_isNull;
 
 	/* Value to substitute for CoerceToDomainValue nodes in expression */
-#define FIELDNO_EXPRCONTEXT_DOMAINDATUM 12
+#define FIELDNO_EXPRCONTEXT_DOMAINDATUM 14
 	Datum		domainValue_datum;
-#define FIELDNO_EXPRCONTEXT_DOMAINNULL 13
+#define FIELDNO_EXPRCONTEXT_DOMAINNULL 15
 	bool		domainValue_isNull;
 
 	/* Tuples that OLD/NEW Var nodes in RETURNING may refer to */
-#define FIELDNO_EXPRCONTEXT_OLDTUPLE 14
+#define FIELDNO_EXPRCONTEXT_OLDTUPLE 16
 	TupleTableSlot *ecxt_oldtuple;
-#define FIELDNO_EXPRCONTEXT_NEWTUPLE 15
+#define FIELDNO_EXPRCONTEXT_NEWTUPLE 17
 	TupleTableSlot *ecxt_newtuple;
 
 	/* Link to containing EState (NULL if a standalone ExprContext) */
@@ -684,6 +686,8 @@ typedef struct EState
 	/* Parameter info: */
 	ParamListInfo es_param_list_info;	/* values of external params */
 	ParamExecData *es_param_exec_vals;	/* values of internal params */
+	ParamExprData *es_param_expr_vals;	/* values of internal params */
+	int			es_num_param_expr_vals;
 
 	QueryEnvironment *es_queryEnv;	/* query environment */
 
diff --git a/src/include/nodes/params.h b/src/include/nodes/params.h
index 4321ca6329..5c43a83f44 100644
--- a/src/include/nodes/params.h
+++ b/src/include/nodes/params.h
@@ -19,6 +19,7 @@ struct Bitmapset;
 struct ExprState;
 struct Param;
 struct ParseState;
+struct SubPlanState;
 
 
 /*
@@ -129,8 +130,8 @@ typedef struct ParamListInfoData
 /* ----------------
  *	  ParamExecData
  *
- *	  ParamExecData entries are used for executor internal parameters
- *	  (that is, values being passed into or out of a sub-query).  The
+ *	  ParamExecData entries are used for executor internal PARAM_EXEC
+ *	  parameters (values being passed between plan nodes).  The
  *	  paramid of a PARAM_EXEC Param is a (zero-based) index into an
  *	  array of ParamExecData records, which is referenced through
  *	  es_param_exec_vals or ecxt_param_exec_vals.
@@ -145,11 +146,31 @@ typedef struct ParamListInfoData
 
 typedef struct ParamExecData
 {
-	void	   *execPlan;		/* should be "SubPlanState *" */
+	struct SubPlanState *execPlan;
 	Datum		value;
 	bool		isnull;
 } ParamExecData;
 
+
+/* ----------------
+ *	  ParamExprData
+ *
+ *	  ParamExprData entries are used for executor internal PARAM_EXPR
+ *	  parameters (values being passed within an expression).  The
+ *	  paramid of a PARAM_EXPR Param is a (one-based) index into an
+ *	  array of ParamExprData records, which is referenced through
+ *	  es_param_expr_vals or ecxt_param_expr_vals.  Values are assumed
+ *	  to be valid when needed.
+ * ----------------
+ */
+
+typedef struct ParamExprData
+{
+	Datum		value;
+	bool		isnull;
+} ParamExprData;
+
+
 /* type of argument for ParamsErrorCallback */
 typedef struct ParamsErrorCbData
 {
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 54ee17697e..5ce7137efc 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -137,6 +137,9 @@ typedef struct PlannerGlobal
 	/* type OIDs for PARAM_EXEC Params */
 	List	   *paramExecTypes;
 
+	/* count of PARAM_EXPR Params (we don't need anything but a count) */
+	int			numExprParams;
+
 	/* highest PlaceHolderVar ID assigned */
 	Index		lastPHId;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 6ef5d0b375..79e64b79ee 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -92,6 +92,8 @@ typedef struct PlannedStmt
 
 	List	   *paramExecTypes; /* type OIDs for PARAM_EXEC Params */
 
+	int			numExprParams;	/* count of PARAM_EXPR Params */
+
 	Node	   *utilityStmt;	/* non-null if this is utility stmt */
 
 	/* statement location in source string (copied from Query) */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 59e7bb26bb..af55661df9 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -361,11 +361,22 @@ typedef struct Const
  *				Such parameters are numbered from 1 to n.
  *
  *		PARAM_EXEC:  The parameter is an internal executor parameter, used
- *				for passing values into and out of sub-queries or from
- *				nestloop joins to their inner scans.
+ *				for passing values between plan nodes (for example, values
+ *				passed into and out of sub-queries or from nestloop joins to
+ *				their inner scans).
  *				For historical reasons, such parameters are numbered from 0.
  *				These numbers are independent of PARAM_EXTERN numbers.
  *
+ *		PARAM_EXPR:  The parameter is an internal executor parameter, used
+ *				for passing values between parts of a single expression
+ *				(for example, from CaseExpr into its test expressions).
+ *				We distinguish these from PARAM_EXEC so that they do not
+ *				affect decisions about inter-plan-node dependencies or
+ *				parallel safety.
+ *				Such parameters are numbered from 1 to n.
+ *				These numbers are independent of PARAM_EXTERN and PARAM_EXEC
+ *				numbers.
+ *
  *		PARAM_SUBLINK:	The parameter represents an output column of a SubLink
  *				node's sub-select.  The column number is contained in the
  *				`paramid' field.  (This type of Param is converted to
@@ -382,6 +393,7 @@ typedef enum ParamKind
 {
 	PARAM_EXTERN,
 	PARAM_EXEC,
+	PARAM_EXPR,
 	PARAM_SUBLINK,
 	PARAM_MULTIEXPR,
 } ParamKind;
@@ -1310,11 +1322,17 @@ typedef struct CollateExpr
  * of the WHEN clauses are just the comparison values.  Parse analysis
  * converts these to valid boolean expressions of the form
  *		CaseTestExpr '=' compexpr
- * where the CaseTestExpr node is a placeholder that emits the correct
+ * where the CaseTestExpr node is a placeholder that will emit the correct
  * value at runtime.  This structure is used so that the testexpr need be
  * evaluated only once.  Note that after parse analysis, the condition
  * expressions always yield boolean.
  *
+ * The planner will replace CaseTestExprs with PARAM_EXPR Params to ensure
+ * that different CASE expressions do not interfere with each other.
+ * Ideally we'd generate that representation in parse analysis and not use
+ * CaseTestExpr at all, but that would complicate rewriting, subquery pullup,
+ * and so forth unduly.
+ *
  * Note: we can test whether a CaseExpr has been through parse analysis
  * yet by checking whether casetype is InvalidOid or not.
  *----------
@@ -1329,6 +1347,8 @@ typedef struct CaseExpr
 	Expr	   *arg;			/* implicit equality comparison argument */
 	List	   *args;			/* the arguments (list of WHEN clauses) */
 	Expr	   *defresult;		/* the default result (ELSE clause) */
+	/* ID of PARAM_EXPR Param representing arg, if assigned; else 0 */
+	int			caseparam pg_node_attr(equal_ignore, query_jumble_ignore);
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } CaseExpr;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a2644a2e65..5edcd31c2c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2022,6 +2022,7 @@ ParallelWorkerInfo
 Param
 ParamCompileHook
 ParamExecData
+ParamExprData
 ParamExternData
 ParamFetchHook
 ParamKind
@@ -3516,6 +3517,7 @@ fmStringInfo
 fmgr_hook_type
 foreign_glob_cxt
 foreign_loc_cxt
+foreign_param_cxt
 freefunc
 fsec_t
 gbt_vsrt_arg
-- 
2.43.5

