diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c
index c3ce480c8f..edea27697e 100644
--- a/src/backend/commands/functioncmds.c
+++ b/src/backend/commands/functioncmds.c
@@ -1628,6 +1628,7 @@ CreateCast(CreateCastStmt *stmt)
 		case COERCION_ASSIGNMENT:
 			castcontext = COERCION_CODE_ASSIGNMENT;
 			break;
+			/* COERCION_PLPGSQL is intentionally not covered here */
 		case COERCION_EXPLICIT:
 			castcontext = COERCION_CODE_EXPLICIT;
 			break;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 70f8b718e0..67dab37a54 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3199,6 +3199,29 @@ _copySetOperationStmt(const SetOperationStmt *from)
 	return newnode;
 }
 
+static ExtensionSetStmt *
+_copyExtensionSetStmt(const ExtensionSetStmt *from)
+{
+	ExtensionSetStmt *newnode = makeNode(ExtensionSetStmt);
+
+	COPY_NODE_FIELD(target);
+	COPY_STRING_FIELD(name);
+	COPY_NODE_FIELD(indirection);
+	COPY_NODE_FIELD(val);
+	COPY_NODE_FIELD(fromClause);
+	COPY_NODE_FIELD(whereClause);
+	COPY_NODE_FIELD(groupClause);
+	COPY_NODE_FIELD(havingClause);
+	COPY_NODE_FIELD(windowClause);
+	COPY_NODE_FIELD(sortClause);
+	COPY_NODE_FIELD(limitOffset);
+	COPY_NODE_FIELD(limitCount);
+	COPY_SCALAR_FIELD(limitOption);
+	COPY_NODE_FIELD(lockingClause);
+
+	return newnode;
+}
+
 static AlterTableStmt *
 _copyAlterTableStmt(const AlterTableStmt *from)
 {
@@ -5220,6 +5243,9 @@ copyObjectImpl(const void *from)
 		case T_SetOperationStmt:
 			retval = _copySetOperationStmt(from);
 			break;
+		case T_ExtensionSetStmt:
+			retval = _copyExtensionSetStmt(from);
+			break;
 		case T_AlterTableStmt:
 			retval = _copyAlterTableStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 541e0e6b48..28884da7ec 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1085,6 +1085,27 @@ _equalSetOperationStmt(const SetOperationStmt *a, const SetOperationStmt *b)
 	return true;
 }
 
