diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
new file mode 100644
index 2c62b0c..ba85e3f
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -55,10 +55,15 @@
 
 typedef struct ExprSetupInfo
 {
-	/* Highest attribute numbers fetched from inner/outer/scan tuple slots: */
+	/*
+	 * Highest attribute numbers fetched from inner/outer/scan/old/new tuple
+	 * slots:
+	 */
 	AttrNumber	last_inner;
 	AttrNumber	last_outer;
 	AttrNumber	last_scan;
+	AttrNumber	last_old;
+	AttrNumber	last_new;
 	/* MULTIEXPR SubPlan nodes appearing in the expression: */
 	List	   *multiexpr_subplans;
 } ExprSetupInfo;
@@ -439,8 +444,29 @@ ExecBuildProjectionInfo(List *targetList
 					/* INDEX_VAR is handled by default case */
 
 				default:
-					/* get the tuple from the relation being scanned */
-					scratch.opcode = EEOP_ASSIGN_SCAN_VAR;
+
+					/*
+					 * Get the tuple from the relation being scanned.
+					 *
+					 * In a RETURNING clause, this defaults to the new version
+					 * of the tuple when doing an INSERT or UPDATE, and the
+					 * old tuple when doing a DELETE, but that may be
+					 * overridden by explicitly referring to OLD/NEW.
+					 */
+					switch (variable->varreturningtype)
+					{
+						case VAR_RETURNING_OLD:
+							scratch.opcode = EEOP_ASSIGN_OLD_VAR;
+							state->flags |= EEO_FLAG_HAS_OLD;
+							break;
+						case VAR_RETURNING_NEW:
+							scratch.opcode = EEOP_ASSIGN_NEW_VAR;
+							state->flags |= EEO_FLAG_HAS_NEW;
+							break;
+						default:
+							scratch.opcode = EEOP_ASSIGN_SCAN_VAR;
+							break;
+					}
 					break;
 			}
 
@@ -528,7 +554,7 @@ ExecBuildUpdateProjection(List *targetLi
 	int			nAssignableCols;
 	bool		sawJunk;
 	Bitmapset  *assignedCols;
-	ExprSetupInfo deform = {0, 0, 0, NIL};
+	ExprSetupInfo deform = {0, 0, 0, 0, 0, NIL};
 	ExprEvalStep scratch = {0};
 	int			outerattnum;
 	ListCell   *lc,
@@ -929,7 +955,18 @@ ExecInitExprRec(Expr *node, ExprState *s
 							/* INDEX_VAR is handled by default case */
 
 						default:
-							scratch.opcode = EEOP_SCAN_SYSVAR;
+							switch (variable->varreturningtype)
+							{
+								case VAR_RETURNING_OLD:
+									scratch.opcode = EEOP_OLD_SYSVAR;
+									break;
+								case VAR_RETURNING_NEW:
+									scratch.opcode = EEOP_NEW_SYSVAR;
+									break;
+								default:
+									scratch.opcode = EEOP_SCAN_SYSVAR;
+									break;
+							}
 							break;
 					}
 				}
@@ -950,11 +987,28 @@ ExecInitExprRec(Expr *node, ExprState *s
 							/* INDEX_VAR is handled by default case */
 
 						default:
-							scratch.opcode = EEOP_SCAN_VAR;
+							switch (variable->varreturningtype)
+							{
+								case VAR_RETURNING_OLD:
+									scratch.opcode = EEOP_OLD_VAR;
+									break;
+								case VAR_RETURNING_NEW:
+									scratch.opcode = EEOP_NEW_VAR;
+									break;
+								default:
+									scratch.opcode = EEOP_SCAN_VAR;
+									break;
+							}
 							break;
 					}
 				}
 
+				/* update the ExprState's flags if Var refers to OLD/NEW */
+				if (variable->varreturningtype == VAR_RETURNING_OLD)
+					state->flags |= EEO_FLAG_HAS_OLD;
+				else if (variable->varreturningtype == VAR_RETURNING_NEW)
+					state->flags |= EEO_FLAG_HAS_NEW;
+
 				ExprEvalPushStep(state, &scratch);
 				break;
 			}