+static bool
+_equalExtensionSetStmt(const ExtensionSetStmt *a, const ExtensionSetStmt *b)
+{
+	COMPARE_NODE_FIELD(target);
+	COMPARE_STRING_FIELD(name);
+	COMPARE_NODE_FIELD(indirection);
+	COMPARE_NODE_FIELD(val);
+	COMPARE_NODE_FIELD(fromClause);
+	COMPARE_NODE_FIELD(whereClause);
+	COMPARE_NODE_FIELD(groupClause);
+	COMPARE_NODE_FIELD(havingClause);
+	COMPARE_NODE_FIELD(windowClause);
+	COMPARE_NODE_FIELD(sortClause);
+	COMPARE_NODE_FIELD(limitOffset);
+	COMPARE_NODE_FIELD(limitCount);
+	COMPARE_SCALAR_FIELD(limitOption);
+	COMPARE_NODE_FIELD(lockingClause);
+
+	return true;
+}
+
 static bool
 _equalAlterTableStmt(const AlterTableStmt *a, const AlterTableStmt *b)
 {
@@ -3275,6 +3296,9 @@ equal(const void *a, const void *b)
 		case T_SetOperationStmt:
 			retval = _equalSetOperationStmt(a, b);
 			break;
+		case T_ExtensionSetStmt:
+			retval = _equalExtensionSetStmt(a, b);
+			break;
 		case T_AlterTableStmt:
 			retval = _equalAlterTableStmt(a, b);
 			break;
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 963f71e99d..166fbeddb2 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3669,6 +3669,36 @@ raw_expression_tree_walker(Node *node,
 					return true;
 			}
 			break;
+		case T_ExtensionSetStmt:
+			{
+				ExtensionSetStmt *stmt = (ExtensionSetStmt *) node;
+
+				if (walker(stmt->target, context))
+					return true;
+				if (walker(stmt->indirection, context))
+					return true;
+				if (walker(stmt->val, context))
+					return true;
+				if (walker(stmt->fromClause, context))
+					return true;
+				if (walker(stmt->whereClause, context))
+					return true;
+				if (walker(stmt->groupClause, context))
+					return true;
+				if (walker(stmt->havingClause, context))
+					return true;
+				if (walker(stmt->windowClause, context))
+					return true;
+				if (walker(stmt->sortClause, context))
+					return true;
+				if (walker(stmt->limitOffset, context))
+					return true;
+				if (walker(stmt->limitCount, context))
+					return true;
+				if (walker(stmt->lockingClause, context))
+					return true;
+			}
+			break;
 		case T_A_Expr:
 			{
 				A_Expr	   *expr = (A_Expr *) node;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index d78b16ed1d..ef85556fc0 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2775,6 +2775,27 @@ _outSelectStmt(StringInfo str, const SelectStmt *node)
 	WRITE_NODE_FIELD(rarg);
 }
 
+static void
+_outExtensionSetStmt(StringInfo str, const ExtensionSetStmt *node)
+{
+	WRITE_NODE_TYPE("EXTENSIONSET");
+
+	WRITE_NODE_FIELD(target);
+	WRITE_STRING_FIELD(name);
+	WRITE_NODE_FIELD(indirection);
+	WRITE_NODE_FIELD(val);
+	WRITE_NODE_FIELD(fromClause);
+	WRITE_NODE_FIELD(whereClause);
+	WRITE_NODE_FIELD(groupClause);
+	WRITE_NODE_FIELD(havingClause);
+	WRITE_NODE_FIELD(windowClause);
+	WRITE_NODE_FIELD(sortClause);
+	WRITE_NODE_FIELD(limitOffset);
+	WRITE_NODE_FIELD(limitCount);
+	WRITE_ENUM_FIELD(limitOption, LimitOption);
+	WRITE_NODE_FIELD(lockingClause);
+}
+
 static void
 _outFuncCall(StringInfo str, const FuncCall *node)
 {
@@ -4211,6 +4232,9 @@ outNode(StringInfo str, const void *obj)
 			case T_SelectStmt:
 				_outSelectStmt(str, obj);
 				break;
+			case T_ExtensionSetStmt:
+				_outExtensionSetStmt(str, obj);
+				break;
 			case T_ColumnDef:
 				_outColumnDef(str, obj);
 				break;
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 084e00f73d..9ef1ed4d4c 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -42,8 +42,10 @@
 #include "parser/parse_param.h"
 #include "parser/parse_relation.h"
 #include "parser/parse_target.h"
+#include "parser/parse_type.h"
 #include "parser/parsetree.h"
 #include "rewrite/rewriteManip.h"
+#include "utils/builtins.h"
 #include "utils/rel.h"
 
 
@@ -70,6 +72,8 @@ static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt);
 static List *transformReturningList(ParseState *pstate, List *returningList);
 static List *transformUpdateTargetList(ParseState *pstate,
 									   List *targetList);
+static Query *transformExtensionSetStmt(ParseState *pstate,
+										ExtensionSetStmt *stmt);
 static Query *transformDeclareCursorStmt(ParseState *pstate,
 										 DeclareCursorStmt *stmt);
 static Query *transformExplainStmt(ParseState *pstate,
@@ -304,6 +308,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
 			}
 			break;
 
+		case T_ExtensionSetStmt:
+			result = transformExtensionSetStmt(pstate,
+											   (ExtensionSetStmt *) parseTree);
+			break;
+
 			/*
 			 * Special cases
 			 */
@@ -367,6 +376,7 @@ analyze_requires_snapshot(RawStmt *parseTree)
 		case T_DeleteStmt:
 		case T_UpdateStmt:
 		case T_SelectStmt:
+		case T_ExtensionSetStmt:
 			result = true;
 			break;
 
@@ -2393,6 +2403,209 @@ transformReturningList(ParseState *pstate, List *returningList)
 }
 
 