@@ -2683,7 +2737,7 @@ ExecInitFunc(ExprEvalStep *scratch, Expr
 static void
 ExecCreateExprSetupSteps(ExprState *state, Node *node)
 {
-	ExprSetupInfo info = {0, 0, 0, NIL};
+	ExprSetupInfo info = {0, 0, 0, 0, 0, NIL};
 
 	/* Prescan to find out what we need. */
 	expr_setup_walker(node, &info);
@@ -2706,8 +2760,8 @@ ExecPushExprSetupSteps(ExprState *state,
 	scratch.resnull = NULL;
 
 	/*
-	 * Add steps deforming the ExprState's inner/outer/scan slots as much as
-	 * required by any Vars appearing in the expression.
+	 * Add steps deforming the ExprState's inner/outer/scan/old/new slots as
+	 * much as required by any Vars appearing in the expression.
 	 */
 	if (info->last_inner > 0)
 	{
@@ -2739,6 +2793,26 @@ ExecPushExprSetupSteps(ExprState *state,
 		if (ExecComputeSlotInfo(state, &scratch))
 			ExprEvalPushStep(state, &scratch);
 	}
+	if (info->last_old > 0)
+	{
+		scratch.opcode = EEOP_OLD_FETCHSOME;
+		scratch.d.fetch.last_var = info->last_old;
+		scratch.d.fetch.fixed = false;
+		scratch.d.fetch.kind = NULL;
+		scratch.d.fetch.known_desc = NULL;
+		if (ExecComputeSlotInfo(state, &scratch))
+			ExprEvalPushStep(state, &scratch);
+	}
+	if (info->last_new > 0)
+	{
+		scratch.opcode = EEOP_NEW_FETCHSOME;
+		scratch.d.fetch.last_var = info->last_new;
+		scratch.d.fetch.fixed = false;
+		scratch.d.fetch.kind = NULL;
+		scratch.d.fetch.known_desc = NULL;
+		if (ExecComputeSlotInfo(state, &scratch))
+			ExprEvalPushStep(state, &scratch);
+	}
 
 	/*
 	 * Add steps to execute any MULTIEXPR SubPlans appearing in the
@@ -2802,7 +2876,18 @@ expr_setup_walker(Node *node, ExprSetupI
 				/* INDEX_VAR is handled by default case */
 
 			default:
-				info->last_scan = Max(info->last_scan, attnum);
+				switch (variable->varreturningtype)
+				{
+					case VAR_RETURNING_OLD:
+						info->last_old = Max(info->last_old, attnum);
+						break;
+					case VAR_RETURNING_NEW:
+						info->last_new = Max(info->last_new, attnum);
+						break;
+					default:
+						info->last_scan = Max(info->last_scan, attnum);
+						break;
+				}
 				break;
 		}
 		return false;
@@ -2841,6 +2926,11 @@ expr_setup_walker(Node *node, ExprSetupI
  * evaluation of the expression will have the same type of slot, with an
  * equivalent descriptor.
  *
+ * EEOP_OLD_FETCHSOME and EEOP_NEW_FETCHSOME are used to process RETURNING, if
+ * OLD/NEW columns are referred to explicitly.  In both cases, the tuple
+ * descriptor comes from the parent scan node, so we treat them the same as
+ * EEOP_SCAN_FETCHSOME.
+ *
  * Returns true if the deforming step is required, false otherwise.
  */
 static bool
@@ -2854,7 +2944,9 @@ ExecComputeSlotInfo(ExprState *state, Ex
 
 	Assert(opcode == EEOP_INNER_FETCHSOME ||
 		   opcode == EEOP_OUTER_FETCHSOME ||
-		   opcode == EEOP_SCAN_FETCHSOME);
+		   opcode == EEOP_SCAN_FETCHSOME ||
+		   opcode == EEOP_OLD_FETCHSOME ||
+		   opcode == EEOP_NEW_FETCHSOME);
 
 	if (op->d.fetch.known_desc != NULL)
 	{
@@ -2906,7 +2998,9 @@ ExecComputeSlotInfo(ExprState *state, Ex
 			desc = ExecGetResultType(os);
 		}
 	}
-	else if (opcode == EEOP_SCAN_FETCHSOME)
+	else if (opcode == EEOP_SCAN_FETCHSOME ||
+			 opcode == EEOP_OLD_FETCHSOME ||
+			 opcode == EEOP_NEW_FETCHSOME)
 	{
 		desc = parent->scandesc;
 
@@ -3455,7 +3549,7 @@ ExecBuildAggTrans(AggState *aggstate, Ag
 	PlanState  *parent = &aggstate->ss.ps;
 	ExprEvalStep scratch = {0};
 	bool		isCombine = DO_AGGSPLIT_COMBINE(aggstate->aggsplit);
-	ExprSetupInfo deform = {0, 0, 0, NIL};
+	ExprSetupInfo deform = {0, 0, 0, 0, 0, NIL};
 
 	state->expr = (Expr *) aggstate;
 	state->parent = parent;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
new file mode 100644
index 24c2b60..b40e000
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -157,17 +157,25 @@ static void ExecEvalRowNullInt(ExprState
 static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull);
 static Datum ExecJustOuterVar(ExprState *state, ExprContext *econtext, bool *isnull);
 static Datum ExecJustScanVar(ExprState *state, ExprContext *econtext, bool *isnull);
+static Datum ExecJustOldVar(ExprState *state, ExprContext *econtext, bool *isnull);
+static Datum ExecJustNewVar(ExprState *state, ExprContext *econtext, bool *isnull);
 static Datum ExecJustAssignInnerVar(ExprState *state, ExprContext *econtext, bool *isnull);
 static Datum ExecJustAssignOuterVar(ExprState *state, ExprContext *econtext, bool *isnull);
 static Datum ExecJustAssignScanVar(ExprState *state, ExprContext *econtext, bool *isnull);
+static Datum ExecJustAssignOldVar(ExprState *state, ExprContext *econtext, bool *isnull);
+static Datum ExecJustAssignNewVar(ExprState *state, ExprContext *econtext, bool *isnull);
 static Datum ExecJustApplyFuncToCase(ExprState *state, ExprContext *econtext, bool *isnull);
 static Datum ExecJustConst(ExprState *state, ExprContext *econtext, bool *isnull);
 static Datum ExecJustInnerVarVirt(ExprState *state, ExprContext *econtext, bool *isnull);
 static Datum ExecJustOuterVarVirt(ExprState *state, ExprContext *econtext, bool *isnull);
 static Datum ExecJustScanVarVirt(ExprState *state, ExprContext *econtext, bool *isnull);
+static Datum ExecJustOldVarVirt(ExprState *state, ExprContext *econtext, bool *isnull);
+static Datum ExecJustNewVarVirt(ExprState *state, ExprContext *econtext, bool *isnull);
 static Datum ExecJustAssignInnerVarVirt(ExprState *state, ExprContext *econtext, bool *isnull);
 static Datum ExecJustAssignOuterVarVirt(ExprState *state, ExprContext *econtext, bool *isnull);
 static Datum ExecJustAssignScanVarVirt(ExprState *state, ExprContext *econtext, bool *isnull);
+static Datum ExecJustAssignOldVarVirt(ExprState *state, ExprContext *econtext, bool *isnull);
+static Datum ExecJustAssignNewVarVirt(ExprState *state, ExprContext *econtext, bool *isnull);
 
 /* execution helper functions */
 static pg_attribute_always_inline void ExecAggPlainTransByVal(AggState *aggstate,
@@ -295,6 +303,18 @@ ExecReadyInterpretedExpr(ExprState *stat
 			state->evalfunc_private = (void *) ExecJustScanVar;
 			return;
 		}
+		else if (step0 == EEOP_OLD_FETCHSOME &&
+				 step1 == EEOP_OLD_VAR)
+		{
+			state->evalfunc_private = (void *) ExecJustOldVar;
+			return;
+		}
+		else if (step0 == EEOP_NEW_FETCHSOME &&
+				 step1 == EEOP_NEW_VAR)
+		{
+			state->evalfunc_private = (void *) ExecJustNewVar;
+			return;
+		}
 		else if (step0 == EEOP_INNER_FETCHSOME &&
 				 step1 == EEOP_ASSIGN_INNER_VAR)
 		{
@@ -313,6 +333,18 @@ ExecReadyInterpretedExpr(ExprState *stat
 			state->evalfunc_private = (void *) ExecJustAssignScanVar;
 			return;
 		}
+		else if (step0 == EEOP_OLD_FETCHSOME &&
+				 step1 == EEOP_ASSIGN_OLD_VAR)
+		{
+			state->evalfunc_private = (void *) ExecJustAssignOldVar;
+			return;
+		}
+		else if (step0 == EEOP_NEW_FETCHSOME &&
+				 step1 == EEOP_ASSIGN_NEW_VAR)
+		{
+			state->evalfunc_private = (void *) ExecJustAssignNewVar;
+			return;
+		}
 		else if (step0 == EEOP_CASE_TESTVAL &&
 				 step1 == EEOP_FUNCEXPR_STRICT &&
 				 state->steps[0].d.casetest.value)
@@ -345,6 +377,16 @@ ExecReadyInterpretedExpr(ExprState *stat
 			state->evalfunc_private = (void *) ExecJustScanVarVirt;
 			return;
 		}
+		else if (step0 == EEOP_OLD_VAR)
+		{
+			state->evalfunc_private = (void *) ExecJustOldVarVirt;
+			return;
+		}
+		else if (step0 == EEOP_NEW_VAR)
+		{
+			state->evalfunc_private = (void *) ExecJustNewVarVirt;
+			return;
+		}
 		else if (step0 == EEOP_ASSIGN_INNER_VAR)
 		{
 			state->evalfunc_private = (void *) ExecJustAssignInnerVarVirt;
@@ -360,6 +402,16 @@ ExecReadyInterpretedExpr(ExprState *stat
 			state->evalfunc_private = (void *) ExecJustAssignScanVarVirt;
 			return;
 		}
+		else if (step0 == EEOP_ASSIGN_OLD_VAR)
+		{
+			state->evalfunc_private = (void *) ExecJustAssignOldVarVirt;
+			return;
+		}
+		else if (step0 == EEOP_ASSIGN_NEW_VAR)
+		{
+			state->evalfunc_private = (void *) ExecJustAssignNewVarVirt;
+			return;
+		}
 	}
 
 #if defined(EEO_USE_COMPUTED_GOTO)
@@ -399,6 +451,8 @@ ExecInterpExpr(ExprState *state, ExprCon
 	TupleTableSlot *innerslot;
 	TupleTableSlot *outerslot;
 	TupleTableSlot *scanslot;
+	TupleTableSlot *oldslot;
+	TupleTableSlot *newslot;
 
 	/*
 	 * This array has to be in the same order as enum ExprEvalOp.
@@ -409,16 +463,24 @@ ExecInterpExpr(ExprState *state, ExprCon
 		&&CASE_EEOP_INNER_FETCHSOME,
 		&&CASE_EEOP_OUTER_FETCHSOME,
 		&&CASE_EEOP_SCAN_FETCHSOME,
+		&&CASE_EEOP_OLD_FETCHSOME,
+		&&CASE_EEOP_NEW_FETCHSOME,
 		&&CASE_EEOP_INNER_VAR,
 		&&CASE_EEOP_OUTER_VAR,
 		&&CASE_EEOP_SCAN_VAR,
+		&&CASE_EEOP_OLD_VAR,
+		&&CASE_EEOP_NEW_VAR,
 		&&CASE_EEOP_INNER_SYSVAR,
 		&&CASE_EEOP_OUTER_SYSVAR,
 		&&CASE_EEOP_SCAN_SYSVAR,
+		&&CASE_EEOP_OLD_SYSVAR,
+		&&CASE_EEOP_NEW_SYSVAR,
 		&&CASE_EEOP_WHOLEROW,
 		&&CASE_EEOP_ASSIGN_INNER_VAR,
 		&&CASE_EEOP_ASSIGN_OUTER_VAR,
 		&&CASE_EEOP_ASSIGN_SCAN_VAR,
+		&&CASE_EEOP_ASSIGN_OLD_VAR,
+		&&CASE_EEOP_ASSIGN_NEW_VAR,
 		&&CASE_EEOP_ASSIGN_TMP,
 		&&CASE_EEOP_ASSIGN_TMP_MAKE_RO,
 		&&CASE_EEOP_CONST,
@@ -517,6 +579,8 @@ ExecInterpExpr(ExprState *state, ExprCon
 	innerslot = econtext->ecxt_innertuple;
 	outerslot = econtext->ecxt_outertuple;
 	scanslot = econtext->ecxt_scantuple;
+	oldslot = econtext->ecxt_oldtuple;
+	newslot = econtext->ecxt_newtuple;
 
 #if defined(EEO_USE_COMPUTED_GOTO)
 	EEO_DISPATCH();
@@ -556,6 +620,24 @@ ExecInterpExpr(ExprState *state, ExprCon
 			EEO_NEXT();
 		}
 
+		EEO_CASE(EEOP_OLD_FETCHSOME)
+		{
+			CheckOpSlotCompatibility(op, oldslot);
+
+			slot_getsomeattrs(oldslot, op->d.fetch.last_var);
+
+			EEO_NEXT();
+		}
+
+		EEO_CASE(EEOP_NEW_FETCHSOME)
+		{
+			CheckOpSlotCompatibility(op, newslot);
+
+			slot_getsomeattrs(newslot, op->d.fetch.last_var);
+
+			EEO_NEXT();
+		}
+
 		EEO_CASE(EEOP_INNER_VAR)
 		{
 			int			attnum = op->d.var.attnum;
@@ -599,6 +681,32 @@ ExecInterpExpr(ExprState *state, ExprCon
 			EEO_NEXT();
 		}
 
+		EEO_CASE(EEOP_OLD_VAR)
+		{
+			int			attnum = op->d.var.attnum;
+
+			/* See EEOP_INNER_VAR comments */
+
+			Assert(attnum >= 0 && attnum < oldslot->tts_nvalid);
+			*op->resvalue = oldslot->tts_values[attnum];
+			*op->resnull = oldslot->tts_isnull[attnum];
+
+			EEO_NEXT();
+		}
+
+		EEO_CASE(EEOP_NEW_VAR)
+		{
+			int			attnum = op->d.var.attnum;
+
+			/* See EEOP_INNER_VAR comments */
+
+			Assert(attnum >= 0 && attnum < newslot->tts_nvalid);
+			*op->resvalue = newslot->tts_values[attnum];
+			*op->resnull = newslot->tts_isnull[attnum];
+
+			EEO_NEXT();
+		}
+
 		EEO_CASE(EEOP_INNER_SYSVAR)
 		{
 			ExecEvalSysVar(state, op, econtext, innerslot);
@@ -617,6 +725,18 @@ ExecInterpExpr(ExprState *state, ExprCon
 			EEO_NEXT();
 		}
 
+		EEO_CASE(EEOP_OLD_SYSVAR)
+		{
+			ExecEvalSysVar(state, op, econtext, oldslot);
+			EEO_NEXT();
+		}
+
+		EEO_CASE(EEOP_NEW_SYSVAR)
+		{
+			ExecEvalSysVar(state, op, econtext, newslot);
+			EEO_NEXT();
+		}
+
 		EEO_CASE(EEOP_WHOLEROW)
 		{
 			/* too complex for an inline implementation */
@@ -676,6 +796,40 @@ ExecInterpExpr(ExprState *state, ExprCon
 			EEO_NEXT();
 		}
 
+		EEO_CASE(EEOP_ASSIGN_OLD_VAR)
+		{
+			int			resultnum = op->d.assign_var.resultnum;
+			int			attnum = op->d.assign_var.attnum;
+
+			/*
+			 * We do not need CheckVarSlotCompatibility here; that was taken
+			 * care of at compilation time.  But see EEOP_INNER_VAR comments.
+			 */
+			Assert(attnum >= 0 && attnum < oldslot->tts_nvalid);
+			Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
+			resultslot->tts_values[resultnum] = oldslot->tts_values[attnum];
+			resultslot->tts_isnull[resultnum] = oldslot->tts_isnull[attnum];
+
+			EEO_NEXT();
+		}
+
+		EEO_CASE(EEOP_ASSIGN_NEW_VAR)
+		{
+			int			resultnum = op->d.assign_var.resultnum;
+			int			attnum = op->d.assign_var.attnum;
+
+			/*
+			 * We do not need CheckVarSlotCompatibility here; that was taken
+			 * care of at compilation time.  But see EEOP_INNER_VAR comments.
+			 */
+			Assert(attnum >= 0 && attnum < newslot->tts_nvalid);
+			Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
+			resultslot->tts_values[resultnum] = newslot->tts_values[attnum];
+			resultslot->tts_isnull[resultnum] = newslot->tts_isnull[attnum];
+
+			EEO_NEXT();
+		}
+
 		EEO_CASE(EEOP_ASSIGN_TMP)
 		{
 			int			resultnum = op->d.assign_tmp.resultnum;
@@ -1880,10 +2034,14 @@ CheckExprStillValid(ExprState *state, Ex
 	TupleTableSlot *innerslot;
 	TupleTableSlot *outerslot;
 	TupleTableSlot *scanslot;
+	TupleTableSlot *oldslot;
+	TupleTableSlot *newslot;
 
 	innerslot = econtext->ecxt_innertuple;
 	outerslot = econtext->ecxt_outertuple;
 	scanslot = econtext->ecxt_scantuple;
+	oldslot = econtext->ecxt_oldtuple;
+	newslot = econtext->ecxt_newtuple;
 
 	for (int i = 0; i < state->steps_len; i++)
 	{
@@ -1914,6 +2072,22 @@ CheckExprStillValid(ExprState *state, Ex
 					CheckVarSlotCompatibility(scanslot, attnum + 1, op->d.var.vartype);
 					break;
 				}
+
+			case EEOP_OLD_VAR:
+				{
+					int			attnum = op->d.var.attnum;
+
+					CheckVarSlotCompatibility(oldslot, attnum + 1, op->d.var.vartype);
+					break;
+				}
+
+			case EEOP_NEW_VAR:
+				{
+					int			attnum = op->d.var.attnum;
+
+					CheckVarSlotCompatibility(newslot, attnum + 1, op->d.var.vartype);
+					break;
+				}
 			default:
 				break;
 		}
@@ -2088,7 +2262,7 @@ get_cached_rowtype(Oid type_id, int32 ty
  * Fast-path functions, for very simple expressions
  */
 
-/* implementation of ExecJust(Inner|Outer|Scan)Var */
+/* implementation of ExecJust(Inner|Outer|Scan|Old|New)Var */
 static pg_attribute_always_inline Datum
 ExecJustVarImpl(ExprState *state, TupleTableSlot *slot, bool *isnull)
 {
@@ -2126,7 +2300,21 @@ ExecJustScanVar(ExprState *state, ExprCo
 	return ExecJustVarImpl(state, econtext->ecxt_scantuple, isnull);
 }
 
-/* implementation of ExecJustAssign(Inner|Outer|Scan)Var */
+/* Simple reference to OLD Var in RETURNING */
+static Datum
+ExecJustOldVar(ExprState *state, ExprContext *econtext, bool *isnull)
+{
+	return ExecJustVarImpl(state, econtext->ecxt_oldtuple, isnull);
+}
+
+/* Simple reference to NEW Var in RETURNING */
+static Datum
+ExecJustNewVar(ExprState *state, ExprContext *econtext, bool *isnull)
+{
+	return ExecJustVarImpl(state, econtext->ecxt_newtuple, isnull);
+}
+
+/* implementation of ExecJustAssign(Inner|Outer|Scan|Old|New)Var */
 static pg_attribute_always_inline Datum
 ExecJustAssignVarImpl(ExprState *state, TupleTableSlot *inslot, bool *isnull)
 {
@@ -2173,6 +2361,20 @@ ExecJustAssignScanVar(ExprState *state,
 	return ExecJustAssignVarImpl(state, econtext->ecxt_scantuple, isnull);
 }
 
+/* Evaluate OLD Var and assign to appropriate column of result tuple */
+static Datum
+ExecJustAssignOldVar(ExprState *state, ExprContext *econtext, bool *isnull)
+{
+	return ExecJustAssignVarImpl(state, econtext->ecxt_oldtuple, isnull);
+}
+
+/* Evaluate NEW Var and assign to appropriate column of result tuple */
+static Datum
+ExecJustAssignNewVar(ExprState *state, ExprContext *econtext, bool *isnull)
+{
+	return ExecJustAssignVarImpl(state, econtext->ecxt_newtuple, isnull);
+}
+
 /* Evaluate CASE_TESTVAL and apply a strict function to it */
 static Datum
 ExecJustApplyFuncToCase(ExprState *state, ExprContext *econtext, bool *isnull)
@@ -2221,7 +2423,7 @@ ExecJustConst(ExprState *state, ExprCont
 	return op->d.constval.value;
 }
 
-/* implementation of ExecJust(Inner|Outer|Scan)VarVirt */
+/* implementation of ExecJust(Inner|Outer|Scan|Old|New)VarVirt */
 static pg_attribute_always_inline Datum
 ExecJustVarVirtImpl(ExprState *state, TupleTableSlot *slot, bool *isnull)
 {
@@ -2264,7 +2466,21 @@ ExecJustScanVarVirt(ExprState *state, Ex
 	return ExecJustVarVirtImpl(state, econtext->ecxt_scantuple, isnull);
 }
 
-/* implementation of ExecJustAssign(Inner|Outer|Scan)VarVirt */
+/* Like ExecJustOldVar, optimized for virtual slots */
+static Datum
+ExecJustOldVarVirt(ExprState *state, ExprContext *econtext, bool *isnull)
+{
+	return ExecJustVarVirtImpl(state, econtext->ecxt_oldtuple, isnull);
+}
+
+/* Like ExecJustNewVar, optimized for virtual slots */
+static Datum
+ExecJustNewVarVirt(ExprState *state, ExprContext *econtext, bool *isnull)
+{
+	return ExecJustVarVirtImpl(state, econtext->ecxt_newtuple, isnull);
+}
+
+/* implementation of ExecJustAssign(Inner|Outer|Scan|Old|New)VarVirt */
 static pg_attribute_always_inline Datum
 ExecJustAssignVarVirtImpl(ExprState *state, TupleTableSlot *inslot, bool *isnull)
 {
@@ -2307,6 +2523,20 @@ ExecJustAssignScanVarVirt(ExprState *sta
 	return ExecJustAssignVarVirtImpl(state, econtext->ecxt_scantuple, isnull);
 }
 
+/* Like ExecJustAssignOldVar, optimized for virtual slots */
+static Datum
+ExecJustAssignOldVarVirt(ExprState *state, ExprContext *econtext, bool *isnull)
+{
+	return ExecJustAssignVarVirtImpl(state, econtext->ecxt_oldtuple, isnull);
+}
+
+/* Like ExecJustAssignNewVar, optimized for virtual slots */
+static Datum
+ExecJustAssignNewVarVirt(ExprState *state, ExprContext *econtext, bool *isnull)
+{
+	return ExecJustAssignVarVirtImpl(state, econtext->ecxt_newtuple, isnull);
+}
+
 #if defined(EEO_USE_COMPUTED_GOTO)
 /*
  * Comparator used when building address->opcode lookup table for
@@ -4219,8 +4449,25 @@ ExecEvalWholeRowVar(ExprState *state, Ex
 			/* INDEX_VAR is handled by default case */
 
 		default:
-			/* get the tuple from the relation being scanned */
-			slot = econtext->ecxt_scantuple;
+
+			/*
+			 * Get the tuple from the relation being scanned.
+			 *
+			 * By default, this uses the "scan" tuple slot, but a wholerow Var
+			 * in the RETURNING list may explicitly refer to OLD/NEW.
+			 */
+			switch (variable->varreturningtype)
+			{
+				case VAR_RETURNING_OLD:
+					slot = econtext->ecxt_oldtuple;
+					break;
+				case VAR_RETURNING_NEW:
+					slot = econtext->ecxt_newtuple;
+					break;
+				default:
+					slot = econtext->ecxt_scantuple;
+					break;
+			}
 			break;
 	}
 
@@ -4428,9 +4675,6 @@ ExecEvalSysVar(ExprState *state, ExprEva
 						op->d.var.attnum,
 						op->resnull);
 	*op->resvalue = d;
-	/* this ought to be unreachable, but it's cheap enough to check */
-	if (unlikely(*op->resnull))
-		elog(ERROR, "failed to fetch attribute from slot");
 }
 
 /*
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index ee7e666..960f1eb
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -98,6 +98,13 @@ typedef struct ModifyTableContext
 	TM_FailureData tmfd;
 
 	/*
+	 * The tuple deleted when doing a cross-partition UPDATE with a RETURNING
+	 * clause that refers to OLD columns (converted to the root's tuple
+	 * descriptor).
+	 */
+	TupleTableSlot *cpDeletedSlot;
+
+	/*
 	 * The tuple projected by the INSERT's RETURNING clause, when doing a
 	 * cross-partition UPDATE
 	 */
@@ -238,34 +245,41 @@ ExecCheckPlanOutput(Relation resultRel,
  * ExecProcessReturning --- evaluate a RETURNING list
  *
  * resultRelInfo: current result rel
- * tupleSlot: slot holding tuple actually inserted/updated/deleted
+ * cmdType: operation performed (INSERT, UPDATE, or DELETE only)
+ * oldSlot: slot holding old tuple deleted or updated
+ * newSlot: slot holding new tuple inserted or updated
  * planSlot: slot holding tuple returned by top subplan node
  *
- * Note: If tupleSlot is NULL, the FDW should have already provided econtext's
- * scan tuple.
+ * Note: If oldSlot/newSlot are NULL, the FDW should have already provided
+ * econtext's scan/old/new tuples.
  *
  * Returns a slot holding the result tuple
  */
 static TupleTableSlot *
 ExecProcessReturning(ResultRelInfo *resultRelInfo,
-					 TupleTableSlot *tupleSlot,
+					 CmdType cmdType,
+					 TupleTableSlot *oldSlot,
+					 TupleTableSlot *newSlot,
 					 TupleTableSlot *planSlot)
 {
 	ProjectionInfo *projectReturning = resultRelInfo->ri_projectReturning;
 	ExprContext *econtext = projectReturning->pi_exprContext;
 
-	/* Make tuple and any needed join variables available to ExecProject */
-	if (tupleSlot)
-		econtext->ecxt_scantuple = tupleSlot;
+	/* Make tuples and any needed join variables available to ExecProject */
+	if (oldSlot)
+	{
+		econtext->ecxt_oldtuple = oldSlot;
+		if (cmdType == CMD_DELETE)
+			econtext->ecxt_scantuple = oldSlot;
+	}
+	if (newSlot)
+	{
+		econtext->ecxt_newtuple = newSlot;
+		if (cmdType != CMD_DELETE)
+			econtext->ecxt_scantuple = newSlot;
+	}
 	econtext->ecxt_outertuple = planSlot;
 
-	/*
-	 * RETURNING expressions might reference the tableoid column, so
-	 * reinitialize tts_tableOid before evaluating them.
-	 */
-	econtext->ecxt_scantuple->tts_tableOid =
-		RelationGetRelid(resultRelInfo->ri_RelationDesc);
-
 	/* Compute the RETURNING expressions */
 	return ExecProject(projectReturning);
 }
@@ -761,6 +775,7 @@ ExecInsert(ModifyTableContext *context,
 	Relation	resultRelationDesc;
 	List	   *recheckIndexes = NIL;
 	TupleTableSlot *planSlot = context->planSlot;
+	TupleTableSlot *oldSlot;
 	TupleTableSlot *result = NULL;
 	TransitionCaptureState *ar_insert_trig_tcs;
 	ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
@@ -1195,7 +1210,63 @@ ExecInsert(ModifyTableContext *context,
 
 	/* Process RETURNING if present */
 	if (resultRelInfo->ri_projectReturning)
-		result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+	{
+		/*
+		 * If this is part of a cross-partition UPDATE, and the RETURNING list
+		 * refers to any OLD columns, ExecDelete() will have saved the tuple
+		 * deleted from the original partition, which we must use here to
+		 * compute the OLD column values.  Otherwise, set all OLD columns
+		 * values to NULL, if requested.
+		 */
+		if (context->cpDeletedSlot)
+		{
+			TupleConversionMap *tupconv_map;
+
+			/*
+			 * Convert the OLD tuple to the new partition's format/slot, if
+			 * needed.  Note that ExceDelete() already converted it to the
+			 * root's partition's format/slot.
+			 */
+			oldSlot = context->cpDeletedSlot;
+			tupconv_map = ExecGetRootToChildMap(resultRelInfo, estate);
+			if (tupconv_map != NULL)
+			{
+				oldSlot = execute_attr_map_slot(tupconv_map->attrMap,
+												oldSlot,
+												ExecGetReturningSlot(estate,
+																	 resultRelInfo));
+
+				oldSlot->tts_tableOid = context->cpDeletedSlot->tts_tableOid;
+				ItemPointerCopy(&context->cpDeletedSlot->tts_tid, &oldSlot->tts_tid);
+			}
+		}
+		else if (resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_OLD)
+		{
+			oldSlot = ExecGetReturningSlot(estate, resultRelInfo);
+
+			ExecStoreAllNullTuple(oldSlot);
+			oldSlot->tts_tableOid = RelationGetRelid(resultRelInfo->ri_RelationDesc);
+		}
+		else
+			oldSlot = NULL;		/* No references to OLD columns */
+
+		result = ExecProcessReturning(resultRelInfo, CMD_INSERT,
+									  oldSlot, slot, planSlot);
+
+		/*
+		 * For a cross-partition UPDATE, release the old tuple, first making
+		 * sure that the result slot has a local copy of any pass-by-reference
+		 * values.
+		 */
+		if (context->cpDeletedSlot)
+		{
+			ExecMaterializeSlot(result);
+			ExecClearTuple(oldSlot);
+			if (context->cpDeletedSlot != oldSlot)
+				ExecClearTuple(context->cpDeletedSlot);
+			context->cpDeletedSlot = NULL;
+		}
+	}
 
 	if (inserted_tuple)
 		*inserted_tuple = slot;
@@ -1433,6 +1504,7 @@ ExecDelete(ModifyTableContext *context,
 	Relation	resultRelationDesc = resultRelInfo->ri_RelationDesc;
 	TupleTableSlot *slot = NULL;
 	TM_Result	result;
+	bool		saveOld;
 
 	if (tupleDeleted)
 		*tupleDeleted = false;
@@ -1667,13 +1739,23 @@ ldelete:
 
 	ExecDeleteEpilogue(context, resultRelInfo, tupleid, oldtuple, changingPart);
 
-	/* Process RETURNING if present and if requested */
-	if (processReturning && resultRelInfo->ri_projectReturning)
+	/*
+	 * Process RETURNING if present and if requested.
+	 *
+	 * If this is part of a cross-partition UPDATE, and the RETURNING list
+	 * refers to any OLD column values, save the old tuple here for later
+	 * processing of the RETURNING list by ExecInsert().
+	 */
+	saveOld = changingPart && resultRelInfo->ri_projectReturning &&
+		resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_OLD;
+
+	if (resultRelInfo->ri_projectReturning && (processReturning || saveOld))
 	{
 		/*
 		 * We have to put the target tuple into a slot, which means first we
 		 * gotta fetch it.  We can use the trigger tuple slot.
 		 */
+		TupleTableSlot *newSlot;
 		TupleTableSlot *rslot;
 
 		if (resultRelInfo->ri_FdwRoutine)
@@ -1696,7 +1778,57 @@ ldelete:
 			}
 		}
 
-		rslot = ExecProcessReturning(resultRelInfo, slot, context->planSlot);
+		/*
+		 * If required, save the old tuple for later processing of the
+		 * RETURNING list by ExecInsert().
+		 */
+		if (saveOld)
+		{
+			TupleConversionMap *tupconv_map;
+			ResultRelInfo *rootRelInfo;
+			TupleTableSlot *oldSlot;
+
+			/*
+			 * Convert the tuple into the root partition's format/slot, if
+			 * needed.  ExecInsert() will then convert it to the new
+			 * partition's format/slot, if necessary.
+			 */
+			tupconv_map = ExecGetChildToRootMap(resultRelInfo);
+			if (tupconv_map != NULL)
+			{
+				rootRelInfo = context->mtstate->rootResultRelInfo;
+				oldSlot = slot;
+				slot = execute_attr_map_slot(tupconv_map->attrMap,
+											 slot,
+											 ExecGetReturningSlot(estate,
+																  rootRelInfo));
+
+				slot->tts_tableOid = oldSlot->tts_tableOid;
+				ItemPointerCopy(&oldSlot->tts_tid, &slot->tts_tid);
+			}
+
+			context->cpDeletedSlot = slot;
+
+			return NULL;
+		}
+
+		/*
+		 * If the RETURNING list refers to NEW columns, return NULLs.  Use
+		 * ExecGetTriggerNewSlot() to store an all-NULL new tuple, since it is
+		 * of the right type, and isn't being used for anything else.
+		 */
+		if (resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_NEW)
+		{
+			newSlot = ExecGetTriggerNewSlot(estate, resultRelInfo);
+
+			ExecStoreAllNullTuple(newSlot);
+			newSlot->tts_tableOid = RelationGetRelid(resultRelInfo->ri_RelationDesc);
+		}
+		else
+			newSlot = NULL;		/* No references to NEW columns */
+
+		rslot = ExecProcessReturning(resultRelInfo, CMD_DELETE,
+									 slot, newSlot, context->planSlot);
 
 		/*
 		 * Before releasing the target tuple again, make sure rslot has a
@@ -1749,6 +1881,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
 	bool		tuple_deleted;
 	TupleTableSlot *epqslot = NULL;
 
+	context->cpDeletedSlot = NULL;
 	context->cpUpdateReturningSlot = NULL;
 	*retry_slot = NULL;
 
@@ -2253,6 +2386,7 @@ ExecCrossPartitionUpdateForeignKey(Modif
  *		foreign table triggers; it is NULL when the foreign table has
  *		no relevant triggers.
  *
+ *		oldSlot contains the old tuple value.
  *		slot contains the new tuple value to be stored.
  *		planSlot is the output of the ModifyTable's subplan; we use it
  *		to access values from other input tables (for RETURNING),
@@ -2263,8 +2397,8 @@ ExecCrossPartitionUpdateForeignKey(Modif
  */
 static TupleTableSlot *
 ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
-		   ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot,
-		   bool canSetTag)
+		   ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *oldSlot,
+		   TupleTableSlot *slot, bool canSetTag)
 {
 	EState	   *estate = context->estate;
 	Relation	resultRelationDesc = resultRelInfo->ri_RelationDesc;
@@ -2379,7 +2513,6 @@ redo_act:
 				{
 					TupleTableSlot *inputslot;
 					TupleTableSlot *epqslot;
-					TupleTableSlot *oldSlot;
 
 					if (IsolationUsesXactSnapshot())
 						ereport(ERROR,
@@ -2486,7 +2619,8 @@ redo_act:
 
 	/* Process RETURNING if present */
 	if (resultRelInfo->ri_projectReturning)
-		return ExecProcessReturning(resultRelInfo, slot, context->planSlot);
+		return ExecProcessReturning(resultRelInfo, CMD_UPDATE,
+									oldSlot, slot, context->planSlot);
 
 	return NULL;
 }
@@ -2698,16 +2832,23 @@ ExecOnConflictUpdate(ModifyTableContext
 
 	/* Execute UPDATE with projection */
 	*returning = ExecUpdate(context, resultRelInfo,
-							conflictTid, NULL,
+							conflictTid, NULL, existing,
 							resultRelInfo->ri_onConflict->oc_ProjSlot,
 							canSetTag);
 
 	/*
 	 * Clear out existing tuple, as there might not be another conflict among
 	 * the next input rows. Don't want to hold resources till the end of the
-	 * query.
+	 * query.  First though, make sure that the returning slot, if any, has a
+	 * local copy of any OLD pass-by-reference values, if it refers to any OLD
+	 * columns.
 	 */
+	if (*returning != NULL &&
+		resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_OLD)
+		ExecMaterializeSlot(*returning);
+
 	ExecClearTuple(existing);
+
 	return true;
 }
 
@@ -3635,6 +3776,7 @@ ExecModifyTable(PlanState *pstate)
 			ResetExprContext(pstate->ps_ExprContext);
 
 		context.planSlot = ExecProcNode(subplanstate);
+		context.cpDeletedSlot = NULL;
 
 		/* No more tuples to process? */
 		if (TupIsNull(context.planSlot))
@@ -3693,9 +3835,12 @@ ExecModifyTable(PlanState *pstate)
 			 * A scan slot containing the data that was actually inserted,
 			 * updated or deleted has already been made available to
 			 * ExecProcessReturning by IterateDirectModify, so no need to
-			 * provide it here.
+			 * provide it here.  The individual old and new slots are not
+			 * needed, since RETURNING OLD/NEW is not supported for foreign
+			 * tables.
 			 */
-			slot = ExecProcessReturning(resultRelInfo, NULL, context.planSlot);
+			slot = ExecProcessReturning(resultRelInfo, operation,
+										NULL, NULL, context.planSlot);
 
 			return slot;
 		}
@@ -3842,7 +3987,7 @@ ExecModifyTable(PlanState *pstate)
 
 				/* Now apply the update. */
 				slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
-								  slot, node->canSetTag);
+								  oldSlot, slot, node->canSetTag);
 				break;
 
 			case CMD_DELETE:
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
new file mode 100644
index a3a0876..01ca8ee
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -106,6 +106,8 @@ llvm_compile_expr(ExprState *state)
 	LLVMValueRef v_outerslot;
 	LLVMValueRef v_scanslot;
 	LLVMValueRef v_resultslot;
+	LLVMValueRef v_oldslot;
+	LLVMValueRef v_newslot;
 
 	/* nulls/values of slots */
 	LLVMValueRef v_innervalues;
@@ -114,6 +116,10 @@ llvm_compile_expr(ExprState *state)
 	LLVMValueRef v_outernulls;
 	LLVMValueRef v_scanvalues;
 	LLVMValueRef v_scannulls;
+	LLVMValueRef v_oldvalues;
+	LLVMValueRef v_oldnulls;
+	LLVMValueRef v_newvalues;
+	LLVMValueRef v_newnulls;
 	LLVMValueRef v_resultvalues;
 	LLVMValueRef v_resultnulls;
 
@@ -205,6 +211,16 @@ llvm_compile_expr(ExprState *state)
 									 v_state,
 									 FIELDNO_EXPRSTATE_RESULTSLOT,
 									 "v_resultslot");
+	v_oldslot = l_load_struct_gep(b,
+								  StructExprContext,
+								  v_econtext,
+								  FIELDNO_EXPRCONTEXT_OLDTUPLE,
+								  "v_oldslot");
+	v_newslot = l_load_struct_gep(b,
+								  StructExprContext,
+								  v_econtext,
+								  FIELDNO_EXPRCONTEXT_NEWTUPLE,
+								  "v_newslot");
 
 	/* build global values/isnull pointers */
 	v_scanvalues = l_load_struct_gep(b,
@@ -217,6 +233,26 @@ llvm_compile_expr(ExprState *state)
 									v_scanslot,
 									FIELDNO_TUPLETABLESLOT_ISNULL,
 									"v_scannulls");
+	v_oldvalues = l_load_struct_gep(b,
+									StructTupleTableSlot,
+									v_oldslot,
+									FIELDNO_TUPLETABLESLOT_VALUES,
+									"v_oldvalues");
+	v_oldnulls = l_load_struct_gep(b,
+								   StructTupleTableSlot,
+								   v_oldslot,
+								   FIELDNO_TUPLETABLESLOT_ISNULL,
+								   "v_oldnulls");
+	v_newvalues = l_load_struct_gep(b,
+									StructTupleTableSlot,
+									v_newslot,
+									FIELDNO_TUPLETABLESLOT_VALUES,
+									"v_newvalues");
+	v_newnulls = l_load_struct_gep(b,
+								   StructTupleTableSlot,
+								   v_newslot,
+								   FIELDNO_TUPLETABLESLOT_ISNULL,
+								   "v_newnulls");
 	v_innervalues = l_load_struct_gep(b,
 									  StructTupleTableSlot,
 									  v_innerslot,
@@ -302,6 +338,8 @@ llvm_compile_expr(ExprState *state)
 			case EEOP_INNER_FETCHSOME:
 			case EEOP_OUTER_FETCHSOME:
 			case EEOP_SCAN_FETCHSOME:
+			case EEOP_OLD_FETCHSOME:
+			case EEOP_NEW_FETCHSOME:
 				{
 					TupleDesc	desc = NULL;
 					LLVMValueRef v_slot;
@@ -326,8 +364,12 @@ llvm_compile_expr(ExprState *state)
 						v_slot = v_innerslot;
 					else if (opcode == EEOP_OUTER_FETCHSOME)
 						v_slot = v_outerslot;
-					else
+					else if (opcode == EEOP_SCAN_FETCHSOME)
 						v_slot = v_scanslot;
+					else if (opcode == EEOP_OLD_FETCHSOME)
+						v_slot = v_oldslot;
+					else
+						v_slot = v_newslot;
 
 					/*
 					 * Check if all required attributes are available, or
@@ -396,6 +438,8 @@ llvm_compile_expr(ExprState *state)
 			case EEOP_INNER_VAR:
 			case EEOP_OUTER_VAR:
 			case EEOP_SCAN_VAR:
+			case EEOP_OLD_VAR:
+			case EEOP_NEW_VAR:
 				{
 					LLVMValueRef value,
 								isnull;
@@ -413,11 +457,21 @@ llvm_compile_expr(ExprState *state)
 						v_values = v_outervalues;
 						v_nulls = v_outernulls;
 					}
-					else
+					else if (opcode == EEOP_SCAN_VAR)
 					{
 						v_values = v_scanvalues;
 						v_nulls = v_scannulls;
 					}
+					else if (opcode == EEOP_OLD_VAR)
+					{
+						v_values = v_oldvalues;
+						v_nulls = v_oldnulls;
+					}
+					else
+					{
+						v_values = v_newvalues;
+						v_nulls = v_newnulls;
+					}
 
 					v_attnum = l_int32_const(lc, op->d.var.attnum);
 					value = l_load_gep1(b, TypeSizeT, v_values, v_attnum, "");
@@ -432,6 +486,8 @@ llvm_compile_expr(ExprState *state)
 			case EEOP_INNER_SYSVAR:
 			case EEOP_OUTER_SYSVAR:
 			case EEOP_SCAN_SYSVAR:
+			case EEOP_OLD_SYSVAR:
+			case EEOP_NEW_SYSVAR:
 				{
 					LLVMValueRef v_slot;
 
@@ -439,8 +495,12 @@ llvm_compile_expr(ExprState *state)
 						v_slot = v_innerslot;
 					else if (opcode == EEOP_OUTER_SYSVAR)
 						v_slot = v_outerslot;
-					else
+					else if (opcode == EEOP_SCAN_SYSVAR)
 						v_slot = v_scanslot;
+					else if (opcode == EEOP_OLD_SYSVAR)
+						v_slot = v_oldslot;
+					else
+						v_slot = v_newslot;
 
 					build_EvalXFunc(b, mod, "ExecEvalSysVar",
 									v_state, op, v_econtext, v_slot);
@@ -458,6 +518,8 @@ llvm_compile_expr(ExprState *state)
 			case EEOP_ASSIGN_INNER_VAR:
 			case EEOP_ASSIGN_OUTER_VAR:
 			case EEOP_ASSIGN_SCAN_VAR:
+			case EEOP_ASSIGN_OLD_VAR:
+			case EEOP_ASSIGN_NEW_VAR:
 				{
 					LLVMValueRef v_value;
 					LLVMValueRef v_isnull;
@@ -478,11 +540,21 @@ llvm_compile_expr(ExprState *state)
 						v_values = v_outervalues;
 						v_nulls = v_outernulls;
 					}
-					else
+					else if (opcode == EEOP_ASSIGN_SCAN_VAR)
 					{
 						v_values = v_scanvalues;
 						v_nulls = v_scannulls;
 					}
+					else if (opcode == EEOP_ASSIGN_OLD_VAR)
+					{
+						v_values = v_oldvalues;
+						v_nulls = v_oldnulls;
+					}
+					else
+					{
+						v_values = v_newvalues;
+						v_nulls = v_newnulls;
+					}
 
 					/* load data */
 					v_attnum = l_int32_const(lc, op->d.assign_var.attnum);
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
new file mode 100644
index 89e77ad..961c0ff
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -81,12 +81,14 @@ makeVar(int varno,
 	var->varlevelsup = varlevelsup;
 
 	/*
-	 * Only a few callers need to make Var nodes with non-null varnullingrels,
-	 * or with varnosyn/varattnosyn different from varno/varattno.  We don't
-	 * provide separate arguments for them, but just initialize them to NULL
-	 * and the given varno/varattno.  This reduces code clutter and chance of
-	 * error for most callers.
+	 * Only a few callers need to make Var nodes with varreturningtype
+	 * different from VAR_RETURNING_DEFAULT, non-null varnullingrels, or with
+	 * varnosyn/varattnosyn different from varno/varattno.  We don't provide
+	 * separate arguments for them, but just initialize them to sensible
+	 * default values.  This reduces code clutter and chance of error for most
+	 * callers.
 	 */
+	var->varreturningtype = VAR_RETURNING_DEFAULT;
 	var->varnullingrels = NULL;
 	var->varnosyn = (Index) varno;
 	var->varattnosyn = varattno;
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index c03f4f2..2921820
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3827,6 +3827,7 @@ raw_expression_tree_walker_impl(Node *no
 		case T_ParamRef:
 		case T_A_Const:
 		case T_A_Star:
+		case T_ReturningOption:
 			/* primitive node types with no subnodes */
 			break;
 		case T_Alias:
@@ -3995,7 +3996,7 @@ raw_expression_tree_walker_impl(Node *no
 					return true;
 				if (WALK(stmt->onConflictClause))
 					return true;
-				if (WALK(stmt->returningList))
+				if (WALK(stmt->returningClause))
 					return true;
 				if (WALK(stmt->withClause))
 					return true;
@@ -4011,7 +4012,7 @@ raw_expression_tree_walker_impl(Node *no
 					return true;
 				if (WALK(stmt->whereClause))
 					return true;
-				if (WALK(stmt->returningList))
+				if (WALK(stmt->returningClause))
 					return true;
 				if (WALK(stmt->withClause))
 					return true;
@@ -4029,7 +4030,7 @@ raw_expression_tree_walker_impl(Node *no
 					return true;
 				if (WALK(stmt->fromClause))
 					return true;
-				if (WALK(stmt->returningList))
+				if (WALK(stmt->returningClause))
 					return true;
 				if (WALK(stmt->withClause))
 					return true;
@@ -4063,6 +4064,16 @@ raw_expression_tree_walker_impl(Node *no
 					return true;
 			}
 			break;
+		case T_ReturningClause:
+			{
+				ReturningClause *returning = (ReturningClause *) node;
+
+				if (WALK(returning->options))
+					return true;
+				if (WALK(returning->exprs))
+					return true;
+			}
+			break;
 		case T_SelectStmt:
 			{
 				SelectStmt *stmt = (SelectStmt *) node;
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
new file mode 100644
index 34ca6d4..3a3da97
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -7135,6 +7135,34 @@ make_modifytable(PlannerInfo *root, Plan
 		}
 
 		/*
+		 * Similarly, RETURNING OLD/NEW is not supported for foreign tables.
+		 */
+		if (root->parse->returningList && fdwroutine != NULL)
+		{
+			List	   *ret_vars = pull_var_clause((Node *) root->parse->returningList,
+												   PVC_RECURSE_AGGREGATES |
+												   PVC_RECURSE_WINDOWFUNCS |
+												   PVC_INCLUDE_PLACEHOLDERS);
+			ListCell   *lc2;
+
+			foreach(lc2, ret_vars)
+			{
+				Var		   *var = lfirst_node(Var, lc2);
+
+				if (var->varreturningtype != VAR_RETURNING_DEFAULT)
+				{
+					RangeTblEntry *rte = planner_rt_fetch(rti, root);
+
+					ereport(ERROR,
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("cannot return OLD/NEW values from relation \"%s\"",
+								   get_rel_name(rte->relid)),
+							errdetail_relkind_not_supported(rte->relkind));
+				}
+			}
+		}
+
+		/*
 		 * Try to modify the foreign table directly if (1) the FDW provides
 		 * callback functions needed for that and (2) there are no local
 		 * structures that need to be run for each modified row: row-level
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
new file mode 100644
index 73ff407..b14d812
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -2363,7 +2363,8 @@ pullup_replace_vars_callback(Var *var,
 		 * expansion with varlevelsup = 0, and then adjust below if needed.
 		 */
 		expandRTE(rcon->target_rte,
-				  var->varno, 0 /* not varlevelsup */ , var->location,
+				  var->varno, 0 /* not varlevelsup */ ,
+				  var->varreturningtype, var->location,
 				  (var->vartype != RECORDOID),
 				  &colnames, &fields);
 		/* Expand the generated per-field Vars, but don't insert PHVs there */
diff --git a/src/backend/optimizer/util/appendinfo.c b/src/backend/optimizer/util/appendinfo.c
new file mode 100644
index f456b3b..a104052
--- a/src/backend/optimizer/util/appendinfo.c
+++ b/src/backend/optimizer/util/appendinfo.c
@@ -279,7 +279,10 @@ adjust_appendrel_attrs_mutator(Node *nod
 					elog(ERROR, "attribute %d of relation \"%s\" does not exist",
 						 var->varattno, get_rel_name(appinfo->parent_reloid));
 				if (IsA(newnode, Var))
+				{
+					((Var *) newnode)->varreturningtype = var->varreturningtype;
 					((Var *) newnode)->varnullingrels = var->varnullingrels;
+				}
 				else if (var->varnullingrels != NULL)
 					elog(ERROR, "failed to apply nullingrels to a non-Var");
 				return newnode;
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
new file mode 100644
index 507c101..1f84f70
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -3371,6 +3371,8 @@ eval_const_expressions_mutator(Node *nod
 										 fselect->resulttypmod,
 										 fselect->resultcollid,
 										 ((Var *) arg)->varlevelsup);
+						/* New Var has same OLD/NEW returning as old one */
+						newvar->varreturningtype = ((Var *) arg)->varreturningtype;
 						/* New Var is nullable by same rels as the old one */
 						newvar->varnullingrels = ((Var *) arg)->varnullingrels;
 						return (Node *) newvar;
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
new file mode 100644
index 0e35b9d..cdc4054
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1792,8 +1792,8 @@ build_physical_tlist(PlannerInfo *root,
 		case RTE_NAMEDTUPLESTORE:
 		case RTE_RESULT:
 			/* Not all of these can have dropped cols, but share code anyway */
-			expandRTE(rte, varno, 0, -1, true /* include dropped */ ,
-					  NULL, &colvars);
+			expandRTE(rte, varno, 0, VAR_RETURNING_DEFAULT, -1,
+					  true /* include dropped */ , NULL, &colvars);
 			foreach(l, colvars)
 			{
 				var = (Var *) lfirst(l);
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 7a1dfb6..2c0368a
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -74,7 +74,8 @@ static void determineRecursiveColTypes(P
 									   Node *larg, List *nrtargetlist);
 static Query *transformReturnStmt(ParseState *pstate, ReturnStmt *stmt);
 static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt);
-static List *transformReturningList(ParseState *pstate, List *returningList);
+static void transformReturningClause(ParseState *pstate, Query *qry,
+									 ReturningClause *returningClause);
 static Query *transformPLAssignStmt(ParseState *pstate,
 									PLAssignStmt *stmt);
 static Query *transformDeclareCursorStmt(ParseState *pstate,
@@ -553,7 +554,7 @@ transformDeleteStmt(ParseState *pstate,
 	qual = transformWhereClause(pstate, stmt->whereClause,
 								EXPR_KIND_WHERE, "WHERE");
 
-	qry->returningList = transformReturningList(pstate, stmt->returningList);
+	transformReturningClause(pstate, qry, stmt->returningClause);
 
 	/* done building the range table and jointree */
 	qry->rtable = pstate->p_rtable;
@@ -965,7 +966,7 @@ transformInsertStmt(ParseState *pstate,
 	 * contain only the target relation, removing any entries added in a
 	 * sub-SELECT or VALUES list.
 	 */
-	if (stmt->onConflictClause || stmt->returningList)
+	if (stmt->onConflictClause || stmt->returningClause)
 	{
 		pstate->p_namespace = NIL;
 		addNSItemToQuery(pstate, pstate->p_target_nsitem,
@@ -978,9 +979,8 @@ transformInsertStmt(ParseState *pstate,
 													stmt->onConflictClause);
 
 	/* Process RETURNING, if any. */
-	if (stmt->returningList)
-		qry->returningList = transformReturningList(pstate,
-													stmt->returningList);
+	if (stmt->returningClause)
+		transformReturningClause(pstate, qry, stmt->returningClause);
 
 	/* done building the range table and jointree */
 	qry->rtable = pstate->p_rtable;
@@ -2445,7 +2445,7 @@ transformUpdateStmt(ParseState *pstate,
 	qual = transformWhereClause(pstate, stmt->whereClause,
 								EXPR_KIND_WHERE, "WHERE");
 
-	qry->returningList = transformReturningList(pstate, stmt->returningList);
+	transformReturningClause(pstate, qry, stmt->returningClause);
 
 	/*
 	 * Now we are done with SELECT-like processing, and can get on with
@@ -2538,17 +2538,118 @@ transformUpdateTargetList(ParseState *ps
 }
 
 /*
- * transformReturningList -
+ * buildNSItemForReturning -
+ *	add a ParseNamespaceItem for the OLD or NEW alias in RETURNING.
+ */
+static void
+addNSItemForReturning(ParseState *pstate, const char *aliasname,
+					  VarReturningType returning_type)
+{
+	List	   *colnames;
+	int			numattrs;
+	ParseNamespaceColumn *nscolumns;
+	ParseNamespaceItem *nsitem;
+
+	/* copy per-column data from the target relation */
+	colnames = pstate->p_target_nsitem->p_rte->eref->colnames;
+	numattrs = list_length(colnames);
+
+	nscolumns = (ParseNamespaceColumn *)
+		palloc(numattrs * sizeof(ParseNamespaceColumn));
+
+	memcpy(nscolumns, pstate->p_target_nsitem->p_nscolumns,
+		   numattrs * sizeof(ParseNamespaceColumn));
+
+	/* mark all columns as returning OLD/NEW */
+	for (int i = 0; i < numattrs; i++)
+		nscolumns[i].p_varreturningtype = returning_type;
+
+	/* build the nsitem, copying most fields from the target relation */
+	nsitem = (ParseNamespaceItem *) palloc(sizeof(ParseNamespaceItem));
+	nsitem->p_names = makeAlias(aliasname, colnames);
+	nsitem->p_rte = pstate->p_target_nsitem->p_rte;
+	nsitem->p_rtindex = pstate->p_target_nsitem->p_rtindex;
+	nsitem->p_perminfo = pstate->p_target_nsitem->p_perminfo;
+	nsitem->p_nscolumns = nscolumns;
+	nsitem->p_lateral_only = pstate->p_target_nsitem->p_lateral_only;
+	nsitem->p_lateral_ok = pstate->p_target_nsitem->p_lateral_ok;
+	nsitem->p_returning_type = returning_type;
+
+	/* add it to the query namespace as a table-only item */
+	addNSItemToQuery(pstate, nsitem, false, true, false);
+}
+
+/*
+ * transformReturningClause -
  *	handle a RETURNING clause in INSERT/UPDATE/DELETE
  */
-static List *
-transformReturningList(ParseState *pstate, List *returningList)
+static void
+transformReturningClause(ParseState *pstate, Query *qry,
+						 ReturningClause *returningClause)
 {
-	List	   *rlist;
+	ListCell   *lc;
 	int			save_next_resno;
 
-	if (returningList == NIL)
-		return NIL;				/* nothing to do */
+	if (returningClause == NULL)
+		return;					/* nothing to do */
+
+	/*
+	 * Scan RETURNING WITH(...) options for OLD/NEW alias names.  Complain if
+	 * there is any conflict with existing relations.
+	 */
+	foreach(lc, returningClause->options)
+	{
+		ReturningOption *option = lfirst_node(ReturningOption, lc);
+
+		if (refnameNamespaceItem(pstate, NULL, option->name, -1, NULL))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_ALIAS),
+					errmsg("table name \"%s\" specified more than once",
+						   option->name),
+					parser_errposition(pstate, option->location));
+
+		if (option->isNew)
+		{
+			if (qry->returningNew != NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("NEW cannot be specified multiple times"),
+						parser_errposition(pstate, option->location));
+			qry->returningNew = option->name;
+		}
+		else
+		{
+			if (qry->returningOld != NULL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("OLD cannot be specified multiple times"),
+						parser_errposition(pstate, option->location));
+			qry->returningOld = option->name;
+		}
+	}
+
+	/*
+	 * If no OLD/NEW aliases specified, use "old"/"new" unless masked by
+	 * existing relations.
+	 */
+	if (qry->returningOld == NULL &&
+		refnameNamespaceItem(pstate, NULL, "old", -1, NULL) == NULL)
+		qry->returningOld = "old";
+	if (qry->returningNew == NULL &&
+		refnameNamespaceItem(pstate, NULL, "new", -1, NULL) == NULL)
+		qry->returningNew = "new";
+
+	pstate->p_returning_old = qry->returningOld;
+	pstate->p_returning_new = qry->returningNew;
+
+	/*
+	 * Add the OLD and NEW aliases to the query namespace, for use in
+	 * expressions in the RETURNING list.
+	 */
+	if (qry->returningOld)
+		addNSItemForReturning(pstate, qry->returningOld, VAR_RETURNING_OLD);
+	if (qry->returningNew)
+		addNSItemForReturning(pstate, qry->returningNew, VAR_RETURNING_NEW);
 
 	/*
 	 * We need to assign resnos starting at one in the RETURNING list. Save
@@ -2558,8 +2659,10 @@ transformReturningList(ParseState *pstat
 	save_next_resno = pstate->p_next_resno;
 	pstate->p_next_resno = 1;
 
-	/* transform RETURNING identically to a SELECT targetlist */
-	rlist = transformTargetList(pstate, returningList, EXPR_KIND_RETURNING);
+	/* transform RETURNING expressions identically to a SELECT targetlist */
+	qry->returningList = transformTargetList(pstate,
+											 returningClause->exprs,
+											 EXPR_KIND_RETURNING);
 
 	/*
 	 * Complain if the nonempty tlist expanded to nothing (which is possible
@@ -2567,24 +2670,22 @@ transformReturningList(ParseState *pstat
 	 * allow this, the parsed Query will look like it didn't have RETURNING,
 	 * with results that would probably surprise the user.
 	 */
-	if (rlist == NIL)
+	if (qry->returningList == NIL)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("RETURNING must have at least one column"),
 				 parser_errposition(pstate,
-									exprLocation(linitial(returningList)))));
+									exprLocation(linitial(returningClause->exprs)))));
 
 	/* mark column origins */
-	markTargetListOrigins(pstate, rlist);
+	markTargetListOrigins(pstate, qry->returningList);
 
 	/* resolve any still-unresolved output columns as being type text */
 	if (pstate->p_resolve_unknowns)
-		resolveTargetListUnknowns(pstate, rlist);
+		resolveTargetListUnknowns(pstate, qry->returningList);
 
 	/* restore state */
 	pstate->p_next_resno = save_next_resno;
-
-	return rlist;
 }
 
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
new file mode 100644
index 63f172e..9242a8a
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -278,6 +278,7 @@ static Node *makeRecursiveViewSelect(cha
 	MergeWhenClause *mergewhen;
 	struct KeyActions *keyactions;
 	struct KeyAction *keyaction;
+	ReturningClause *retclause;
 }
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
@@ -445,7 +446,8 @@ static Node *makeRecursiveViewSelect(cha
 				opclass_purpose opt_opfamily transaction_mode_list_or_empty
 				OptTableFuncElementList TableFuncElementList opt_type_modifiers
 				prep_type_clause
-				execute_param_clause using_clause returning_clause
+				execute_param_clause using_clause
+				returning_with_clause returning_options
 				opt_enum_val_list enum_val_list table_func_column_list
 				create_generic_options alter_generic_options
 				relation_expr_list dostmt_opt_list
@@ -454,6 +456,9 @@ static Node *makeRecursiveViewSelect(cha
 				vacuum_relation_list opt_vacuum_relation_list
 				drop_option_list pub_obj_list
 
+%type <retclause> returning_clause
+%type <node>	returning_option
+%type <boolean>	returning_option_is_new
 %type <node>	opt_routine_body
 %type <groupclause> group_clause
 %type <list>	group_by_list
@@ -12049,7 +12054,7 @@ InsertStmt:
 				{
 					$5->relation = $4;
 					$5->onConflictClause = $6;
-					$5->returningList = $7;
+					$5->returningClause = $7;
 					$5->withClause = $1;
 					$$ = (Node *) $5;
 				}
@@ -12182,8 +12187,45 @@ opt_conf_expr:
 		;
 
 returning_clause:
-			RETURNING target_list		{ $$ = $2; }
-			| /* EMPTY */				{ $$ = NIL; }
+			RETURNING returning_with_clause target_list
+				{
+					ReturningClause *n = makeNode(ReturningClause);
+
+					n->options = $2;
+					n->exprs = $3;
+					$$ = n;
+				}
+			| /* EMPTY */
+				{
+					$$ = NULL;
+				}
+		;
+
+returning_with_clause:
+			WITH '(' returning_options ')'		{ $$ = $3; }
+			| /* EMPTY */						{ $$ = NIL; }
+		;
+
+returning_options:
+			returning_option							{ $$ = list_make1($1); }
+			| returning_options ',' returning_option	{ $$ = lappend($1, $3); }
+		;
+
+returning_option:
+			returning_option_is_new AS ColId
+				{
+					ReturningOption *n = makeNode(ReturningOption);
+
+					n->isNew = $1;
+					n->name = $3;
+					n->location = @1;
+					$$ = (Node *) n;
+				}
+		;
+
+returning_option_is_new:
+			OLD			{ $$ = false; }
+			| NEW		{ $$ = true; }
 		;
 
 
@@ -12202,7 +12244,7 @@ DeleteStmt: opt_with_clause DELETE_P FRO
 					n->relation = $4;
 					n->usingClause = $5;
 					n->whereClause = $6;
-					n->returningList = $7;
+					n->returningClause = $7;
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
@@ -12276,7 +12318,7 @@ UpdateStmt: opt_with_clause UPDATE relat
 					n->targetList = $5;
 					n->fromClause = $6;
 					n->whereClause = $7;
-					n->returningList = $8;
+					n->returningClause = $8;
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
new file mode 100644
index 334b9b4..4dabb62
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -1581,6 +1581,7 @@ transformFromClauseItem(ParseState *psta
 			jnsitem->p_cols_visible = true;
 			jnsitem->p_lateral_only = false;
 			jnsitem->p_lateral_ok = true;
+			jnsitem->p_returning_type = VAR_RETURNING_DEFAULT;
 			/* Per SQL, we must check for alias conflicts */
 			checkNameSpaceConflicts(pstate, list_make1(jnsitem), my_namespace);
 			my_namespace = lappend(my_namespace, jnsitem);
@@ -1643,6 +1644,7 @@ buildVarFromNSColumn(ParseState *pstate,
 				  nscol->p_varcollid,
 				  0);
 	/* makeVar doesn't offer parameters for these, so set by hand: */
+	var->varreturningtype = nscol->p_varreturningtype;
 	var->varnosyn = nscol->p_varnosyn;
 	var->varattnosyn = nscol->p_varattnosyn;
 
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
new file mode 100644
index 64c582c..eed2f25
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -2574,6 +2574,13 @@ transformWholeRowRef(ParseState *pstate,
 	 * point, there seems no harm in expanding it now rather than during
 	 * planning.
 	 *
+	 * Note that if the nsitem is an OLD/NEW alias for the target RTE (as can
+	 * appear in a RETURNING list), its alias won't match the target RTE's
+	 * alias, but we still want to make a whole-row Var here rather than a
+	 * RowExpr, for consistency with direct references to the target RTE, and
+	 * so that any dropped columns are handled correctly.  Thus we also check
+	 * p_returning_type here.
+	 *
 	 * Note that if the RTE is a function returning scalar, we create just a
 	 * plain reference to the function value, not a composite containing a
 	 * single column.  This is pretty inconsistent at first sight, but it's
@@ -2581,13 +2588,17 @@ transformWholeRowRef(ParseState *pstate,
 	 * "rel.*" mean the same thing for composite relations, so why not for
 	 * scalar functions...
 	 */
-	if (nsitem->p_names == nsitem->p_rte->eref)
+	if (nsitem->p_names == nsitem->p_rte->eref ||
+		nsitem->p_returning_type != VAR_RETURNING_DEFAULT)
 	{
 		Var		   *result;
 
 		result = makeWholeRowVar(nsitem->p_rte, nsitem->p_rtindex,
 								 sublevels_up, true);
 
+		/* mark Var for RETURNING OLD/NEW, as necessary */
+		result->varreturningtype = nsitem->p_returning_type;
+
 		/* location is not filled in by makeWholeRowVar */
 		result->location = location;
 
@@ -2610,9 +2621,8 @@ transformWholeRowRef(ParseState *pstate,
 		 * are in the RTE.  We needn't worry about marking the RTE for SELECT
 		 * access, as the common columns are surely so marked already.
 		 */
-		expandRTE(nsitem->p_rte, nsitem->p_rtindex,
-				  sublevels_up, location, false,
-				  NULL, &fields);
+		expandRTE(nsitem->p_rte, nsitem->p_rtindex, sublevels_up,
+				  nsitem->p_returning_type, location, false, NULL, &fields);
 		rowexpr = makeNode(RowExpr);
 		rowexpr->args = list_truncate(fields,
 									  list_length(nsitem->p_names->colnames));
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
new file mode 100644
index 864ea9b..92c9cd5
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -91,11 +91,13 @@ static void markRTEForSelectPriv(ParseSt
 								 int rtindex, AttrNumber col);
 static void expandRelation(Oid relid, Alias *eref,
 						   int rtindex, int sublevels_up,
+						   VarReturningType returning_type,
 						   int location, bool include_dropped,
 						   List **colnames, List **colvars);
 static void expandTupleDesc(TupleDesc tupdesc, Alias *eref,
 							int count, int offset,
 							int rtindex, int sublevels_up,
+							VarReturningType returning_type,
 							int location, bool include_dropped,
 							List **colnames, List **colvars);
 static int	specialAttNum(const char *attname);
@@ -763,6 +765,9 @@ scanNSItemForColumn(ParseState *pstate,
 	}
 	var->location = location;
 
+	/* Mark Var for RETURNING OLD/NEW, as necessary */
+	var->varreturningtype = nsitem->p_returning_type;
+
 	/* Mark Var if it's nulled by any outer joins */
 	markNullableIfNeeded(pstate, var);
 
@@ -1336,6 +1341,7 @@ buildNSItemFromTupleDesc(RangeTblEntry *
 	nsitem->p_cols_visible = true;
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;
+	nsitem->p_returning_type = VAR_RETURNING_DEFAULT;
 
 	return nsitem;
 }
@@ -1399,6 +1405,7 @@ buildNSItemFromLists(RangeTblEntry *rte,
 	nsitem->p_cols_visible = true;
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;
+	nsitem->p_returning_type = VAR_RETURNING_DEFAULT;
 
 	return nsitem;
 }
@@ -2305,6 +2312,7 @@ addRangeTableEntryForJoin(ParseState *ps
 	nsitem->p_cols_visible = true;
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;
+	nsitem->p_returning_type = VAR_RETURNING_DEFAULT;
 
 	return nsitem;
 }
@@ -2653,9 +2661,10 @@ addNSItemToQuery(ParseState *pstate, Par
  * results.  If include_dropped is true then empty strings and NULL constants
  * (not Vars!) are returned for dropped columns.
  *
- * rtindex, sublevels_up, and location are the varno, varlevelsup, and location
- * values to use in the created Vars.  Ordinarily rtindex should match the
- * actual position of the RTE in its rangetable.
+ * rtindex, sublevels_up, returning_type, and location are the varno,
+ * varlevelsup, varreturningtype, and location values to use in the created
+ * Vars.  Ordinarily rtindex should match the actual position of the RTE in
+ * its rangetable.
  *
  * The output lists go into *colnames and *colvars.
  * If only one of the two kinds of output list is needed, pass NULL for the
@@ -2663,6 +2672,7 @@ addNSItemToQuery(ParseState *pstate, Par
  */
 void
 expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up,
+		  VarReturningType returning_type,
 		  int location, bool include_dropped,
 		  List **colnames, List **colvars)
 {
@@ -2678,7 +2688,7 @@ expandRTE(RangeTblEntry *rte, int rtinde
 		case RTE_RELATION:
 			/* Ordinary relation RTE */
 			expandRelation(rte->relid, rte->eref,
-						   rtindex, sublevels_up, location,
+						   rtindex, sublevels_up, returning_type, location,
 						   include_dropped, colnames, colvars);
 			break;
 		case RTE_SUBQUERY:
@@ -2757,7 +2767,8 @@ expandRTE(RangeTblEntry *rte, int rtinde
 						Assert(tupdesc);
 						expandTupleDesc(tupdesc, rte->eref,
 										rtfunc->funccolcount, atts_done,
-										rtindex, sublevels_up, location,
+										rtindex, sublevels_up,
+										returning_type, location,
 										include_dropped, colnames, colvars);
 					}
 					else if (functypclass == TYPEFUNC_SCALAR)
@@ -3016,6 +3027,7 @@ expandRTE(RangeTblEntry *rte, int rtinde
  */
 static void
 expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up,
+			   VarReturningType returning_type,
 			   int location, bool include_dropped,
 			   List **colnames, List **colvars)
 {
@@ -3024,7 +3036,7 @@ expandRelation(Oid relid, Alias *eref, i
 	/* Get the tupledesc and turn it over to expandTupleDesc */
 	rel = relation_open(relid, AccessShareLock);
 	expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0,
-					rtindex, sublevels_up,
+					rtindex, sublevels_up, returning_type,
 					location, include_dropped,
 					colnames, colvars);
 	relation_close(rel, AccessShareLock);
@@ -3042,6 +3054,7 @@ expandRelation(Oid relid, Alias *eref, i
 static void
 expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset,
 				int rtindex, int sublevels_up,
+				VarReturningType returning_type,
 				int location, bool include_dropped,
 				List **colnames, List **colvars)
 {
@@ -3102,6 +3115,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias
 							  attr->atttypid, attr->atttypmod,
 							  attr->attcollation,
 							  sublevels_up);
+			varnode->varreturningtype = returning_type;
 			varnode->location = location;
 
 			*colvars = lappend(*colvars, varnode);
@@ -3154,6 +3168,7 @@ expandNSItemVars(ParseState *pstate, Par
 						  nscol->p_varcollid,
 						  sublevels_up);
 			/* makeVar doesn't offer parameters for these, so set by hand: */
+			var->varreturningtype = nscol->p_varreturningtype;
 			var->varnosyn = nscol->p_varnosyn;
 			var->varattnosyn = nscol->p_varattnosyn;
 			var->location = location;
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
new file mode 100644
index 3bc62ac..1d1a005
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1534,8 +1534,8 @@ expandRecordVariable(ParseState *pstate,
 				   *lvar;
 		int			i;
 
-		expandRTE(rte, var->varno, 0, var->location, false,
-				  &names, &vars);
+		expandRTE(rte, var->varno, 0, var->varreturningtype,
+				  var->location, false, &names, &vars);
 
 		tupleDesc = CreateTemplateTupleDesc(list_length(vars));
 		i = 1;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 41a3623..53c574a
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -663,15 +663,14 @@ rewriteRuleAction(Query *parsetree,
 					 errmsg("cannot have RETURNING lists in multiple rules")));
 		*returning_flag = true;
 		rule_action->returningList = (List *)
-			ReplaceVarsFromTargetList((Node *) parsetree->returningList,
-									  parsetree->resultRelation,
-									  0,
-									  rt_fetch(parsetree->resultRelation,
-											   parsetree->rtable),
-									  rule_action->returningList,
-									  REPLACEVARS_REPORT_ERROR,
-									  0,
-									  &rule_action->hasSubLinks);
+			ReplaceReturningVarsFromTargetList((Node *) parsetree->returningList,
+											   parsetree->resultRelation,
+											   0,
+											   rt_fetch(parsetree->resultRelation,
+														parsetree->rtable),
+											   rule_action->returningList,
+											   rule_action->resultRelation,
+											   &rule_action->hasSubLinks);
 
 		/*
 		 * There could have been some SubLinks in parsetree's returningList,
@@ -3321,14 +3320,13 @@ rewriteTargetView(Query *parsetree, Rela
 	 * reference the appropriate column of the base relation instead.
 	 */
 	parsetree = (Query *)
-		ReplaceVarsFromTargetList((Node *) parsetree,
-								  parsetree->resultRelation,
-								  0,
-								  view_rte,
-								  view_targetlist,
-								  REPLACEVARS_REPORT_ERROR,
-								  0,
-								  NULL);
+		ReplaceReturningVarsFromTargetList((Node *) parsetree,
+										   parsetree->resultRelation,
+										   0,
+										   view_rte,
+										   view_targetlist,
+										   new_rt_index,
+										   NULL);
 
 	/*
 	 * Update all other RTI references in the query that point to the view
diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c
new file mode 100644
index 32bd2f1..3970861
--- a/src/backend/rewrite/rewriteManip.c
+++ b/src/backend/rewrite/rewriteManip.c
@@ -875,6 +875,68 @@ IncrementVarSublevelsUp_rtable(List *rta
 					   QTW_EXAMINE_RTES_BEFORE);
 }
 
+/*
+ * SetVarReturningType - adjust Vars by setting their returning type.
+ *
+ * Find all Var nodes referring to the specified result relation in the given
+ * expression and set their varreturningtype to the specified value.
+ *
+ * NOTE: although this has the form of a walker, we cheat and modify the
+ * Var nodes in-place.  The given expression tree should have been copied
+ * earlier to ensure that no unwanted side-effects occur!
+ */
+
+typedef struct
+{
+	int			result_relation;
+	int			sublevels_up;
+	VarReturningType returning_type;
+} SetVarReturningType_context;
+
+static bool
+SetVarReturningType_walker(Node *node, SetVarReturningType_context *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, Var))
+	{
+		Var		   *var = (Var *) node;
+
+		if (var->varno == context->result_relation &&
+			var->varlevelsup == context->sublevels_up)
+			var->varreturningtype = context->returning_type;
+
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		/* Recurse into subselects */
+		bool		result;
+
+		context->sublevels_up++;
+		result = query_tree_walker((Query *) node, SetVarReturningType_walker,
+								   (void *) context, 0);
+		context->sublevels_up--;
+		return result;
+	}
+	return expression_tree_walker(node, SetVarReturningType_walker,
+								  (void *) context);
+}
+
+static void
+SetVarReturningType(Node *node, int result_relation, int sublevels_up,
+					VarReturningType returning_type)
+{
+	SetVarReturningType_context context;
+
+	context.result_relation = result_relation;
+	context.sublevels_up = sublevels_up;
+	context.returning_type = returning_type;
+
+	/* Expect to start with an expression */
+	SetVarReturningType_walker(node, &context);
+}
 
 /*
  * rangeTableEntry_used - detect whether an RTE is referenced somewhere
@@ -1675,8 +1737,8 @@ ReplaceVarsFromTargetList_callback(Var *
 		 * the RowExpr for use of the executor and ruleutils.c.
 		 */
 		expandRTE(rcon->target_rte,
-				  var->varno, var->varlevelsup, var->location,
-				  (var->vartype != RECORDOID),
+				  var->varno, var->varlevelsup, VAR_RETURNING_DEFAULT,
+				  var->location, (var->vartype != RECORDOID),
 				  &colnames, &fields);
 		/* Adjust the generated per-field Vars... */
 		fields = (List *) replace_rte_variables_mutator((Node *) fields,
@@ -1778,3 +1840,58 @@ ReplaceVarsFromTargetList(Node *node,
 								 (void *) &context,
 								 outer_hasSubLinks);
 }
+
+
+/*
+ * ReplaceReturningVarsFromTargetList -
+ *	replace RETURNING list Vars with items from a targetlist
+ *
+ * This is equivalent to calling ReplaceVarsFromTargetList() with a
+ * nomatch_option of REPLACEVARS_REPORT_ERROR, but with the added effect of
+ * copying varreturningtype onto any Vars referring to new_result_relation,
+ * allowing RETURNING OLD/NEW to work in the rewritten query.
+ */
+
+typedef struct
+{
+	ReplaceVarsFromTargetList_context rv_con;
+	int			new_result_relation;
+} ReplaceReturningVarsFromTargetList_context;
+
+static Node *
+ReplaceReturningVarsFromTargetList_callback(Var *var,
+											replace_rte_variables_context *context)
+{
+	ReplaceReturningVarsFromTargetList_context *rcon = (ReplaceReturningVarsFromTargetList_context *) context->callback_arg;
+	Node	   *newnode;
+
+	newnode = ReplaceVarsFromTargetList_callback(var, context);
+
+	if (var->varreturningtype != VAR_RETURNING_DEFAULT)
+		SetVarReturningType((Node *) newnode, rcon->new_result_relation,
+							var->varlevelsup, var->varreturningtype);
+
+	return newnode;
+}
+
+Node *
+ReplaceReturningVarsFromTargetList(Node *node,
+								   int target_varno, int sublevels_up,
+								   RangeTblEntry *target_rte,
+								   List *targetlist,
+								   int new_result_relation,
+								   bool *outer_hasSubLinks)
+{
+	ReplaceReturningVarsFromTargetList_context context;
+
+	context.rv_con.target_rte = target_rte;
+	context.rv_con.targetlist = targetlist;
+	context.rv_con.nomatch_option = REPLACEVARS_REPORT_ERROR;
+	context.rv_con.nomatch_varno = 0;
+	context.new_result_relation = new_result_relation;
+
+	return replace_rte_variables(node, target_varno, sublevels_up,
+								 ReplaceReturningVarsFromTargetList_callback,
+								 (void *) &context,
+								 outer_hasSubLinks);
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index ed7f40f..aa5a826
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -117,6 +117,8 @@ typedef struct
 	List	   *namespaces;		/* List of deparse_namespace nodes */
 	List	   *windowClause;	/* Current query level's WINDOW clause */
 	List	   *windowTList;	/* targetlist for resolving WINDOW clause */
+	char	   *returningOld;	/* alias for OLD in RETURNING list */
+	char	   *returningNew;	/* alias for NEW in RETURNING list */
 	int			prettyFlags;	/* enabling of pretty-print functions */
 	int			wrapColumn;		/* max line length, or -1 for no limit */
 	int			indentLevel;	/* current indent level for pretty-print */
@@ -418,6 +420,8 @@ static void get_basic_select_query(Query
 								   TupleDesc resultDesc, bool colNamesVisible);
 static void get_target_list(List *targetList, deparse_context *context,
 							TupleDesc resultDesc, bool colNamesVisible);
+static void get_returning_clause(Query *query, deparse_context *context,
+								 bool colNamesVisible);
 static void get_setop_query(Node *setOp, Query *query,
 							deparse_context *context,
 							TupleDesc resultDesc, bool colNamesVisible);
@@ -1083,6 +1087,8 @@ pg_get_triggerdef_worker(Oid trigid, boo
 		context.namespaces = list_make1(&dpns);
 		context.windowClause = NIL;
 		context.windowTList = NIL;
+		context.returningOld = NULL;
+		context.returningNew = NULL;
 		context.varprefix = true;
 		context.prettyFlags = GET_PRETTY_FLAGS(pretty);
 		context.wrapColumn = WRAP_COLUMN_DEFAULT;
@@ -3636,6 +3642,8 @@ deparse_expression_pretty(Node *expr, Li
 	context.namespaces = dpcontext;
 	context.windowClause = NIL;
 	context.windowTList = NIL;
+	context.returningOld = NULL;
+	context.returningNew = NULL;
 	context.varprefix = forceprefix;
 	context.prettyFlags = prettyFlags;
 	context.wrapColumn = WRAP_COLUMN_DEFAULT;
@@ -4367,8 +4375,8 @@ set_relation_column_names(deparse_namesp
 		if (rte->rtekind == RTE_FUNCTION && rte->functions != NIL)
 		{
 			/* Since we're not creating Vars, rtindex etc. don't matter */
-			expandRTE(rte, 1, 0, -1, true /* include dropped */ ,
-					  &colnames, NULL);
+			expandRTE(rte, 1, 0, VAR_RETURNING_DEFAULT, -1,
+					  true /* include dropped */ , &colnames, NULL);
 		}
 		else
 			colnames = rte->eref->colnames;
@@ -5284,6 +5292,8 @@ make_ruledef(StringInfo buf, HeapTuple r
 		context.namespaces = list_make1(&dpns);
 		context.windowClause = NIL;
 		context.windowTList = NIL;
+		context.returningOld = NULL;
+		context.returningNew = NULL;
 		context.varprefix = (list_length(query->rtable) != 1);
 		context.prettyFlags = prettyFlags;
 		context.wrapColumn = WRAP_COLUMN_DEFAULT;
@@ -5452,6 +5462,8 @@ get_query_def(Query *query, StringInfo b
 	context.namespaces = lcons(&dpns, list_copy(parentnamespace));
 	context.windowClause = NIL;
 	context.windowTList = NIL;
+	context.returningOld = NULL;
+	context.returningNew = NULL;
 	context.varprefix = (parentnamespace != NIL ||
 						 list_length(query->rtable) != 1);
 	context.prettyFlags = prettyFlags;
@@ -6157,6 +6169,52 @@ get_target_list(List *targetList, depars
 }
 
 static void
+get_returning_clause(Query *query, deparse_context *context,
+					 bool colNamesVisible)
+{
+	StringInfo	buf = context->buf;
+
+	if (query->returningList)
+	{
+		char	   *saved_returning_old = context->returningOld;
+		char	   *saved_returning_new = context->returningNew;
+		bool		have_with = false;
+
+		appendContextKeyword(context, " RETURNING",
+							 -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+
+		/* Add WITH options, if they're not the defaults */
+		if (query->returningOld && strcmp(query->returningOld, "old") != 0)
+		{
+			appendStringInfo(buf, " WITH (OLD AS %s", query->returningOld);
+			have_with = true;
+		}
+		if (query->returningNew && strcmp(query->returningNew, "new") != 0)
+		{
+			if (have_with)
+				appendStringInfo(buf, ", ");
+			else
+			{
+				appendStringInfo(buf, " WITH (");
+				have_with = true;
+			}
+			appendStringInfo(buf, "NEW AS %s", query->returningNew);
+		}
+		if (have_with)
+			appendStringInfo(buf, ")");
+
+		/* Add the returning expressions themselves (may refer to OLD/NEW) */
+		context->returningOld = query->returningOld;
+		context->returningNew = query->returningNew;
+
+		get_target_list(query->returningList, context, NULL, colNamesVisible);
+
+		context->returningOld = saved_returning_old;
+		context->returningNew = saved_returning_new;
+	}
+}
+
+static void
 get_setop_query(Node *setOp, Query *query, deparse_context *context,
 				TupleDesc resultDesc, bool colNamesVisible)
 {
@@ -6810,12 +6868,7 @@ get_insert_query_def(Query *query, depar
 	}
 
 	/* Add RETURNING if present */
-	if (query->returningList)
-	{
-		appendContextKeyword(context, " RETURNING",
-							 -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
-		get_target_list(query->returningList, context, NULL, colNamesVisible);
-	}
+	get_returning_clause(query, context, colNamesVisible);
 }
 
 
@@ -6867,12 +6920,7 @@ get_update_query_def(Query *query, depar
 	}
 
 	/* Add RETURNING if present */
-	if (query->returningList)
-	{
-		appendContextKeyword(context, " RETURNING",
-							 -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
-		get_target_list(query->returningList, context, NULL, colNamesVisible);
-	}
+	get_returning_clause(query, context, colNamesVisible);
 }
 
 
@@ -7071,12 +7119,7 @@ get_delete_query_def(Query *query, depar
 	}
 
 	/* Add RETURNING if present */
-	if (query->returningList)
-	{
-		appendContextKeyword(context, " RETURNING",
-							 -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
-		get_target_list(query->returningList, context, NULL, colNamesVisible);
-	}
+	get_returning_clause(query, context, colNamesVisible);
 }
 
 
@@ -7345,7 +7388,13 @@ get_variable(Var *var, int levelsup, boo
 		}
 
 		rte = rt_fetch(varno, dpns->rtable);
-		refname = (char *) list_nth(dpns->rtable_names, varno - 1);
+		if (var->varreturningtype == VAR_RETURNING_OLD)
+			refname = context->returningOld;
+		else if (var->varreturningtype == VAR_RETURNING_NEW)
+			refname = context->returningNew;
+		else
+			refname = (char *) list_nth(dpns->rtable_names, varno - 1);
+
 		colinfo = deparse_columns_fetch(varno, dpns);
 		attnum = varattno;
 	}
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
new file mode 100644
index 048573c..1288da2
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -25,9 +25,9 @@ struct JsonConstructorExprState;
 
 /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */
 /* expression's interpreter has been initialized */
-#define EEO_FLAG_INTERPRETER_INITIALIZED	(1 << 1)
+#define EEO_FLAG_INTERPRETER_INITIALIZED	(1 << 3)
 /* jump-threading is in use */
-#define EEO_FLAG_DIRECT_THREADED			(1 << 2)
+#define EEO_FLAG_DIRECT_THREADED			(1 << 4)
 
 /* Typical API for out-of-line evaluation subroutines */
 typedef void (*ExecEvalSubroutine) (ExprState *state,
@@ -71,16 +71,22 @@ typedef enum ExprEvalOp
 	EEOP_INNER_FETCHSOME,
 	EEOP_OUTER_FETCHSOME,
 	EEOP_SCAN_FETCHSOME,
+	EEOP_OLD_FETCHSOME,
+	EEOP_NEW_FETCHSOME,
 
 	/* compute non-system Var value */
 	EEOP_INNER_VAR,
 	EEOP_OUTER_VAR,
 	EEOP_SCAN_VAR,
+	EEOP_OLD_VAR,
+	EEOP_NEW_VAR,
 
 	/* compute system Var value */
 	EEOP_INNER_SYSVAR,
 	EEOP_OUTER_SYSVAR,
 	EEOP_SCAN_SYSVAR,
+	EEOP_OLD_SYSVAR,
+	EEOP_NEW_SYSVAR,
 
 	/* compute wholerow Var */
 	EEOP_WHOLEROW,
@@ -93,6 +99,8 @@ typedef enum ExprEvalOp
 	EEOP_ASSIGN_INNER_VAR,
 	EEOP_ASSIGN_OUTER_VAR,
 	EEOP_ASSIGN_SCAN_VAR,
+	EEOP_ASSIGN_OLD_VAR,
+	EEOP_ASSIGN_NEW_VAR,
 
 	/* assign ExprState's resvalue/resnull to a column of its resultslot */
 	EEOP_ASSIGN_TMP,
diff --git a/src/include/executor/tuptable.h b/src/include/executor/tuptable.h
new file mode 100644
index 5250be5..d4636eb
--- a/src/include/executor/tuptable.h
+++ b/src/include/executor/tuptable.h
@@ -411,12 +411,21 @@ slot_getsysattr(TupleTableSlot *slot, in
 {
 	Assert(attnum < 0);			/* caller error */
 
+	/*
+	 * If the tid is not valid, there is no physical row, and all system
+	 * attributes are deemed to be NULL, except for the tableoid.
+	 */
 	if (attnum == TableOidAttributeNumber)
 	{
 		*isnull = false;
 		return ObjectIdGetDatum(slot->tts_tableOid);
 	}
-	else if (attnum == SelfItemPointerAttributeNumber)
+	if (!ItemPointerIsValid(&slot->tts_tid))
+	{
+		*isnull = true;
+		return PointerGetDatum(NULL);
+	}
+	if (attnum == SelfItemPointerAttributeNumber)
 	{
 		*isnull = false;
 		return PointerGetDatum(&slot->tts_tid);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 5d7f17d..2664966
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -73,6 +73,10 @@ typedef Datum (*ExprStateEvalFunc) (stru
 /* Bits in ExprState->flags (see also execExpr.h for private flag bits): */
 /* expression is for use with ExecQual() */
 #define EEO_FLAG_IS_QUAL					(1 << 0)
+/* expression refers to OLD table columns (used in RETURNING lists) */
+#define EEO_FLAG_HAS_OLD					(1 << 1)
+/* expression refers to NEW table columns (used in RETURNING lists) */
+#define EEO_FLAG_HAS_NEW					(1 << 2)
 
 typedef struct ExprState
 {
@@ -280,6 +284,12 @@ typedef struct ExprContext
 #define FIELDNO_EXPRCONTEXT_DOMAINNULL 13
 	bool		domainValue_isNull;
 
+	/* Tuples that OLD/NEW Var nodes in RETURNING may refer to */
+#define FIELDNO_EXPRCONTEXT_OLDTUPLE 14
+	TupleTableSlot *ecxt_oldtuple;
+#define FIELDNO_EXPRCONTEXT_NEWTUPLE 15
+	TupleTableSlot *ecxt_newtuple;
+
 	/* Link to containing EState (NULL if a standalone ExprContext) */
 	struct EState *ecxt_estate;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index e494309..f9deabd
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -185,6 +185,8 @@ typedef struct Query
 
 	OnConflictExpr *onConflict; /* ON CONFLICT DO [NOTHING | UPDATE] */
 
+	char	   *returningOld;	/* alias for OLD in RETURNING list */
+	char	   *returningNew;	/* alias for NEW in RETURNING list */
 	List	   *returningList;	/* return-values list (of TargetEntry) */
 
 	List	   *groupClause;	/* a list of SortGroupClause's */
@@ -1675,6 +1677,32 @@ typedef struct MergeWhenClause
 } MergeWhenClause;
 
 /*
+ * ReturningOption -
+ *		Option in RETURNING WITH(...) list
+ *
+ * Currently, this is used only for specifying the OLD/NEW aliases available
+ * for use in the RETURNING expression list.
+ */
+typedef struct ReturningOption
+{
+	NodeTag		type;
+	bool		isNew;
+	char	   *name;
+	int			location;
+} ReturningOption;
+
+/*
+ * ReturningClause -
+ *		List of RETURNING expressions, together with any WITH(...) options
+ */
+typedef struct ReturningClause
+{
+	NodeTag		type;
+	List	   *options;		/* list of ReturningOption elements */
+	List	   *exprs;			/* list of expressions to return */
+} ReturningClause;
+
+/*
  * TriggerTransition -
  *	   representation of transition row or table naming clause
  *
@@ -1882,7 +1910,7 @@ typedef struct InsertStmt
 	List	   *cols;			/* optional: names of the target columns */
 	Node	   *selectStmt;		/* the source SELECT/VALUES, or NULL */
 	OnConflictClause *onConflictClause; /* ON CONFLICT clause */
-	List	   *returningList;	/* list of expressions to return */
+	ReturningClause *returningClause;	/* RETURNING clause */
 	WithClause *withClause;		/* WITH clause */
 	OverridingKind override;	/* OVERRIDING clause */
 } InsertStmt;
@@ -1897,7 +1925,7 @@ typedef struct DeleteStmt
 	RangeVar   *relation;		/* relation to delete from */
 	List	   *usingClause;	/* optional using clause for more tables */
 	Node	   *whereClause;	/* qualifications */
-	List	   *returningList;	/* list of expressions to return */
+	ReturningClause *returningClause;	/* RETURNING clause */
 	WithClause *withClause;		/* WITH clause */
 } DeleteStmt;
 
@@ -1912,7 +1940,7 @@ typedef struct UpdateStmt
 	List	   *targetList;		/* the target list (of ResTarget) */
 	Node	   *whereClause;	/* qualifications */
 	List	   *fromClause;		/* optional from clause for more tables */
-	List	   *returningList;	/* list of expressions to return */
+	ReturningClause *returningClause;	/* RETURNING clause */
 	WithClause *withClause;		/* WITH clause */
 } UpdateStmt;
 
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
new file mode 100644
index bb930af..2022208
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -209,6 +209,11 @@ typedef struct Expr
  * Note that it affects the meaning of all of varno, varnullingrels, and
  * varnosyn, all of which refer to the range table of that query level.
  *
+ * varreturningtype is used for Vars in the RETURNING list of data-modifying
+ * queries, for Vars that refer to the target relation.  For such Vars, there
+ * are 3 possible behaviors, depending on whether the target relation was
+ * referred to directly, or via the OLD or NEW aliases.
+ *
  * In the parser, varnosyn and varattnosyn are either identical to
  * varno/varattno, or they specify the column's position in an aliased JOIN
  * RTE that hides the semantic referent RTE's refname.  This is a syntactic
@@ -230,6 +235,14 @@ typedef struct Expr
 #define    PRS2_OLD_VARNO			1
 #define    PRS2_NEW_VARNO			2
 
+/* Returning behavior for Vars in RETURNING list */
+typedef enum VarReturningType
+{
+	VAR_RETURNING_DEFAULT,		/* return OLD for DELETE, else return NEW */
+	VAR_RETURNING_OLD,			/* return OLD for DELETE/UPDATE, else NULL */
+	VAR_RETURNING_NEW,			/* return NEW for INSERT/UPDATE, else NULL */
+} VarReturningType;
+
 typedef struct Var
 {
 	Expr		xpr;
@@ -265,6 +278,9 @@ typedef struct Var
 	 */
 	Index		varlevelsup;
 
+	/* returning type of this var (see above) */
+	VarReturningType varreturningtype;
+
 	/*
 	 * varnosyn/varattnosyn are ignored for equality, because Vars with
 	 * different syntactic identifiers are semantically the same as long as
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
new file mode 100644
index f589112..18483e8
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -175,6 +175,10 @@ typedef Node *(*CoerceParamHook) (ParseS
  * p_resolve_unknowns: resolve unknown-type SELECT output columns as type TEXT
  * (this is true by default).
  *
+ * p_returning_old: alias for OLD in RETURNING list, or NULL.
+ *
+ * p_returning_new: alias for NEW in RETURNING list, or NULL.
+ *
  * p_hasAggs, p_hasWindowFuncs, etc: true if we've found any of the indicated
  * constructs in the query.
  *
@@ -215,6 +219,8 @@ struct ParseState
 										 * with FOR UPDATE/FOR SHARE */
 	bool		p_resolve_unknowns; /* resolve unknown-type SELECT outputs as
 									 * type text */
+	char	   *p_returning_old;	/* alias for OLD in RETURNING list */
+	char	   *p_returning_new;	/* alias for NEW in RETURNING list */
 
 	QueryEnvironment *p_queryEnv;	/* curr env, incl refs to enclosing env */
 
@@ -275,6 +281,11 @@ struct ParseState
  * of SQL:2008 requires us to do it this way.  We also use p_lateral_ok to
  * forbid LATERAL references to an UPDATE/DELETE target table.
  *
+ * While processing the RETURNING clause, special namespace items are added to
+ * refer to the OLD and NEW state of the result relation.  These namespace
+ * items have p_returning_type set appropriately, for use when creating Vars.
+ * For convenience, this information is duplicated on each namespace column.
+ *
  * At no time should a namespace list contain two entries that conflict
  * according to the rules in checkNameSpaceConflicts; but note that those
  * are more complicated than "must have different alias names", so in practice
@@ -292,6 +303,7 @@ struct ParseNamespaceItem
 	bool		p_cols_visible; /* Column names visible as unqualified refs? */
 	bool		p_lateral_only; /* Is only visible to LATERAL expressions? */
 	bool		p_lateral_ok;	/* If so, does join type allow use? */
+	VarReturningType p_returning_type;	/* Is OLD/NEW for use in RETURNING? */
 };
 
 /*
@@ -322,6 +334,7 @@ struct ParseNamespaceColumn
 	Oid			p_vartype;		/* pg_type OID */
 	int32		p_vartypmod;	/* type modifier value */
 	Oid			p_varcollid;	/* OID of collation, or InvalidOid */
+	VarReturningType p_varreturningtype;	/* for RETURNING OLD/NEW */
 	Index		p_varnosyn;		/* rangetable index of syntactic referent */
 	AttrNumber	p_varattnosyn;	/* attribute number of syntactic referent */
 	bool		p_dontexpand;	/* not included in star expansion */
diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h
new file mode 100644
index 67d9b1e..55355ea
--- a/src/include/parser/parse_relation.h
+++ b/src/include/parser/parse_relation.h
@@ -112,6 +112,7 @@ extern void errorMissingRTE(ParseState *
 extern void errorMissingColumn(ParseState *pstate,
 							   const char *relname, const char *colname, int location) pg_attribute_noreturn();
 extern void expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up,
+					  VarReturningType returning_type,
 					  int location, bool include_dropped,
 					  List **colnames, List **colvars);
 extern List *expandNSItemVars(ParseState *pstate, ParseNamespaceItem *nsitem,
diff --git a/src/include/rewrite/rewriteManip.h b/src/include/rewrite/rewriteManip.h
new file mode 100644
index ca12780..036f2de
--- a/src/include/rewrite/rewriteManip.h
+++ b/src/include/rewrite/rewriteManip.h
@@ -93,4 +93,12 @@ extern Node *ReplaceVarsFromTargetList(N
 									   int nomatch_varno,
 									   bool *outer_hasSubLinks);
 
+extern Node *ReplaceReturningVarsFromTargetList(Node *node,
+												int target_varno,
+												int sublevels_up,
+												RangeTblEntry *target_rte,
+												List *targetlist,
+												int new_result_relation,
+												bool *outer_hasSubLinks);
+
 #endif							/* REWRITEMANIP_H */
diff --git a/src/interfaces/ecpg/preproc/parse.pl b/src/interfaces/ecpg/preproc/parse.pl
new file mode 100644
index b0caeb3..292347d
--- a/src/interfaces/ecpg/preproc/parse.pl
+++ b/src/interfaces/ecpg/preproc/parse.pl
@@ -118,8 +118,8 @@ my %replace_line = (
 	  'SHOW TRANSACTION ISOLATION LEVEL ecpg_into',
 	'VariableShowStmtSHOWSESSIONAUTHORIZATION' =>
 	  'SHOW SESSION AUTHORIZATION ecpg_into',
-	'returning_clauseRETURNINGtarget_list' =>
-	  'RETURNING target_list opt_ecpg_into',
+	'returning_clauseRETURNINGreturning_with_clausetarget_list' =>
+	  'RETURNING returning_with_clause target_list opt_ecpg_into',
 	'ExecuteStmtEXECUTEnameexecute_param_clause' =>
 	  'EXECUTE prepared_name execute_param_clause execute_rest',
 	'ExecuteStmtCREATEOptTempTABLEcreate_as_targetASEXECUTEnameexecute_param_clauseopt_with_data'
diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out
new file mode 100644
index cb51bb8..fa3198e
--- a/src/test/regress/expected/returning.out
+++ b/src/test/regress/expected/returning.out
@@ -355,3 +355,229 @@ INSERT INTO foo AS bar DEFAULT VALUES RE
  42
 (1 row)
 
+--
+-- Test RETURNING OLD/NEW.
+--
+-- Start with new data, to ensure predictable TIDs.
+--
+TRUNCATE foo;
+INSERT INTO foo VALUES (1, 'xxx', 10, 20), (2, 'more', 42, 141), (3, 'zoo2', 57, 99);
+-- Error cases
+INSERT INTO foo DEFAULT VALUES RETURNING WITH (nonsuch AS something) *;
+ERROR:  syntax error at or near "nonsuch"
+LINE 1: INSERT INTO foo DEFAULT VALUES RETURNING WITH (nonsuch AS so...
+                                                       ^
+INSERT INTO foo DEFAULT VALUES RETURNING WITH (new AS foo) *;
+ERROR:  table name "foo" specified more than once
+LINE 1: INSERT INTO foo DEFAULT VALUES RETURNING WITH (new AS foo) *...
+                                                       ^
+INSERT INTO foo DEFAULT VALUES RETURNING WITH (old AS o, new AS n, old AS o) *;
+ERROR:  OLD cannot be specified multiple times
+LINE 1: ...EFAULT VALUES RETURNING WITH (old AS o, new AS n, old AS o) ...
+                                                             ^
+-- INSERT has NEW, but not OLD
+INSERT INTO foo VALUES (4)
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+ tableoid | ctid | f1 | f2 | f3 | f4 | tableoid | ctid  | f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4 
+----------+------+----+----+----+----+----------+-------+----+----+----+----+----+----+----+----
+ foo      |      |    |    |    |    | foo      | (0,4) |  4 |    | 42 | 99 |  4 |    | 42 | 99
+(1 row)
+
+-- INSERT ... ON CONFLICT ... UPDATE has OLD and NEW
+CREATE UNIQUE INDEX foo_f1_idx ON foo (f1);
+INSERT INTO foo VALUES (4, 'conflict'), (5, 'ok')
+  ON CONFLICT (f1) DO UPDATE SET f2 = excluded.f2||'ed', f3 = -1
+  RETURNING WITH (OLD AS o, NEW AS n)
+            o.tableoid::regclass, o.ctid, o.*,
+            n.tableoid::regclass, n.ctid, n.*, *;
+ tableoid | ctid  | f1 | f2 | f3 | f4 | tableoid | ctid  | f1 |     f2     | f3 | f4 | f1 |     f2     | f3 | f4 
+----------+-------+----+----+----+----+----------+-------+----+------------+----+----+----+------------+----+----
+ foo      | (0,4) |  4 |    | 42 | 99 | foo      | (0,5) |  4 | conflicted | -1 | 99 |  4 | conflicted | -1 | 99
+ foo      |       |    |    |    |    | foo      | (0,6) |  5 | ok         | 42 | 99 |  5 | ok         | 42 | 99
+(2 rows)
+
+-- UPDATE has OLD and NEW
+UPDATE foo SET f4 = 100 WHERE f1 = 5
+  RETURNING old.tableoid::regclass, old.ctid, old.*, old,
+            new.tableoid::regclass, new.ctid, new.*, new,
+            old.f4::text||'->'||new.f4::text AS change;
+ tableoid | ctid  | f1 | f2 | f3 | f4 |     old      | tableoid | ctid  | f1 | f2 | f3 | f4  |      new      | change  
+----------+-------+----+----+----+----+--------------+----------+-------+----+----+----+-----+---------------+---------
+ foo      | (0,6) |  5 | ok | 42 | 99 | (5,ok,42,99) | foo      | (0,7) |  5 | ok | 42 | 100 | (5,ok,42,100) | 99->100
+(1 row)
+
+-- DELETE has OLD, but not NEW
+DELETE FROM foo WHERE f1 = 5
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+ tableoid | ctid  | f1 | f2 | f3 | f4  | tableoid | ctid | f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4  
+----------+-------+----+----+----+-----+----------+------+----+----+----+----+----+----+----+-----
+ foo      | (0,7) |  5 | ok | 42 | 100 | foo      |      |    |    |    |    |  5 | ok | 42 | 100
+(1 row)
+
+-- DELETE turned into UPDATE by a rule has OLD and NEW
+CREATE RULE foo_del_rule AS ON DELETE TO foo DO INSTEAD
+  UPDATE foo SET f2 = f2||' (deleted)', f3 = -1, f4 = -1 WHERE f1 = OLD.f1
+  RETURNING *;
+DELETE FROM foo WHERE f1 = 4 RETURNING old.*,new.*, *;
+ f1 |     f2     | f3 | f4 | f1 |          f2          | f3 | f4 | f1 |          f2          | f3 | f4 
+----+------------+----+----+----+----------------------+----+----+----+----------------------+----+----
+  4 | conflicted | -1 | 99 |  4 | conflicted (deleted) | -1 | -1 |  4 | conflicted (deleted) | -1 | -1
+(1 row)
+
+-- UPDATE on view with rule
+UPDATE joinview SET f3 = f3 + 1 WHERE f3 = 57
+  RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3;
+ f1 |  f2  | f3 | f4 | other | f1 |  f2  | f3 | f4 | other | f1 |  f2  | f3 | f4 | other | delta_f3 
+----+------+----+----+-------+----+------+----+----+-------+----+------+----+----+-------+----------
+  3 | zoo2 | 57 | 99 | 54321 |  3 | zoo2 | 58 | 99 | 54321 |  3 | zoo2 | 58 | 99 | 54321 |        1
+(1 row)
+
+-- UPDATE on view with INSTEAD OF trigger
+CREATE FUNCTION joinview_upd_trig_fn() RETURNS trigger
+LANGUAGE plpgsql AS
+$$
+BEGIN
+  RAISE NOTICE 'UPDATE: % -> %', old, new;
+  UPDATE foo SET f1 = new.f1, f3 = new.f3, f4 = new.f4 * 10
+    FROM joinme WHERE f2 = f2j AND f2 = old.f2
+    RETURNING new.f1, new.f4 INTO new.f1, new.f4;  -- should fail
+  RETURN NEW;
+END;
+$$;
+CREATE TRIGGER joinview_upd_trig INSTEAD OF UPDATE ON joinview
+  FOR EACH ROW EXECUTE FUNCTION joinview_upd_trig_fn();
+DROP RULE joinview_u ON joinview;
+UPDATE joinview SET f3 = f3 + 1, f4 = 7 WHERE f3 = 58
+  RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3;  -- should fail
+NOTICE:  UPDATE: (3,zoo2,58,99,54321) -> (3,zoo2,59,7,54321)
+ERROR:  column reference "new.f1" is ambiguous
+LINE 3:     RETURNING new.f1, new.f4
+                      ^
+DETAIL:  It could refer to either a PL/pgSQL variable or a table column.
+QUERY:  UPDATE foo SET f1 = new.f1, f3 = new.f3, f4 = new.f4 * 10
+    FROM joinme WHERE f2 = f2j AND f2 = old.f2
+    RETURNING new.f1, new.f4
+CONTEXT:  PL/pgSQL function joinview_upd_trig_fn() line 4 at SQL statement
+CREATE OR REPLACE FUNCTION joinview_upd_trig_fn() RETURNS trigger
+LANGUAGE plpgsql AS
+$$
+BEGIN
+  RAISE NOTICE 'UPDATE: % -> %', old, new;
+  UPDATE foo SET f1 = new.f1, f3 = new.f3, f4 = new.f4 * 10
+    FROM joinme WHERE f2 = f2j AND f2 = old.f2
+    RETURNING WITH (new AS n) new.f1, n.f4 INTO new.f1, new.f4;  -- now ok
+  RETURN NEW;
+END;
+$$;
+UPDATE joinview SET f3 = f3 + 1, f4 = 7 WHERE f3 = 58
+  RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3;  -- should succeed
+NOTICE:  UPDATE: (3,zoo2,58,99,54321) -> (3,zoo2,59,7,54321)
+ f1 |  f2  | f3 | f4 | other | f1 |  f2  | f3 | f4 | other | f1 |  f2  | f3 | f4 | other | delta_f3 
+----+------+----+----+-------+----+------+----+----+-------+----+------+----+----+-------+----------
+  3 | zoo2 | 58 | 99 | 54321 |  3 | zoo2 | 59 | 70 | 54321 |  3 | zoo2 | 59 | 70 | 54321 |        1
+(1 row)
+
+-- Test wholerow & dropped column handling
+ALTER TABLE foo DROP COLUMN f3 CASCADE;
+NOTICE:  drop cascades to 3 other objects
+DETAIL:  drop cascades to rule voo_i on view voo
+drop cascades to view joinview
+drop cascades to rule foo_del_rule on table foo
+UPDATE foo SET f4 = f4 + 1 RETURNING old.f3;  -- should fail
+ERROR:  column old.f3 does not exist
+LINE 1: UPDATE foo SET f4 = f4 + 1 RETURNING old.f3;
+                                             ^
+UPDATE foo SET f4 = f4 + 1 RETURNING old, new;
+              old              |             new              
+-------------------------------+------------------------------
+ (1,xxx,20)                    | (1,xxx,21)
+ (2,more,141)                  | (2,more,142)
+ (4,"conflicted (deleted)",-1) | (4,"conflicted (deleted)",0)
+ (3,zoo2,70)                   | (3,zoo2,71)
+(4 rows)
+
+-- INSERT/DELETE on zero column table
+CREATE TABLE zerocol();
+INSERT INTO zerocol SELECT
+  RETURNING old.tableoid::regclass, old.ctid,
+            new.tableoid::regclass, new.ctid, ctid, *;
+ tableoid | ctid | tableoid | ctid  | ctid  
+----------+------+----------+-------+-------
+ zerocol  |      | zerocol  | (0,1) | (0,1)
+(1 row)
+
+DELETE FROM zerocol
+  RETURNING old.tableoid::regclass, old.ctid,
+            new.tableoid::regclass, new.ctid, ctid, *;
+ tableoid | ctid  | tableoid | ctid | ctid  
+----------+-------+----------+------+-------
+ zerocol  | (0,1) | zerocol  |      | (0,1)
+(1 row)
+
+DROP TABLE zerocol;
+-- Test cross-partition updates and attribute mapping
+CREATE TABLE foo_parted (a int, b float8, c text) PARTITION BY LIST (a);
+CREATE TABLE foo_part_s1 PARTITION OF foo_parted FOR VALUES IN (1);
+CREATE TABLE foo_part_s2 PARTITION OF foo_parted FOR VALUES IN (2);
+CREATE TABLE foo_part_d1 (c text, a int, b float8);
+ALTER TABLE foo_parted ATTACH PARTITION foo_part_d1 FOR VALUES IN (3);
+CREATE TABLE foo_part_d2 (b float8, c text, a int);
+ALTER TABLE foo_parted ATTACH PARTITION foo_part_d2 FOR VALUES IN (4);
+INSERT INTO foo_parted
+  VALUES (1, 17.1, 'P1'), (2, 17.2, 'P2'), (3, 17.3, 'P3'), (4, 17.4, 'P4')
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+  tableoid   | ctid | a | b | c |  tableoid   | ctid  | a |  b   | c  | a |  b   | c  
+-------------+------+---+---+---+-------------+-------+---+------+----+---+------+----
+ foo_part_s1 |      |   |   |   | foo_part_s1 | (0,1) | 1 | 17.1 | P1 | 1 | 17.1 | P1
+ foo_part_s2 |      |   |   |   | foo_part_s2 | (0,1) | 2 | 17.2 | P2 | 2 | 17.2 | P2
+ foo_part_d1 |      |   |   |   | foo_part_d1 | (0,1) | 3 | 17.3 | P3 | 3 | 17.3 | P3
+ foo_part_d2 |      |   |   |   | foo_part_d2 | (0,1) | 4 | 17.4 | P4 | 4 | 17.4 | P4
+(4 rows)
+
+UPDATE foo_parted SET a = 2, b = b + 1, c = c || '->P2' WHERE a = 1
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+  tableoid   | ctid  | a |  b   | c  |  tableoid   | ctid  | a |  b   |   c    | a |  b   |   c    
+-------------+-------+---+------+----+-------------+-------+---+------+--------+---+------+--------
+ foo_part_s1 | (0,1) | 1 | 17.1 | P1 | foo_part_s2 | (0,2) | 2 | 18.1 | P1->P2 | 2 | 18.1 | P1->P2
+(1 row)
+
+UPDATE foo_parted SET a = 1, b = b + 1, c = c || '->P1' WHERE a = 3
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+  tableoid   | ctid  | a |  b   | c  |  tableoid   | ctid  | a |  b   |   c    | a |  b   |   c    
+-------------+-------+---+------+----+-------------+-------+---+------+--------+---+------+--------
+ foo_part_d1 | (0,1) | 3 | 17.3 | P3 | foo_part_s1 | (0,2) | 1 | 18.3 | P3->P1 | 1 | 18.3 | P3->P1
+(1 row)
+
+UPDATE foo_parted SET a = 3, b = b + 1, c = c || '->P3' WHERE a = 1
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+  tableoid   | ctid  | a |  b   |   c    |  tableoid   | ctid  | a |  b   |     c      | a |  b   |     c      
+-------------+-------+---+------+--------+-------------+-------+---+------+------------+---+------+------------
+ foo_part_s1 | (0,2) | 1 | 18.3 | P3->P1 | foo_part_d1 | (0,2) | 3 | 19.3 | P3->P1->P3 | 3 | 19.3 | P3->P1->P3
+(1 row)
+
+UPDATE foo_parted SET a = 4, b = b + 1, c = c || '->P4' WHERE a = 3
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+  tableoid   | ctid  | a |  b   |     c      |  tableoid   | ctid  | a |  b   |       c        | a |  b   |       c        
+-------------+-------+---+------+------------+-------------+-------+---+------+----------------+---+------+----------------
+ foo_part_d1 | (0,2) | 3 | 19.3 | P3->P1->P3 | foo_part_d2 | (0,2) | 4 | 20.3 | P3->P1->P3->P4 | 4 | 20.3 | P3->P1->P3->P4
+(1 row)
+
+DELETE FROM foo_parted
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+  tableoid   | ctid  | a |  b   |       c        |  tableoid   | ctid | a | b | c | a |  b   |       c        
+-------------+-------+---+------+----------------+-------------+------+---+---+---+---+------+----------------
+ foo_part_s2 | (0,1) | 2 | 17.2 | P2             | foo_part_s2 |      |   |   |   | 2 | 17.2 | P2
+ foo_part_s2 | (0,2) | 2 | 18.1 | P1->P2         | foo_part_s2 |      |   |   |   | 2 | 18.1 | P1->P2
+ foo_part_d2 | (0,1) | 4 | 17.4 | P4             | foo_part_d2 |      |   |   |   | 4 | 17.4 | P4
+ foo_part_d2 | (0,2) | 4 | 20.3 | P3->P1->P3->P4 | foo_part_d2 |      |   |   |   | 4 | 20.3 | P3->P1->P3->P4
+(4 rows)
+
+DROP TABLE foo_parted;
diff --git a/src/test/regress/sql/returning.sql b/src/test/regress/sql/returning.sql
new file mode 100644
index a460f82..6b699a8
--- a/src/test/regress/sql/returning.sql
+++ b/src/test/regress/sql/returning.sql
@@ -160,3 +160,133 @@ INSERT INTO foo AS bar DEFAULT VALUES RE
 INSERT INTO foo AS bar DEFAULT VALUES RETURNING foo.*; -- fails, wrong name
 INSERT INTO foo AS bar DEFAULT VALUES RETURNING bar.*; -- ok
 INSERT INTO foo AS bar DEFAULT VALUES RETURNING bar.f3; -- ok
+
+--
+-- Test RETURNING OLD/NEW.
+--
+-- Start with new data, to ensure predictable TIDs.
+--
+TRUNCATE foo;
+INSERT INTO foo VALUES (1, 'xxx', 10, 20), (2, 'more', 42, 141), (3, 'zoo2', 57, 99);
+
+-- Error cases
+INSERT INTO foo DEFAULT VALUES RETURNING WITH (nonsuch AS something) *;
+INSERT INTO foo DEFAULT VALUES RETURNING WITH (new AS foo) *;
+INSERT INTO foo DEFAULT VALUES RETURNING WITH (old AS o, new AS n, old AS o) *;
+
+-- INSERT has NEW, but not OLD
+INSERT INTO foo VALUES (4)
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+
+-- INSERT ... ON CONFLICT ... UPDATE has OLD and NEW
+CREATE UNIQUE INDEX foo_f1_idx ON foo (f1);
+INSERT INTO foo VALUES (4, 'conflict'), (5, 'ok')
+  ON CONFLICT (f1) DO UPDATE SET f2 = excluded.f2||'ed', f3 = -1
+  RETURNING WITH (OLD AS o, NEW AS n)
+            o.tableoid::regclass, o.ctid, o.*,
+            n.tableoid::regclass, n.ctid, n.*, *;
+
+-- UPDATE has OLD and NEW
+UPDATE foo SET f4 = 100 WHERE f1 = 5
+  RETURNING old.tableoid::regclass, old.ctid, old.*, old,
+            new.tableoid::regclass, new.ctid, new.*, new,
+            old.f4::text||'->'||new.f4::text AS change;
+
+-- DELETE has OLD, but not NEW
+DELETE FROM foo WHERE f1 = 5
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+
+-- DELETE turned into UPDATE by a rule has OLD and NEW
+CREATE RULE foo_del_rule AS ON DELETE TO foo DO INSTEAD
+  UPDATE foo SET f2 = f2||' (deleted)', f3 = -1, f4 = -1 WHERE f1 = OLD.f1
+  RETURNING *;
+DELETE FROM foo WHERE f1 = 4 RETURNING old.*,new.*, *;
+
+-- UPDATE on view with rule
+UPDATE joinview SET f3 = f3 + 1 WHERE f3 = 57
+  RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3;
+
+-- UPDATE on view with INSTEAD OF trigger
+CREATE FUNCTION joinview_upd_trig_fn() RETURNS trigger
+LANGUAGE plpgsql AS
+$$
+BEGIN
+  RAISE NOTICE 'UPDATE: % -> %', old, new;
+  UPDATE foo SET f1 = new.f1, f3 = new.f3, f4 = new.f4 * 10
+    FROM joinme WHERE f2 = f2j AND f2 = old.f2
+    RETURNING new.f1, new.f4 INTO new.f1, new.f4;  -- should fail
+  RETURN NEW;
+END;
+$$;
+CREATE TRIGGER joinview_upd_trig INSTEAD OF UPDATE ON joinview
+  FOR EACH ROW EXECUTE FUNCTION joinview_upd_trig_fn();
+DROP RULE joinview_u ON joinview;
+UPDATE joinview SET f3 = f3 + 1, f4 = 7 WHERE f3 = 58
+  RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3;  -- should fail
+
+CREATE OR REPLACE FUNCTION joinview_upd_trig_fn() RETURNS trigger
+LANGUAGE plpgsql AS
+$$
+BEGIN
+  RAISE NOTICE 'UPDATE: % -> %', old, new;
+  UPDATE foo SET f1 = new.f1, f3 = new.f3, f4 = new.f4 * 10
+    FROM joinme WHERE f2 = f2j AND f2 = old.f2
+    RETURNING WITH (new AS n) new.f1, n.f4 INTO new.f1, new.f4;  -- now ok
+  RETURN NEW;
+END;
+$$;
+UPDATE joinview SET f3 = f3 + 1, f4 = 7 WHERE f3 = 58
+  RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3;  -- should succeed
+
+-- Test wholerow & dropped column handling
+ALTER TABLE foo DROP COLUMN f3 CASCADE;
+UPDATE foo SET f4 = f4 + 1 RETURNING old.f3;  -- should fail
+UPDATE foo SET f4 = f4 + 1 RETURNING old, new;
+
+-- INSERT/DELETE on zero column table
+CREATE TABLE zerocol();
+INSERT INTO zerocol SELECT
+  RETURNING old.tableoid::regclass, old.ctid,
+            new.tableoid::regclass, new.ctid, ctid, *;
+DELETE FROM zerocol
+  RETURNING old.tableoid::regclass, old.ctid,
+            new.tableoid::regclass, new.ctid, ctid, *;
+DROP TABLE zerocol;
+
+-- Test cross-partition updates and attribute mapping
+CREATE TABLE foo_parted (a int, b float8, c text) PARTITION BY LIST (a);
+CREATE TABLE foo_part_s1 PARTITION OF foo_parted FOR VALUES IN (1);
+CREATE TABLE foo_part_s2 PARTITION OF foo_parted FOR VALUES IN (2);
+CREATE TABLE foo_part_d1 (c text, a int, b float8);
+ALTER TABLE foo_parted ATTACH PARTITION foo_part_d1 FOR VALUES IN (3);
+CREATE TABLE foo_part_d2 (b float8, c text, a int);
+ALTER TABLE foo_parted ATTACH PARTITION foo_part_d2 FOR VALUES IN (4);
+
+INSERT INTO foo_parted
+  VALUES (1, 17.1, 'P1'), (2, 17.2, 'P2'), (3, 17.3, 'P3'), (4, 17.4, 'P4')
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+
+UPDATE foo_parted SET a = 2, b = b + 1, c = c || '->P2' WHERE a = 1
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+
+UPDATE foo_parted SET a = 1, b = b + 1, c = c || '->P1' WHERE a = 3
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+
+UPDATE foo_parted SET a = 3, b = b + 1, c = c || '->P3' WHERE a = 1
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+
+UPDATE foo_parted SET a = 4, b = b + 1, c = c || '->P4' WHERE a = 3
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+
+DELETE FROM foo_parted
+  RETURNING old.tableoid::regclass, old.ctid, old.*,
+            new.tableoid::regclass, new.ctid, new.*, *;
+
+DROP TABLE foo_parted;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
new file mode 100644
index 5fd46b7..a25332f
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2354,6 +2354,7 @@ ReorderBufferUpdateProgressTxnCB
 ReorderTuple
 RepOriginId
 ReparameterizeForeignPathByChild_function
+ReplaceReturningVarsFromTargetList_context
 ReplaceVarsFromTargetList_context
 ReplaceVarsNoMatchOption
 ReplicaIdentityStmt
@@ -2383,6 +2384,8 @@ RestrictInfo
 Result
 ResultRelInfo
 ResultState
+ReturningClause
+ReturningOption
 ReturnSetInfo
 ReturnStmt
 RevmapContents
@@ -2527,6 +2530,7 @@ SetOperationStmt
 SetQuantifier
 SetToDefault
 SetupWorkerPtrType
+SetVarReturningType_context
 ShDependObjectInfo
 SharedAggInfo
 SharedBitmapState
@@ -2977,6 +2981,7 @@ VariableSpace
 VariableStatData
 VariableSubstituteHook
 Variables
+VarReturningType
 Vector32
 Vector8
 VersionedQuery