+/*
+ * transformExtensionSetStmt -
+ *	  transform an Extension SET Statement,
+ *	  SET n: name opt_indirection := a_expr ...
+ *
+ * This undocumented syntax allows PL/pgSQL to make use of the regular parser
+ * for assignment statements.  Everything after the colon is taken verbatim
+ * from the PL/pgSQL assignment statement, while the integer "n" says how many
+ * dotted names comprise the target ColumnRef.  (That part is handled by
+ * gram.y, so we just see a ColumnRef and a suitably-truncated indirection
+ * list here.)  If there is no opt_indirection, the transformed statement
+ * looks like "SELECT a_expr ...", except the expression has been cast to
+ * the type of the target.  With indirection, it's still a SELECT, but the
+ * expression will incorporate FieldStore and/or assignment SubscriptingRef
+ * nodes to compute a new value for a container-type variable represented by
+ * the target.  The expression references the target as the container source.
+ */
+static Query *
+transformExtensionSetStmt(ParseState *pstate,
+						  ExtensionSetStmt *stmt)
+{
+	Query	   *qry = makeNode(Query);
+	Node	   *target;
+	Oid			targettype;
+	int32		targettypmod;
+	Oid			targetcollation;
+	TargetEntry *tle;
+	Oid			type_id;
+	Node	   *qual;
+	ListCell   *l;
+
+	/*
+	 * Transform the target reference.  Typically we will get back a Param
+	 * node, but there's no reason to be too picky about its type.
+	 */
+	target = transformExpr(pstate, (Node *) stmt->target,
+						   EXPR_KIND_UPDATE_TARGET);
+	targettype = exprType(target);
+	targettypmod = exprTypmod(target);
+	targetcollation = exprCollation(target);
+
+	/*
+	 * The rest mostly matches transformSelectStmt, except that we needn't
+	 * consider WITH or DISTINCT, and we build a targetlist our own way.
+	 */
+
+	qry->commandType = CMD_SELECT;
+	pstate->p_is_insert = false;
+
+	/* make FOR UPDATE/FOR SHARE info available to addRangeTableEntry */
+	pstate->p_locking_clause = stmt->lockingClause;
+
+	/* make WINDOW info available for window functions, too */
+	pstate->p_windowdefs = stmt->windowClause;
+
+	/* process the FROM clause */
+	transformFromClause(pstate, stmt->fromClause);
+
+	/* Transform the assignment source (cf. transformUpdateTargetList) */
+	tle = transformTargetEntry(pstate,
+							   stmt->val,
+							   NULL,
+							   EXPR_KIND_SELECT_TARGET,
+							   NULL,
+							   false);
+
+	/*
+	 * This next bit is similar to transformAssignedExpr; the key difference
+	 * is we use COERCION_PLPGSQL not COERCION_ASSIGNMENT.
+	 */
+	type_id = exprType((Node *) tle->expr);
+
+	pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET;
+
+	if (stmt->indirection)
+	{
+		tle->expr = (Expr *)
+			transformAssignmentIndirection(pstate,
+										   target,
+										   stmt->name,
+										   false,
+										   targettype,
+										   targettypmod,
+										   targetcollation,
+										   COERCION_PLPGSQL,
+										   stmt->indirection,
+										   list_head(stmt->indirection),
+										   (Node *) tle->expr,
+										   exprLocation(target));
+	}
+	else if (targettype != type_id &&
+			 (targettype == RECORDOID || ISCOMPLEX(targettype)) &&
+			 (type_id == RECORDOID || ISCOMPLEX(type_id)))
+	{
+		/*
+		 * Hack: do not let coerce_to_target_type() deal with inconsistent
+		 * composite types.  Just pass the expression result through as-is,
+		 * and let the PL/pgSQL executor do the conversion its way.  This is
+		 * rather bogus, but it's needed for backwards compatibility.
+		 */
+	}
+	else
+	{
+		/*
+		 * For normal non-qualified target column, do type checking and
+		 * coercion.
+		 */
+		Node	   *orig_expr = (Node *) tle->expr;
+
+		tle->expr = (Expr *)
+			coerce_to_target_type(pstate,
+								  orig_expr, type_id,
+								  targettype, targettypmod,
+								  COERCION_PLPGSQL,
+								  COERCE_IMPLICIT_CAST,
+								  -1);
+		/* With COERCION_PLPGSQL, this error is probably unreachable */
+		if (tle->expr == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("variable \"%s\" is of type %s"
+							" but expression is of type %s",
+							stmt->name,
+							format_type_be(targettype),
+							format_type_be(type_id)),
+					 errhint("You will need to rewrite or cast the expression."),
+					 parser_errposition(pstate, exprLocation(orig_expr))));
+	}
+
+	pstate->p_expr_kind = EXPR_KIND_NONE;
+
+	qry->targetList = list_make1(tle);
+
+	/* transform WHERE */
+	qual = transformWhereClause(pstate, stmt->whereClause,
+								EXPR_KIND_WHERE, "WHERE");
+
+	/* initial processing of HAVING clause is much like WHERE clause */
+	qry->havingQual = transformWhereClause(pstate, stmt->havingClause,
+										   EXPR_KIND_HAVING, "HAVING");
+
+	/*
+	 * Transform sorting/grouping stuff.  Do ORDER BY first because both
+	 * transformGroupClause and transformDistinctClause need the results. Note
+	 * that these functions can also change the targetList, so it's passed to
+	 * them by reference.
+	 */
+	qry->sortClause = transformSortClause(pstate,
+										  stmt->sortClause,
+										  &qry->targetList,
+										  EXPR_KIND_ORDER_BY,
+										  false /* allow SQL92 rules */ );
+
+	qry->groupClause = transformGroupClause(pstate,
+											stmt->groupClause,
+											&qry->groupingSets,
+											&qry->targetList,
+											qry->sortClause,
+											EXPR_KIND_GROUP_BY,
+											false /* allow SQL92 rules */ );
+
+	/* No DISTINCT clause */
+	qry->distinctClause = NIL;
+	qry->hasDistinctOn = false;
+
+	/* transform LIMIT */
+	qry->limitOffset = transformLimitClause(pstate, stmt->limitOffset,
+											EXPR_KIND_OFFSET, "OFFSET",
+											stmt->limitOption);
+	qry->limitCount = transformLimitClause(pstate, stmt->limitCount,
+										   EXPR_KIND_LIMIT, "LIMIT",
+										   stmt->limitOption);
+	qry->limitOption = stmt->limitOption;
+
+	/* transform window clauses after we have seen all window functions */
+	qry->windowClause = transformWindowDefinitions(pstate,
+												   pstate->p_windowdefs,
+												   &qry->targetList);
+
+	qry->rtable = pstate->p_rtable;
+	qry->jointree = makeFromExpr(pstate->p_joinlist, qual);
+
+	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
+	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
+	qry->hasAggs = pstate->p_hasAggs;
+
+	foreach(l, stmt->lockingClause)
+	{
+		transformLockingClause(pstate, qry,
+							   (LockingClause *) lfirst(l), false);
+	}
+
+	assign_query_collations(pstate, qry);
+
+	/* this must be done after collations, for reliable comparison of exprs */
+	if (pstate->p_hasAggs || qry->groupClause || qry->groupingSets || qry->havingQual)
+		parseCheckAggregates(pstate, qry);
+
+	return qry;
+}
+
+
 /*
  * transformDeclareCursorStmt -
  *	transform a DECLARE CURSOR Statement
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 8f341ac006..5279d23b7b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -275,7 +275,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		DropCastStmt DropRoleStmt
 		DropdbStmt DropTableSpaceStmt
 		DropTransformStmt
-		DropUserMappingStmt ExplainStmt FetchStmt
+		DropUserMappingStmt ExplainStmt ExtensionSetStmt FetchStmt
 		GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt
 		ListenStmt LoadStmt LockStmt NotifyStmt ExplainableStmt PreparableStmt
 		CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt
@@ -535,7 +535,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <str>		ColId ColLabel BareColLabel
 %type <str>		NonReservedWord NonReservedWord_or_Sconst
 %type <str>		var_name type_function_name param_name
-%type <str>		createdb_opt_name
+%type <str>		createdb_opt_name extension_set_target
 %type <node>	var_value zone_value
 %type <rolespec> auth_ident RoleSpec opt_granted_by
 
@@ -916,6 +916,7 @@ stmt :
 			| DropdbStmt
 			| ExecuteStmt
 			| ExplainStmt
+			| ExtensionSetStmt
 			| FetchStmt
 			| GrantStmt
 			| GrantRoleStmt
@@ -11041,6 +11042,73 @@ set_target_list:
 		;
 
 
+/*****************************************************************************
+ *
+ *		QUERY:
+ *				SET n: name opt_indirection := a_expr ...
+ *
+ * This undocumented syntax allows PL/pgSQL to make use of the regular parser
+ * for its assignment statements.  The stuff after the a_expr should match
+ * what can appear after the targetlist in SELECT.
+ *****************************************************************************/
+
+ExtensionSetStmt: SET Iconst ':' extension_set_target opt_indirection
+			extension_set_assignment a_expr
+			from_clause where_clause
+			group_clause having_clause window_clause
+			opt_sort_clause opt_select_limit opt_for_locking_clause
+				{
+					ExtensionSetStmt *n = makeNode(ExtensionSetStmt);
+					ColumnRef  *cref = makeNode(ColumnRef);
+					int			nnames = $2;
+					List	   *indirection = $5;
+
+					cref->fields = list_make1(makeString($4));
+					cref->location = @4;
+					while (--nnames > 0 && indirection != NIL)
+					{
+						Node   *ind = (Node *) linitial(indirection);
+
+						if (!IsA(ind, String))
+							elog(ERROR, "invalid name count in extension SET");
+						cref->fields = lappend(cref->fields, ind);
+						indirection = list_delete_first(indirection);
+					}
+					n->target = cref;
+					n->name = $4;
+					n->indirection = check_indirection(indirection, yyscanner);
+					n->val = $7;
+					n->fromClause = $8;
+					n->whereClause = $9;
+					n->groupClause = $10;
+					n->havingClause = $11;
+					n->windowClause = $12;
+					n->sortClause = $13;
+					if ($14)
+					{
+						n->limitOffset = $14->limitOffset;
+						n->limitCount = $14->limitCount;
+						if (!n->sortClause &&
+							$14->limitOption == LIMIT_OPTION_WITH_TIES)
+							ereport(ERROR,
+									(errcode(ERRCODE_SYNTAX_ERROR),
+									 errmsg("WITH TIES cannot be specified without ORDER BY clause")));
+						n->limitOption = $14->limitOption;
+					}
+					n->lockingClause = $15;
+					$$ = (Node *) n;
+				}
+		;
+
+extension_set_target: ColId						{ $$ = $1; }
+			| PARAM								{ $$ = psprintf("$%d", $1); }
+		;
+
+extension_set_assignment: COLON_EQUALS
+			| '='
+		;
+
+
 /*****************************************************************************
  *
  *		QUERY:
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index da6c3ae4b5..462e2af74e 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -2815,6 +2815,14 @@ find_coercion_pathway(Oid targetTypeId, Oid sourceTypeId,
 		}
 	}
 
+	/*
+	 * When parsing PL/pgSQL assignments, allow an I/O cast to be used
+	 * whenever no normal coercion is available.
+	 */
+	if (result == COERCION_PATH_NONE &&
+		ccontext == COERCION_PLPGSQL)
+		result = COERCION_PATH_COERCEVIAIO;
+
 	return result;
 }
 
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 3dda8e2847..55a9a808da 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -34,23 +34,13 @@
 
 static void markTargetListOrigin(ParseState *pstate, TargetEntry *tle,
 								 Var *var, int levelsup);
-static Node *transformAssignmentIndirection(ParseState *pstate,
-											Node *basenode,
-											const char *targetName,
-											bool targetIsSubscripting,
-											Oid targetTypeId,
-											int32 targetTypMod,
-											Oid targetCollation,
-											List *indirection,
-											ListCell *indirection_cell,
-											Node *rhs,
-											int location);
 static Node *transformAssignmentSubscripts(ParseState *pstate,
 										   Node *basenode,
 										   const char *targetName,
 										   Oid targetTypeId,
 										   int32 targetTypMod,
 										   Oid targetCollation,
+										   CoercionContext ccontext,
 										   List *subscripts,
 										   bool isSlice,
 										   List *indirection,
@@ -558,6 +548,7 @@ transformAssignedExpr(ParseState *pstate,
 										   attrtype,
 										   attrtypmod,
 										   attrcollation,
+										   COERCION_ASSIGNMENT,
 										   indirection,
 										   list_head(indirection),
 										   (Node *) expr,
@@ -648,9 +639,9 @@ updateTargetListEntry(ParseState *pstate,
  * operation.
  *
  * In the initial call, basenode is a Var for the target column in UPDATE,
- * or a null Const of the target's type in INSERT.  In recursive calls,
- * basenode is NULL, indicating that a substitute node should be consed up if
- * needed.
+ * or a null Const of the target's type in INSERT, or a Param for the target
+ * variable in extension SET operations.  In recursive calls, basenode is
+ * NULL, indicating that a substitute node should be consed up if needed.
  *
  * targetName is the name of the field or subfield we're assigning to, and
  * targetIsSubscripting is true if we're subscripting it.  These are just for
@@ -660,6 +651,10 @@ updateTargetListEntry(ParseState *pstate,
  * collation of the object to be assigned to (initially the target column,
  * later some subobject).
  *
+ * ccontext is the coercion level to use while coercing the "rhs".  For
+ * normal statements it'll be COERCION_ASSIGNMENT, but extension SET uses
+ * a special value.
+ *
  * indirection is the list of indirection nodes, and indirection_cell is the
  * start of the sublist remaining to process.  When it's NULL, we're done
  * recursing and can just coerce and return the RHS.
@@ -672,7 +667,7 @@ updateTargetListEntry(ParseState *pstate,
  * might want to decorate indirection cells with their own location info,
  * in which case the location argument could probably be dropped.)
  */
-static Node *
+Node *
 transformAssignmentIndirection(ParseState *pstate,
 							   Node *basenode,
 							   const char *targetName,
@@ -680,6 +675,7 @@ transformAssignmentIndirection(ParseState *pstate,
 							   Oid targetTypeId,
 							   int32 targetTypMod,
 							   Oid targetCollation,
+							   CoercionContext ccontext,
 							   List *indirection,
 							   ListCell *indirection_cell,
 							   Node *rhs,
@@ -752,6 +748,7 @@ transformAssignmentIndirection(ParseState *pstate,
 													 targetTypeId,
 													 targetTypMod,
 													 targetCollation,
+													 ccontext,
 													 subscripts,
 													 isSlice,
 													 indirection,
@@ -804,6 +801,7 @@ transformAssignmentIndirection(ParseState *pstate,
 												 fieldTypeId,
 												 fieldTypMod,
 												 fieldCollation,
+												 ccontext,
 												 indirection,
 												 lnext(indirection, i),
 												 rhs,
@@ -840,6 +838,7 @@ transformAssignmentIndirection(ParseState *pstate,
 											 targetTypeId,
 											 targetTypMod,
 											 targetCollation,
+											 ccontext,
 											 subscripts,
 											 isSlice,
 											 indirection,
@@ -853,7 +852,7 @@ transformAssignmentIndirection(ParseState *pstate,
 	result = coerce_to_target_type(pstate,
 								   rhs, exprType(rhs),
 								   targetTypeId, targetTypMod,
-								   COERCION_ASSIGNMENT,
+								   ccontext,
 								   COERCE_IMPLICIT_CAST,
 								   -1);
 	if (result == NULL)
@@ -893,6 +892,7 @@ transformAssignmentSubscripts(ParseState *pstate,
 							  Oid targetTypeId,
 							  int32 targetTypMod,
 							  Oid targetCollation,
+							  CoercionContext ccontext,
 							  List *subscripts,
 							  bool isSlice,
 							  List *indirection,
@@ -946,6 +946,7 @@ transformAssignmentSubscripts(ParseState *pstate,
 										 typeNeeded,
 										 typmodNeeded,
 										 collationNeeded,
+										 ccontext,
 										 indirection,
 										 next_indirection,
 										 rhs,
@@ -969,7 +970,7 @@ transformAssignmentSubscripts(ParseState *pstate,
 		result = coerce_to_target_type(pstate,
 									   result, resulttype,
 									   targetTypeId, targetTypMod,
-									   COERCION_ASSIGNMENT,
+									   ccontext,
 									   COERCE_IMPLICIT_CAST,
 									   -1);
 		/* can fail if we had int2vector/oidvector, but not for true domains */
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index a42ead7d69..a2e3b332c7 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2313,6 +2313,10 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_SELECT;
 			break;
 
+		case T_ExtensionSetStmt:
+			tag = CMDTAG_SELECT;
+			break;
+
 			/* utility statements --- same whether raw or cooked */
 		case T_TransactionStmt:
 			{
@@ -3181,6 +3185,10 @@ GetCommandLogLevel(Node *parsetree)
 				lev = LOGSTMT_ALL;
 			break;
 
+		case T_ExtensionSetStmt:
+			lev = LOGSTMT_ALL;
+			break;
+
 			/* utility statements --- same whether raw or cooked */
 		case T_TransactionStmt:
 			lev = LOGSTMT_ALL;
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 3684f87a88..273b8181e4 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -314,6 +314,7 @@ typedef enum NodeTag
 	T_DeleteStmt,
 	T_UpdateStmt,
 	T_SelectStmt,
+	T_ExtensionSetStmt,
 	T_AlterTableStmt,
 	T_AlterTableCmd,
 	T_AlterDomainStmt,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 48a79a7657..4d073f862d 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1675,6 +1675,41 @@ typedef struct SetOperationStmt
 } SetOperationStmt;
 
 
+/* ----------------------
+ *		Extension SET Statement
+ *
+ * Like SelectStmt, this is transformed into a SELECT Query.
+ * However, the targetlist of the result looks more like an UPDATE.
+ * ----------------------
+ */
+typedef struct ExtensionSetStmt
+{
+	NodeTag		type;
+
+	/* These fields represent "target opt_indirection := value" */
+	ColumnRef  *target;			/* ColumnRef for the target variable */
+	char	   *name;			/* initial column name */
+	List	   *indirection;	/* subscripts and field names, or NIL */
+	Node	   *val;			/* the value expression to assign */
+
+	/*
+	 * Historically, PL/pgSQL has allowed an assignment to have any decoration
+	 * that could occur after the targetlist of SELECT, so we preserve that
+	 * capability.
+	 */
+	List	   *fromClause;		/* the FROM clause */
+	Node	   *whereClause;	/* WHERE qualification */
+	List	   *groupClause;	/* GROUP BY clauses */
+	Node	   *havingClause;	/* HAVING conditional-expression */
+	List	   *windowClause;	/* WINDOW window_name AS (...), ... */
+	List	   *sortClause;		/* sort clause (a list of SortBy's) */
+	Node	   *limitOffset;	/* # of result tuples to skip */
+	Node	   *limitCount;		/* # of result tuples to return */
+	LimitOption limitOption;	/* limit type */
+	List	   *lockingClause;	/* FOR UPDATE (list of LockingClause's) */
+} ExtensionSetStmt;
+
+
 /*****************************************************************************
  *		Other Statements (no optimizations required)
  *
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index dd85908fe2..71a58804dc 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -457,6 +457,7 @@ typedef enum CoercionContext
 {
 	COERCION_IMPLICIT,			/* coercion in context of expression */
 	COERCION_ASSIGNMENT,		/* coercion in context of assignment */
+	COERCION_PLPGSQL,			/* if no assignment cast, use CoerceViaIO */
 	COERCION_EXPLICIT			/* explicit cast operation */
 } CoercionContext;
 
diff --git a/src/include/parser/parse_target.h b/src/include/parser/parse_target.h
index 7039df29cb..6f5741f824 100644
--- a/src/include/parser/parse_target.h
+++ b/src/include/parser/parse_target.h
@@ -36,6 +36,18 @@ extern void updateTargetListEntry(ParseState *pstate, TargetEntry *tle,
 								  char *colname, int attrno,
 								  List *indirection,
 								  int location);
+extern Node *transformAssignmentIndirection(ParseState *pstate,
+											Node *basenode,
+											const char *targetName,
+											bool targetIsSubscripting,
+											Oid targetTypeId,
+											int32 targetTypMod,
+											Oid targetCollation,
+											CoercionContext ccontext,
+											List *indirection,
+											ListCell *indirection_cell,
+											Node *rhs,
+											int location);
 extern List *checkInsertTargets(ParseState *pstate, List *cols,
 								List **attrnos);
 extern TupleDesc expandRecordVariable(ParseState *pstate, Var *var,
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index ccbc50fc45..22fe037f90 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -8005,10 +8005,14 @@ get_cast_hashentry(PLpgSQL_execstate *estate,
 		placeholder->collation = get_typcollation(srctype);
 
 		/*
-		 * Apply coercion.  We use ASSIGNMENT coercion because that's the
-		 * closest match to plpgsql's historical behavior; in particular,
-		 * EXPLICIT coercion would allow silent truncation to a destination
-		 * varchar/bpchar's length, which we do not want.
+		 * Apply coercion.  We use the special coercion context
+		 * COERCION_PLPGSQL to match plpgsql's historical behavior, namely
+		 * that any cast not available at ASSIGNMENT level will be implemented
+		 * as an I/O coercion.  (It's somewhat dubious that we prefer I/O
+		 * coercion over cast pathways that exist at EXPLICIT level.  Changing
+		 * that would cause assorted minor behavioral differences though, and
+		 * a user who wants the explicit-cast behavior can always write an
+		 * explicit cast.)
 		 *
 		 * If source type is UNKNOWN, coerce_to_target_type will fail (it only
 		 * expects to see that for Const input nodes), so don't call it; we'll
@@ -8021,7 +8025,7 @@ get_cast_hashentry(PLpgSQL_execstate *estate,
 			cast_expr = coerce_to_target_type(NULL,
 											  (Node *) placeholder, srctype,
 											  dsttype, dsttypmod,
-											  COERCION_ASSIGNMENT,
+											  COERCION_PLPGSQL,
 											  COERCE_IMPLICIT_CAST,
 											  -1);
 
@@ -8029,7 +8033,8 @@ get_cast_hashentry(PLpgSQL_execstate *estate,
 		 * If there's no cast path according to the parser, fall back to using
 		 * an I/O coercion; this is semantically dubious but matches plpgsql's
 		 * historical behavior.  We would need something of the sort for
-		 * UNKNOWN literals in any case.
+		 * UNKNOWN literals in any case.  (This is probably now only reachable
+		 * in the case where srctype is UNKNOWN/RECORD.)
 		 */
 		if (cast_expr == NULL)
 		{
@@ -8338,7 +8343,8 @@ exec_check_rw_parameter(PLpgSQL_expr *expr, int target_dno)
 		return;
 
 	/*
-	 * Top level of expression must be a simple FuncExpr or OpExpr.
+	 * Top level of expression must be a simple FuncExpr, OpExpr, or
+	 * SubscriptingRef.
 	 */
 	if (IsA(expr->expr_simple_expr, FuncExpr))
 	{
@@ -8354,6 +8360,33 @@ exec_check_rw_parameter(PLpgSQL_expr *expr, int target_dno)
 		funcid = opexpr->opfuncid;
 		fargs = opexpr->args;
 	}
+	else if (IsA(expr->expr_simple_expr, SubscriptingRef))
+	{
+		SubscriptingRef *sbsref = (SubscriptingRef *) expr->expr_simple_expr;
+
+		/* We only trust standard varlena arrays to be safe */
+		if (get_typsubscript(sbsref->refcontainertype, NULL) !=
+			F_ARRAY_SUBSCRIPT_HANDLER)
+			return;
+
+		/* refexpr can be a simple Param, otherwise must not contain target */
+		if (!(sbsref->refexpr && IsA(sbsref->refexpr, Param)) &&
+			contains_target_param((Node *) sbsref->refexpr, &target_dno))
+			return;
+
+		/* the other subexpressions must not contain target */
+		if (contains_target_param((Node *) sbsref->refupperindexpr,
+								  &target_dno) ||
+			contains_target_param((Node *) sbsref->reflowerindexpr,
+								  &target_dno) ||
+			contains_target_param((Node *) sbsref->refassgnexpr,
+								  &target_dno))
+			return;
+
+		/* OK, we can pass target as a read-write parameter */
+		expr->rwparam = target_dno;
+		return;
+	}
 	else
 		return;
 
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index 8227bf0449..561b0dec59 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -949,16 +949,21 @@ stmt_call		: K_CALL
 					}
 				;
 
-stmt_assign		: assign_var assign_operator expr_until_semi
+stmt_assign		: T_DATUM
 					{
 						PLpgSQL_stmt_assign *new;
+						int nnames = $1.ident ? 1 : list_length($1.idents);
 
+						check_assignable($1.datum, @1);
 						new = palloc0(sizeof(PLpgSQL_stmt_assign));
 						new->cmd_type = PLPGSQL_STMT_ASSIGN;
 						new->lineno   = plpgsql_location_to_lineno(@1);
 						new->stmtid = ++plpgsql_curr_compile->nstatements;
-						new->varno = $1->dno;
-						new->expr  = $3;
+						new->varno = $1.datum->dno;
+						/* Push back the head name to include it in the stmt */
+						plpgsql_push_back_token(T_DATUM);
+						new->expr = read_sql_stmt(psprintf("SET %d: ",
+														   nnames));
 
 						$$ = (PLpgSQL_stmt *)new;
 					}
