From 03337cf5da6f02d6d7cfc4d70ee381e3e6af9394 Mon Sep 17 00:00:00 2001
From: Midhush Karthic <mimosk25@gmail.com>
Date: Sun, 27 Sep 2026 17:36:36 -0400
Subject: [PATCH v1] Add target column context to errors

---
 src/backend/catalog/heap.c              |  11 +-
 src/backend/catalog/pg_attrdef.c        |   4 +
 src/backend/commands/tablecmds.c        |  10 ++
 src/backend/commands/typecmds.c         |   4 +-
 src/backend/executor/execExpr.c         |   2 +
 src/backend/executor/execExprInterp.c   |  60 +++++++
 src/backend/jit/llvm/llvmjit_expr.c     |   6 +
 src/backend/jit/llvm/llvmjit_types.c    |   1 +
 src/backend/optimizer/util/clauses.c    |  27 ++-
 src/backend/parser/parse_coerce.c       |  72 ++++++++
 src/backend/parser/parse_target.c       |   6 +-
 src/backend/rewrite/rewriteHandler.c    |   4 +
 src/include/catalog/catversion.h        |   2 +-
 src/include/catalog/heap.h              |   3 +-
 src/include/executor/execExpr.h         |   3 +
 src/include/nodes/primnodes.h           |   5 +
 src/include/parser/parse_coerce.h       |   2 +
 src/test/regress/expected/arrays.out    |   1 +
 src/test/regress/expected/bit.out       |   3 +
 src/test/regress/expected/char.out      |   2 +
 src/test/regress/expected/char_1.out    |   2 +
 src/test/regress/expected/char_2.out    |   2 +
 src/test/regress/expected/copy.out      |   2 +
 src/test/regress/expected/domain.out    |   5 +
 src/test/regress/expected/numeric.out   |  10 ++
 src/test/regress/expected/varchar.out   | 221 ++++++++++++++++++++++++
 src/test/regress/expected/varchar_1.out | 221 ++++++++++++++++++++++++
 src/test/regress/expected/varchar_2.out | 221 ++++++++++++++++++++++++
 src/test/regress/sql/varchar.sql        | 190 ++++++++++++++++++++
 29 files changed, 1091 insertions(+), 11 deletions(-)

diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 1c188b7a0ff..a83691a5c6e 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2459,7 +2459,8 @@ AddRelationNewConstraints(Relation rel,
 		expr = cookDefault(pstate, colDef->raw_default,
 						   atp->atttypid, atp->atttypmod,
 						   NameStr(atp->attname),
-						   atp->attgenerated);
+						   atp->attgenerated,
+						   RelationGetRelid(rel), colDef->attnum);
 
 		/*
 		 * If the expression is just a NULL constant, we do not bother to make
@@ -3355,6 +3356,7 @@ check_virtual_generated_security(ParseState *pstate, Node *node)
  * If atttypid is not InvalidOid, coerce the expression to the specified
  * type (and typmod atttypmod).   attname is only needed in this case:
  * it is used in the error message, if any.
+ * relid and attnum identify the destination column for coercion errors.
  */
 Node *
 cookDefault(ParseState *pstate,
@@ -3362,7 +3364,8 @@ cookDefault(ParseState *pstate,
 			Oid atttypid,
 			int32 atttypmod,
 			const char *attname,
-			char attgenerated)
+			char attgenerated,
+			Oid relid, AttrNumber attnum)
 {
 	Node	   *expr;
 
@@ -3405,6 +3408,7 @@ cookDefault(ParseState *pstate,
 	if (OidIsValid(atttypid))
 	{
 		Oid			type_id = exprType(expr);
+		Node	   *orig_expr = expr;
 
 		expr = coerce_to_target_type(pstate, expr, type_id,
 									 atttypid, atttypmod,
@@ -3420,6 +3424,9 @@ cookDefault(ParseState *pstate,
 							format_type_be(atttypid),
 							format_type_be(type_id)),
 					 errhint("You will need to rewrite or cast the expression.")));
+
+		if (OidIsValid(relid))
+			set_coercion_target(expr, orig_expr, relid, attnum);
 	}
 
 	/*
diff --git a/src/backend/catalog/pg_attrdef.c b/src/backend/catalog/pg_attrdef.c
index 60bd7a40759..5abb2f48e87 100644
--- a/src/backend/catalog/pg_attrdef.c
+++ b/src/backend/catalog/pg_attrdef.c
@@ -22,6 +22,7 @@
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_attrdef.h"
+#include "parser/parse_coerce.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
@@ -58,6 +59,9 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
 
 	adrel = table_open(AttrDefaultRelationId, RowExclusiveLock);
 
+	/* An inherited or copied default now belongs to this column. */
+	set_coercion_target(expr, NULL, RelationGetRelid(rel), attnum);
+
 	/*
 	 * Flatten expression to string form for storage.
 	 */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2f073ddb84a..9322cfec413 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -14947,6 +14947,8 @@ ATPrepAlterColumnType(List **wqueue,
 	else if (tab->relkind == RELKIND_RELATION ||
 			 tab->relkind == RELKIND_PARTITIONED_TABLE)
 	{
+		Node	   *orig_transform;
+
 		/*
 		 * Set up an expression to transform the old data value to the new
 		 * type. If a USING option was given, use the expression as
@@ -14964,6 +14966,7 @@ ATPrepAlterColumnType(List **wqueue,
 										 0);
 		}
 
+		orig_transform = transform;
 		transform = coerce_to_target_type(pstate,
 										  transform, exprType(transform),
 										  targettype, targettypmod,
@@ -14993,6 +14996,8 @@ ATPrepAlterColumnType(List **wqueue,
 														  targettypmod)) : 0));
 		}
 
+		set_coercion_target(transform, orig_transform, RelationGetRelid(rel), attnum);
+
 		/* Fix collations after all else */
 		assign_expr_collations(pstate, transform);
 
@@ -15279,9 +15284,12 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 	 */
 	if (attTup->atthasdef)
 	{
+		Node	   *orig_default;
+
 		defaultexpr = build_column_default(rel, attnum);
 		Assert(defaultexpr);
 		defaultexpr = strip_implicit_coercions(defaultexpr);
+		orig_default = defaultexpr;
 		defaultexpr = coerce_to_target_type(NULL,	/* no UNKNOWN params */
 											defaultexpr, exprType(defaultexpr),
 											targettype, targettypmod,
@@ -15301,6 +15309,8 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 						 errmsg("default for column \"%s\" cannot be cast automatically to type %s",
 								colName, format_type_be(targettype))));
 		}
+		set_coercion_target(defaultexpr, orig_default,
+							RelationGetRelid(rel), attnum);
 	}
 	else
 		defaultexpr = NULL;
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 99a0ae2228e..5ff7c16aa14 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -901,7 +901,7 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 											  basetypeoid,
 											  basetypeMod,
 											  domainName,
-											  0);
+											  0, InvalidOid, InvalidAttrNumber);
 
 					/*
 					 * If the expression is just a NULL constant, we treat it
@@ -2701,7 +2701,7 @@ AlterDomainDefault(List *names, Node *defaultRaw)
 								  typTup->typbasetype,
 								  typTup->typtypmod,
 								  NameStr(typTup->typname),
-								  0);
+								  0, InvalidOid, InvalidAttrNumber);
 
 		/*
 		 * If the expression is just a NULL constant, we treat the command
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 82e846a1f4f..e095d048b55 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1196,6 +1196,8 @@ ExecInitExprRec(Expr *node, ExprState *state,
 				ExecInitFunc(&scratch, node,
 							 func->args, func->funcid, func->inputcollid,
 							 state);
+				if (OidIsValid(func->functargetrelid))
+					scratch.opcode = EEOP_FUNCEXPR_COERCION;
 				ExprEvalPushStep(state, &scratch);
 				break;
 			}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 397219f7a3a..082f349d606 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -514,6 +514,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 		&&CASE_EEOP_FUNCEXPR_STRICT_2,
 		&&CASE_EEOP_FUNCEXPR_FUSAGE,
 		&&CASE_EEOP_FUNCEXPR_STRICT_FUSAGE,
+		&&CASE_EEOP_FUNCEXPR_COERCION,
 		&&CASE_EEOP_BOOL_AND_STEP_FIRST,
 		&&CASE_EEOP_BOOL_AND_STEP,
 		&&CASE_EEOP_BOOL_AND_STEP_LAST,
@@ -1032,6 +1033,13 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();
 		}
 
+		EEO_CASE(EEOP_FUNCEXPR_COERCION)
+		{
+			ExecEvalFuncExprCoercion(state, op, econtext);
+
+			EEO_NEXT();
+		}
+
 		/*
 		 * If any of its clauses is FALSE, an AND's result is FALSE regardless
 		 * of the states of the rest of the clauses, so we can stop evaluating
@@ -3048,6 +3056,58 @@ ExecEvalFuncExprStrictFusage(ExprState *state, ExprEvalStep *op,
 	pgstat_end_function_usage(&fcusage, true);
 }
 
+/*
+ * Add destination context for an assignment coercion.  Resolve names only
+ * when reporting an error, so stored expressions continue to reflect renames.
+ * Leave the datatype's primary message and object diagnostics untouched.
+ */
+static void
+coercion_error_callback(void *arg)
+{
+	FuncExpr   *expr = (FuncExpr *) arg;
+	char	   *colname;
+	char	   *relname;
+
+	Assert(IsA(expr, FuncExpr));
+	Assert(OidIsValid(expr->functargetrelid));
+
+	colname = get_attname(expr->functargetrelid, expr->functargetattnum, true);
+	if (colname == NULL)
+		return;
+	relname = get_rel_name(expr->functargetrelid);
+	if (relname == NULL)
+		return;
+
+	errcontext("column \"%s\" of relation \"%s\"", colname, relname);
+}
+
+/*
+ * Evaluate EEOP_FUNCEXPR_COERCION with destination-column error context.
+ * Arguments have already been evaluated, so errors in the source expression
+ * are not attributed to the destination.
+ */
+void
+ExecEvalFuncExprCoercion(ExprState *state, ExprEvalStep *op,
+						 ExprContext *econtext)
+{
+	FunctionCallInfo fcinfo = op->d.func.fcinfo_data;
+	ErrorContextCallback errcallback;
+
+	errcallback.callback = coercion_error_callback;
+	errcallback.arg = fcinfo->flinfo->fn_expr;
+	errcallback.previous = error_context_stack;
+	error_context_stack = &errcallback;
+
+	/* These helpers also handle disabled function statistics collection. */
+	if (fcinfo->flinfo->fn_strict)
+		ExecEvalFuncExprStrictFusage(state, op, econtext);
+	else
+		ExecEvalFuncExprFusage(state, op, econtext);
+
+	/* On ERROR, the surrounding error handler restores the callback stack. */
+	error_context_stack = errcallback.previous;
+}
+
 /*
  * Evaluate a PARAM_EXEC parameter.
  *
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index fc80d3e55fe..1dac4f9b9b9 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -761,6 +761,12 @@ llvm_compile_expr(ExprState *state)
 				LLVMBuildBr(b, opblocks[opno + 1]);
 				break;
 
+			case EEOP_FUNCEXPR_COERCION:
+				build_EvalXFunc(b, mod, "ExecEvalFuncExprCoercion",
+								v_state, op, v_econtext);
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
+
 				/*
 				 * Treat them the same for now, optimizer can remove
 				 * redundancy. Could be worthwhile to optimize during emission
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
index c8a1f841293..586daf03959 100644
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -151,6 +151,7 @@ void	   *referenced_functions[] =
 	ExecEvalFieldSelect,
 	ExecEvalFieldStoreDeForm,
 	ExecEvalFieldStoreForm,
+	ExecEvalFuncExprCoercion,
 	ExecEvalFuncExprFusage,
 	ExecEvalFuncExprStrictFusage,
 	ExecEvalGroupingFunc,
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 3e1f210652d..41af999482e 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -153,6 +153,7 @@ static Expr *simplify_function(Oid funcid,
 							   Oid result_type, int32 result_typmod,
 							   Oid result_collid, Oid input_collid, List **args_p,
 							   bool funcvariadic, bool process_args, bool allow_non_const,
+							   FuncExpr *original,
 							   eval_const_expressions_context *context);
 static Node *simplify_aggref(Aggref *aggref,
 							 eval_const_expressions_context *context);
@@ -168,6 +169,7 @@ static Expr *evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
 							   Oid result_collid, Oid input_collid, List *args,
 							   bool funcvariadic,
 							   HeapTuple func_tuple,
+							   FuncExpr *original,
 							   eval_const_expressions_context *context);
 static Expr *inline_function(Oid funcid, Oid result_type, Oid result_collid,
 							 Oid input_collid, List *args,
@@ -2985,6 +2987,7 @@ eval_const_expressions_mutator(Node *node,
 										   expr->funcvariadic,
 										   true,
 										   true,
+										   expr,
 										   context);
 				if (simple)		/* successfully simplified it */
 					return (Node *) simple;
@@ -3004,6 +3007,8 @@ eval_const_expressions_mutator(Node *node,
 				newexpr->funccollid = expr->funccollid;
 				newexpr->inputcollid = expr->inputcollid;
 				newexpr->args = args;
+				newexpr->functargetrelid = expr->functargetrelid;
+				newexpr->functargetattnum = expr->functargetattnum;
 				newexpr->location = expr->location;
 				return (Node *) newexpr;
 			}
@@ -3037,6 +3042,7 @@ eval_const_expressions_mutator(Node *node,
 										   false,
 										   true,
 										   true,
+										   NULL,
 										   context);
 				if (simple)		/* successfully simplified it */
 					return (Node *) simple;
@@ -3156,6 +3162,7 @@ eval_const_expressions_mutator(Node *node,
 											   false,
 											   false,
 											   false,
+											   NULL,
 											   context);
 					if (simple) /* successfully simplified it */
 					{
@@ -3515,6 +3522,7 @@ eval_const_expressions_mutator(Node *node,
 										   false,
 										   true,
 										   true,
+										   NULL,
 										   context);
 				if (simple)		/* successfully simplified output fn */
 				{
@@ -3547,6 +3555,7 @@ eval_const_expressions_mutator(Node *node,
 											   false,
 											   false,
 											   true,
+											   NULL,
 											   context);
 					if (simple) /* successfully simplified input fn */
 						return (Node *) simple;
@@ -4668,7 +4677,8 @@ simplify_boolean_equality(Oid opno, List *args)
  * polymorphic functions), result typmod, result collation, the input
  * collation to use for the function, the original argument list (not
  * const-simplified yet, unless process_args is false), and some flags;
- * also the context data for eval_const_expressions.
+ * also the original FuncExpr (if any), for preserving the coercion target,
+ * and the context data for eval_const_expressions.
  *
  * Returns a simplified expression if successful, or NULL if cannot
  * simplify the function call.
@@ -4685,6 +4695,7 @@ static Expr *
 simplify_function(Oid funcid, Oid result_type, int32 result_typmod,
 				  Oid result_collid, Oid input_collid, List **args_p,
 				  bool funcvariadic, bool process_args, bool allow_non_const,
+				  FuncExpr *original,
 				  eval_const_expressions_context *context)
 {
 	List	   *args = *args_p;
@@ -4730,7 +4741,7 @@ simplify_function(Oid funcid, Oid result_type, int32 result_typmod,
 	newexpr = evaluate_function(funcid, result_type, result_typmod,
 								result_collid, input_collid,
 								args, funcvariadic,
-								func_tuple, context);
+								func_tuple, original, context);
 
 	if (!newexpr && allow_non_const && OidIsValid(func_form->prosupport))
 	{
@@ -4753,6 +4764,8 @@ simplify_function(Oid funcid, Oid result_type, int32 result_typmod,
 		fexpr.funccollid = result_collid;
 		fexpr.inputcollid = input_collid;
 		fexpr.args = args;
+		fexpr.functargetrelid = original ? original->functargetrelid : InvalidOid;
+		fexpr.functargetattnum = original ? original->functargetattnum : InvalidAttrNumber;
 		fexpr.location = -1;
 
 		req.type = T_SupportRequestSimplify;
@@ -4767,7 +4780,9 @@ simplify_function(Oid funcid, Oid result_type, int32 result_typmod,
 		Assert(newexpr != (Expr *) &fexpr);
 	}
 
-	if (!newexpr && allow_non_const)
+	/* Inlining would discard the assignment coercion's error context. */
+	if (!newexpr && allow_non_const &&
+		!(original && OidIsValid(original->functargetrelid)))
 		newexpr = inline_function(funcid, result_type, result_collid,
 								  input_collid, args, funcvariadic,
 								  func_tuple, context);
@@ -5370,6 +5385,7 @@ evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
 				  Oid result_collid, Oid input_collid, List *args,
 				  bool funcvariadic,
 				  HeapTuple func_tuple,
+				  FuncExpr *original,
 				  eval_const_expressions_context *context)
 {
 	Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple);
@@ -5454,6 +5470,11 @@ evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
 	newexpr->funccollid = result_collid;	/* doesn't matter */
 	newexpr->inputcollid = input_collid;
 	newexpr->args = args;
+	if (original)
+	{
+		newexpr->functargetrelid = original->functargetrelid;
+		newexpr->functargetattnum = original->functargetattnum;
+	}
 	newexpr->location = -1;
 
 	return evaluate_expr((Expr *) newexpr, result_type, result_typmod,
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index d3240f4b265..cc7c7745d7a 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -131,6 +131,78 @@ coerce_to_target_type(ParseState *pstate, Node *expr, Oid exprtype,
 	return result;
 }
 
+/*
+ * Label assignment length coercions with their destination column.  Follow
+ * only the coercion and assignment nodes added above source: an error in the
+ * source expression must not be attributed to the destination.  Coercion can
+ * strip off source's CollateExpr wrappers, so recognize its unwrapped form too.
+ *
+ * This is deliberately not a general expression-tree walk.  Each additional
+ * case must distinguish assignment-added coercions from the source expression.
+ * Unsupported shapes are left unannotated.
+ *
+ * With source == NULL, only update coercions that already have a target.  This
+ * is used to retarget defaults inherited from another column or table.
+ * The caller must own the expression tree, since we modify it in place.
+ */
+void
+set_coercion_target(Node *expr, Node *source, Oid relid, AttrNumber attnum)
+{
+	if (expr == NULL || expr == source)
+		return;
+	while (source && IsA(source, CollateExpr))
+	{
+		source = (Node *) ((CollateExpr *) source)->arg;
+		if (expr == source)
+			return;
+	}
+
+	switch (nodeTag(expr))
+	{
+		case T_FuncExpr:
+			{
+				FuncExpr   *fexpr = (FuncExpr *) expr;
+
+				if (fexpr->funcformat == COERCE_IMPLICIT_CAST &&
+					exprIsLengthCoercion(expr, NULL) &&
+					(source != NULL || OidIsValid(fexpr->functargetrelid)))
+				{
+					fexpr->functargetrelid = relid;
+					fexpr->functargetattnum = attnum;
+				}
+				break;
+			}
+		case T_CollateExpr:
+			set_coercion_target((Node *) ((CollateExpr *) expr)->arg,
+								source, relid, attnum);
+			break;
+		case T_RelabelType:
+			if (((RelabelType *) expr)->relabelformat == COERCE_IMPLICIT_CAST)
+				set_coercion_target((Node *) ((RelabelType *) expr)->arg,
+									source, relid, attnum);
+			break;
+		case T_CoerceToDomain:
+			if (((CoerceToDomain *) expr)->coercionformat == COERCE_IMPLICIT_CAST)
+				set_coercion_target((Node *) ((CoerceToDomain *) expr)->arg,
+									source, relid, attnum);
+			break;
+		case T_ArrayCoerceExpr:
+			if (((ArrayCoerceExpr *) expr)->coerceformat == COERCE_IMPLICIT_CAST)
+				set_coercion_target((Node *) ((ArrayCoerceExpr *) expr)->elemexpr,
+									source, relid, attnum);
+			break;
+		case T_FieldStore:
+			foreach_ptr(Node, value, ((FieldStore *) expr)->newvals)
+				set_coercion_target(value, source, relid, attnum);
+			break;
+		case T_SubscriptingRef:
+			set_coercion_target((Node *) ((SubscriptingRef *) expr)->refassgnexpr,
+								source, relid, attnum);
+			break;
+		default:
+			break;
+	}
+}
 
 /*
  * coerce_type()
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 728936b502e..66518de2043 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -466,6 +466,7 @@ transformAssignedExpr(ParseState *pstate,
 	int32		attrtypmod;
 	Oid			attrcollation;	/* collation of target column */
 	ParseExprKind sv_expr_kind;
+	Node	   *orig_expr = (Node *) expr;
 
 	/*
 	 * Save and restore identity of expression type we're parsing.  We must
@@ -576,8 +577,6 @@ transformAssignedExpr(ParseState *pstate,
 		 * For normal non-qualified target column, do type checking and
 		 * coercion.
 		 */
-		Node	   *orig_expr = (Node *) expr;
-
 		expr = (Expr *)
 			coerce_to_target_type(pstate,
 								  orig_expr, type_id,
@@ -597,6 +596,9 @@ transformAssignedExpr(ParseState *pstate,
 					 parser_errposition(pstate, exprLocation(orig_expr))));
 	}
 
+	set_coercion_target((Node *) expr, orig_expr,
+						RelationGetRelid(rd), attrno);
+
 	pstate->p_expr_kind = sv_expr_kind;
 
 	return expr;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 27964cb83c6..bce08a5c806 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1282,6 +1282,7 @@ build_column_default(Relation rel, int attrno)
 	int32		atttypmod = att_tup->atttypmod;
 	Node	   *expr = NULL;
 	Oid			exprtype;
+	Node	   *orig_expr;
 
 	if (att_tup->attidentity)
 	{
@@ -1321,6 +1322,7 @@ build_column_default(Relation rel, int attrno)
 	 * the parser's processing of non-defaulted expressions --- see
 	 * transformAssignedExpr().
 	 */
+	orig_expr = expr;
 	exprtype = exprType(expr);
 
 	expr = coerce_to_target_type(NULL,	/* no UNKNOWN params here */
@@ -1339,6 +1341,8 @@ build_column_default(Relation rel, int attrno)
 						format_type_be(exprtype)),
 				 errhint("You will need to rewrite or cast the expression.")));
 
+	set_coercion_target(expr, orig_expr, RelationGetRelid(rel), attrno);
+
 	return expr;
 }
 
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 6f3e526de96..7d9065c51de 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202609152
+#define CATALOG_VERSION_NO	202609271
 
 #endif
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index 6c9ac812aa0..7948a662076 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -131,7 +131,8 @@ extern Node *cookDefault(ParseState *pstate,
 						 Oid atttypid,
 						 int32 atttypmod,
 						 const char *attname,
-						 char attgenerated);
+						 char attgenerated,
+						 Oid relid, AttrNumber attnum);
 
 extern void DeleteRelationTuple(Oid relid);
 extern void DeleteAttributeTuples(Oid relid);
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index c61b3d624d5..d9ba1c66397 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -125,6 +125,7 @@ typedef enum ExprEvalOp
 	EEOP_FUNCEXPR_STRICT_2,
 	EEOP_FUNCEXPR_FUSAGE,
 	EEOP_FUNCEXPR_STRICT_FUSAGE,
+	EEOP_FUNCEXPR_COERCION,
 
 	/*
 	 * Evaluate boolean AND expression, one step per subexpression. FIRST/LAST
@@ -856,6 +857,8 @@ extern void ExecEvalFuncExprFusage(ExprState *state, ExprEvalStep *op,
 								   ExprContext *econtext);
 extern void ExecEvalFuncExprStrictFusage(ExprState *state, ExprEvalStep *op,
 										 ExprContext *econtext);
+extern void ExecEvalFuncExprCoercion(ExprState *state, ExprEvalStep *op,
+									 ExprContext *econtext);
 extern void ExecEvalParamExec(ExprState *state, ExprEvalStep *op,
 							  ExprContext *econtext);
 extern void ExecEvalParamSet(ExprState *state, ExprEvalStep *op,
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 2a832a27f49..626d60820ff 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -790,6 +790,11 @@ typedef struct FuncExpr
 	Oid			inputcollid pg_node_attr(query_jumble_ignore);
 	/* arguments to the function */
 	List	   *args;
+
+	/* destination column of an assignment coercion */
+	Oid			functargetrelid pg_node_attr(equal_ignore, query_jumble_ignore);
+	AttrNumber	functargetattnum pg_node_attr(equal_ignore, query_jumble_ignore);
+
 	/* token location, or -1 if unknown */
 	ParseLoc	location;
 } FuncExpr;
diff --git a/src/include/parser/parse_coerce.h b/src/include/parser/parse_coerce.h
index aabacd49b65..b3be6326283 100644
--- a/src/include/parser/parse_coerce.h
+++ b/src/include/parser/parse_coerce.h
@@ -45,6 +45,8 @@ extern Node *coerce_to_target_type(ParseState *pstate,
 								   int location);
 extern bool can_coerce_type(int nargs, const Oid *input_typeids, const Oid *target_typeids,
 							CoercionContext ccontext);
+extern void set_coercion_target(Node *expr, Node *source,
+								Oid relid, AttrNumber attnum);
 extern Node *coerce_type(ParseState *pstate, Node *node,
 						 Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
 						 CoercionContext ccontext, CoercionForm cformat, int location);
diff --git a/src/test/regress/expected/arrays.out b/src/test/regress/expected/arrays.out
index 66439d427a3..e4b9d99081d 100644
--- a/src/test/regress/expected/arrays.out
+++ b/src/test/regress/expected/arrays.out
@@ -30,6 +30,7 @@ UPDATE arrtest SET e[1] = '2.2';
 INSERT INTO arrtest (f)
    VALUES ('{"too long"}');
 ERROR:  value too long for type character(5)
+CONTEXT:  column "f" of relation "arrtest"
 INSERT INTO arrtest (a, b[1:2][1:2], c, d, e, f, g)
    VALUES ('{11,12,23}', '{{3,4},{4,5}}', '{"foobar"}',
            '{{"elt1", "elt2"}}', '{"3.4", "6.7"}',
diff --git a/src/test/regress/expected/bit.out b/src/test/regress/expected/bit.out
index 6a436288bb1..48aa1840f5d 100644
--- a/src/test/regress/expected/bit.out
+++ b/src/test/regress/expected/bit.out
@@ -7,11 +7,13 @@
 CREATE TABLE BIT_TABLE(b BIT(11));
 INSERT INTO BIT_TABLE VALUES (B'10'); -- too short
 ERROR:  bit string length 2 does not match type bit(11)
+CONTEXT:  column "b" of relation "bit_table"
 INSERT INTO BIT_TABLE VALUES (B'00000000000');
 INSERT INTO BIT_TABLE VALUES (B'11011000000');
 INSERT INTO BIT_TABLE VALUES (B'01010101010');
 INSERT INTO BIT_TABLE VALUES (B'101011111010'); -- too long
 ERROR:  bit string length 12 does not match type bit(11)
+CONTEXT:  column "b" of relation "bit_table"
 --INSERT INTO BIT_TABLE VALUES ('X554');
 --INSERT INTO BIT_TABLE VALUES ('X555');
 SELECT * FROM BIT_TABLE;
@@ -29,6 +31,7 @@ INSERT INTO VARBIT_TABLE VALUES (B'010101');
 INSERT INTO VARBIT_TABLE VALUES (B'01010101010');
 INSERT INTO VARBIT_TABLE VALUES (B'101011111010'); -- too long
 ERROR:  bit string too long for type bit varying(11)
+CONTEXT:  column "v" of relation "varbit_table"
 --INSERT INTO VARBIT_TABLE VALUES ('X554');
 --INSERT INTO VARBIT_TABLE VALUES ('X555');
 SELECT * FROM VARBIT_TABLE;
diff --git a/src/test/regress/expected/char.out b/src/test/regress/expected/char.out
index 4df596d111d..a20f07d189b 100644
--- a/src/test/regress/expected/char.out
+++ b/src/test/regress/expected/char.out
@@ -25,6 +25,7 @@ INSERT INTO CHAR_TBL (f1) VALUES ('');
 -- try char's of greater than 1 length
 INSERT INTO CHAR_TBL (f1) VALUES ('cd');
 ERROR:  value too long for type character(1)
+CONTEXT:  column "f1" of relation "char_tbl"
 INSERT INTO CHAR_TBL (f1) VALUES ('c     ');
 SELECT * FROM CHAR_TBL;
  f1 
@@ -110,6 +111,7 @@ DROP TABLE CHAR_TBL;
 --
 INSERT INTO CHAR_TBL (f1) VALUES ('abcde');
 ERROR:  value too long for type character(4)
+CONTEXT:  column "f1" of relation "char_tbl"
 SELECT * FROM CHAR_TBL;
   f1  
 ------
diff --git a/src/test/regress/expected/char_1.out b/src/test/regress/expected/char_1.out
index 3add81e9f0f..7dd071b9051 100644
--- a/src/test/regress/expected/char_1.out
+++ b/src/test/regress/expected/char_1.out
@@ -25,6 +25,7 @@ INSERT INTO CHAR_TBL (f1) VALUES ('');
 -- try char's of greater than 1 length
 INSERT INTO CHAR_TBL (f1) VALUES ('cd');
 ERROR:  value too long for type character(1)
+CONTEXT:  column "f1" of relation "char_tbl"
 INSERT INTO CHAR_TBL (f1) VALUES ('c     ');
 SELECT * FROM CHAR_TBL;
  f1 
@@ -110,6 +111,7 @@ DROP TABLE CHAR_TBL;
 --
 INSERT INTO CHAR_TBL (f1) VALUES ('abcde');
 ERROR:  value too long for type character(4)
+CONTEXT:  column "f1" of relation "char_tbl"
 SELECT * FROM CHAR_TBL;
   f1  
 ------
diff --git a/src/test/regress/expected/char_2.out b/src/test/regress/expected/char_2.out
index ebde0f3e59f..c31f0b892a5 100644
--- a/src/test/regress/expected/char_2.out
+++ b/src/test/regress/expected/char_2.out
@@ -25,6 +25,7 @@ INSERT INTO CHAR_TBL (f1) VALUES ('');
 -- try char's of greater than 1 length
 INSERT INTO CHAR_TBL (f1) VALUES ('cd');
 ERROR:  value too long for type character(1)
+CONTEXT:  column "f1" of relation "char_tbl"
 INSERT INTO CHAR_TBL (f1) VALUES ('c     ');
 SELECT * FROM CHAR_TBL;
  f1 
@@ -110,6 +111,7 @@ DROP TABLE CHAR_TBL;
 --
 INSERT INTO CHAR_TBL (f1) VALUES ('abcde');
 ERROR:  value too long for type character(4)
+CONTEXT:  column "f1" of relation "char_tbl"
 SELECT * FROM CHAR_TBL;
   f1  
 ------
diff --git a/src/test/regress/expected/copy.out b/src/test/regress/expected/copy.out
index 0af0b646921..ce46a7bfb55 100644
--- a/src/test/regress/expected/copy.out
+++ b/src/test/regress/expected/copy.out
@@ -518,9 +518,11 @@ copy oversized_column_default from stdin;
 -- error if the column is excluded
 copy oversized_column_default (col2) from stdin;
 ERROR:  value too long for type character varying(5)
+CONTEXT:  column "col1" of relation "oversized_column_default"
 -- error if the DEFAULT option is given
 copy oversized_column_default from stdin (default '');
 ERROR:  value too long for type character varying(5)
+CONTEXT:  column "col1" of relation "oversized_column_default"
 drop table oversized_column_default;
 --
 -- Create partitioned table that does not allow bulk insertions, to test bugs
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 62a48a523a2..bb2b1b2c6e8 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -98,6 +98,7 @@ create table basictest
 INSERT INTO basictest values ('88', 'haha', 'short', '123.12');      -- Good
 INSERT INTO basictest values ('88', 'haha', 'short text', '123.12'); -- Bad varchar
 ERROR:  value too long for type character varying(5)
+CONTEXT:  column "testvarchar" of relation "basictest"
 INSERT INTO basictest values ('88', 'haha', 'short', '123.1212');    -- Truncate numeric
 -- Test copy
 COPY basictest (testvarchar) FROM stdin; -- fail
@@ -204,6 +205,7 @@ INSERT INTO domarrtest values ('{2,2}', '{{"a"},{"c"}}');
 INSERT INTO domarrtest values (NULL, '{{"a","b","c"},{"d","e","f"}}');
 INSERT INTO domarrtest values (NULL, '{{"toolong","b","c"},{"d","e","f"}}');
 ERROR:  value too long for type character varying(4)
+CONTEXT:  column "testchar4arr" of relation "domarrtest"
 INSERT INTO domarrtest (testint4arr[1], testint4arr[3]) values (11,22);
 select * from domarrtest;
   testint4arr  |    testchar4arr     
@@ -568,6 +570,7 @@ create domain vc4 as varchar(4);
 create table vc4table (f1 vc4[]);
 insert into vc4table values(array['too long']);  -- fail
 ERROR:  value too long for type character varying(4)
+CONTEXT:  column "f1" of relation "vc4table"
 insert into vc4table values(array['too long']::vc4[]);  -- cast truncates
 select * from vc4table;
     f1    
@@ -997,8 +1000,10 @@ create temp table dtest(f1 dtop);
 insert into dtest values('x123');
 insert into dtest values('x1234'); -- fail, implicit coercion
 ERROR:  value too long for type character varying(4)
+CONTEXT:  column "f1" of relation "dtest"
 insert into dtest values('y1234'); -- fail, implicit coercion
 ERROR:  value too long for type character varying(4)
+CONTEXT:  column "f1" of relation "dtest"
 insert into dtest values('y123'); -- fail
 ERROR:  value for domain dtop violates check constraint "dinter_check"
 insert into dtest values('yz23'); -- fail
diff --git a/src/test/regress/expected/numeric.out b/src/test/regress/expected/numeric.out
index c58e232a263..4c33dba43c1 100644
--- a/src/test/regress/expected/numeric.out
+++ b/src/test/regress/expected/numeric.out
@@ -1127,20 +1127,24 @@ INSERT INTO fract_only VALUES (2, '0.1');
 INSERT INTO fract_only VALUES (3, '1.0');	-- should fail
 ERROR:  numeric field overflow
 DETAIL:  A field with precision 4, scale 4 must round to an absolute value less than 1.
+CONTEXT:  column "val" of relation "fract_only"
 INSERT INTO fract_only VALUES (4, '-0.9999');
 INSERT INTO fract_only VALUES (5, '0.99994');
 INSERT INTO fract_only VALUES (6, '0.99995');  -- should fail
 ERROR:  numeric field overflow
 DETAIL:  A field with precision 4, scale 4 must round to an absolute value less than 1.
+CONTEXT:  column "val" of relation "fract_only"
 INSERT INTO fract_only VALUES (7, '0.00001');
 INSERT INTO fract_only VALUES (8, '0.00017');
 INSERT INTO fract_only VALUES (9, 'NaN');
 INSERT INTO fract_only VALUES (10, 'Inf');	-- should fail
 ERROR:  numeric field overflow
 DETAIL:  A field with precision 4, scale 4 cannot hold an infinite value.
+CONTEXT:  column "val" of relation "fract_only"
 INSERT INTO fract_only VALUES (11, '-Inf');	-- should fail
 ERROR:  numeric field overflow
 DETAIL:  A field with precision 4, scale 4 cannot hold an infinite value.
+CONTEXT:  column "val" of relation "fract_only"
 SELECT * FROM fract_only;
  id |   val   
 ----+---------
@@ -2739,21 +2743,27 @@ SELECT scale(millions), * FROM num_typemod_test ORDER BY millions;
 INSERT INTO num_typemod_test (millions) VALUES ('inf');
 ERROR:  numeric field overflow
 DETAIL:  A field with precision 3, scale -6 cannot hold an infinite value.
+CONTEXT:  column "millions" of relation "num_typemod_test"
 INSERT INTO num_typemod_test (millions) VALUES (999500000);
 ERROR:  numeric field overflow
 DETAIL:  A field with precision 3, scale -6 must round to an absolute value less than 10^9.
+CONTEXT:  column "millions" of relation "num_typemod_test"
 INSERT INTO num_typemod_test (thousands) VALUES (999500);
 ERROR:  numeric field overflow
 DETAIL:  A field with precision 3, scale -3 must round to an absolute value less than 10^6.
+CONTEXT:  column "thousands" of relation "num_typemod_test"
 INSERT INTO num_typemod_test (units) VALUES (999.5);
 ERROR:  numeric field overflow
 DETAIL:  A field with precision 3, scale 0 must round to an absolute value less than 10^3.
+CONTEXT:  column "units" of relation "num_typemod_test"
 INSERT INTO num_typemod_test (thousandths) VALUES (0.9995);
 ERROR:  numeric field overflow
 DETAIL:  A field with precision 3, scale 3 must round to an absolute value less than 1.
+CONTEXT:  column "thousandths" of relation "num_typemod_test"
 INSERT INTO num_typemod_test (millionths) VALUES (0.0009995);
 ERROR:  numeric field overflow
 DETAIL:  A field with precision 3, scale 6 must round to an absolute value less than 10^-3.
+CONTEXT:  column "millionths" of relation "num_typemod_test"
 --
 -- Test some corner cases for multiplication
 --
diff --git a/src/test/regress/expected/varchar.out b/src/test/regress/expected/varchar.out
index 28863529a6b..7bac6cbdd40 100644
--- a/src/test/regress/expected/varchar.out
+++ b/src/test/regress/expected/varchar.out
@@ -17,6 +17,7 @@ INSERT INTO VARCHAR_TBL (f1) VALUES ('');
 -- try varchar's of greater than 1 length
 INSERT INTO VARCHAR_TBL (f1) VALUES ('cd');
 ERROR:  value too long for type character varying(1)
+CONTEXT:  column "f1" of relation "varchar_tbl"
 INSERT INTO VARCHAR_TBL (f1) VALUES ('c     ');
 SELECT * FROM VARCHAR_TBL;
  f1 
@@ -102,6 +103,7 @@ DROP TABLE VARCHAR_TBL;
 --
 INSERT INTO VARCHAR_TBL (f1) VALUES ('abcde');
 ERROR:  value too long for type character varying(4)
+CONTEXT:  column "f1" of relation "varchar_tbl"
 SELECT * FROM VARCHAR_TBL;
   f1  
 ------
@@ -130,3 +132,222 @@ SELECT * FROM pg_input_error_info('abcde', 'varchar(4)');
  value too long for type character varying(4) |        |      | 22001
 (1 row)
 
+-- report the destination column for planning-time and runtime errors
+CREATE TEMP TABLE varchar_target (id int, username varchar(10), email varchar(10));
+INSERT INTO varchar_target (email, username) VALUES ('123456789012', 'ok');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "email" of relation "varchar_target"
+INSERT INTO varchar_target (username) VALUES ('ok'), ('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+CREATE TEMP TABLE varchar_source (v text);
+INSERT INTO varchar_source VALUES ('123456789012');
+INSERT INTO varchar_target VALUES (1, 'ok', 'ok');
+UPDATE varchar_target SET username = (SELECT v FROM varchar_source);
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+-- keep the destination in generic plans and use the current column name
+SET plan_cache_mode = force_generic_plan;
+PREPARE varchar_insert(text) AS
+  INSERT INTO varchar_target VALUES (2, $1, NULL);
+EXECUTE varchar_insert('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+ALTER TABLE varchar_target RENAME COLUMN username TO "User Name";
+EXECUTE varchar_insert('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "User Name" of relation "varchar_target"
+DEALLOCATE varchar_insert;
+RESET plan_cache_mode;
+-- keep the column identity in stored SQL bodies across renames
+CREATE FUNCTION pg_temp.varchar_insert_fn(text) RETURNS void
+LANGUAGE SQL BEGIN ATOMIC
+  INSERT INTO varchar_target VALUES (3, $1, NULL);
+END;
+ALTER TABLE varchar_target RENAME COLUMN "User Name" TO username;
+SELECT pg_temp.varchar_insert_fn('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+SQL function "varchar_insert_fn" statement 1
+DROP FUNCTION pg_temp.varchar_insert_fn(text);
+-- runtime diagnostics and callback cleanup
+DO $$
+DECLARE
+  message text;
+  context text;
+  colname text;
+  tabname text;
+  schemaname text;
+BEGIN
+  BEGIN
+    INSERT INTO varchar_target (email) SELECT v FROM varchar_source;
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS message = MESSAGE_TEXT,
+                            context = PG_EXCEPTION_CONTEXT,
+                            colname = COLUMN_NAME,
+                            tabname = TABLE_NAME,
+                            schemaname = SCHEMA_NAME;
+    RAISE NOTICE 'generic error: %, destination context: %, object fields empty: %',
+      message = 'value too long for type character varying(10)',
+      position('column "email" of relation "varchar_target"' in context) = 1,
+      colname = '' AND tabname = '' AND schemaname = '';
+  END;
+
+  -- Context must not leak after an error, a NULL input, or a successful call.
+  -- Source argument errors must not acquire the destination context.
+  INSERT INTO varchar_target (username) SELECT NULLIF(v, v) FROM varchar_source;
+  BEGIN
+    INSERT INTO varchar_target (username, email)
+      VALUES ((SELECT 'ok'::text FROM varchar_source),
+              pg_catalog.varchar((SELECT v FROM varchar_source), 15, false));
+    RAISE EXCEPTION 'expected a source length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_target"' in context) <> 0 THEN
+      RAISE EXCEPTION 'unexpected assignment context: %', context;
+    END IF;
+  END;
+  BEGIN
+    PERFORM pg_catalog.varchar((SELECT v FROM varchar_source), 14, false);
+    RAISE EXCEPTION 'expected a length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_target"' in context) <> 0 THEN
+      RAISE EXCEPTION 'assignment context leaked: %', context;
+    END IF;
+  END;
+END;
+$$;
+NOTICE:  generic error: t, destination context: t, object fields empty: t
+-- collation and array subscript assignments
+INSERT INTO varchar_target (username) VALUES ('123456789012' COLLATE "C");
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+CREATE TEMP TABLE varchar_nested (a varchar(10)[]);
+INSERT INTO varchar_nested (a) VALUES (ARRAY['ok']);
+UPDATE varchar_nested SET a[1] = (SELECT v FROM varchar_source);
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "a" of relation "varchar_nested"
+-- composite literals and row coercions currently omit the column
+-- direct field assignments report the containing column
+CREATE TYPE varchar_composite AS (v varchar(10));
+CREATE TEMP TABLE varchar_composite_target (c varchar_composite);
+INSERT INTO varchar_composite_target VALUES ('(123456789012)');
+ERROR:  value too long for type character varying(10)
+LINE 1: INSERT INTO varchar_composite_target VALUES ('(123456789012)...
+                                                     ^
+INSERT INTO varchar_composite_target VALUES (ROW('123456789012'));
+ERROR:  value too long for type character varying(10)
+INSERT INTO varchar_composite_target (c.v) VALUES ('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "c" of relation "varchar_composite_target"
+DROP TABLE varchar_composite_target;
+DROP TYPE varchar_composite;
+-- errors in the source expression must not name the destination
+INSERT INTO varchar_target (username)
+  VALUES (pg_catalog.varchar('123456789012', 5, false));
+ERROR:  value too long for type character varying(1)
+-- XMLSERIALIZE coercions belong to the source expression
+-- skip in builds without XML support
+DO $$
+DECLARE
+  context text;
+BEGIN
+  BEGIN
+    PERFORM XMLPARSE(CONTENT '<a/>');
+  EXCEPTION WHEN feature_not_supported THEN
+    RETURN;
+  END;
+  CREATE TEMP TABLE varchar_unbounded (
+    v text DEFAULT XMLSERIALIZE(CONTENT '<a>long</a>' AS varchar(3)));
+  BEGIN
+    INSERT INTO varchar_unbounded
+      VALUES (XMLSERIALIZE(CONTENT '<a>long</a>' AS varchar(3)) COLLATE "C");
+    RAISE EXCEPTION 'expected a length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_unbounded"' in context) <> 0 THEN
+      RAISE EXCEPTION 'source error attributed to column %', context;
+    END IF;
+  END;
+  BEGIN
+    INSERT INTO varchar_unbounded DEFAULT VALUES;
+    RAISE EXCEPTION 'expected a length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_unbounded"' in context) <> 0 THEN
+      RAISE EXCEPTION 'default source error attributed to column %', context;
+    END IF;
+  END;
+  DROP TABLE varchar_unbounded;
+END;
+$$;
+-- column defaults and stored generated columns
+CREATE TEMP TABLE varchar_default (
+  username varchar(10) DEFAULT repeat('x', 11 + (random() * 0)::int));
+INSERT INTO varchar_default DEFAULT VALUES;
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_default"
+CREATE TEMP TABLE varchar_generated (
+  v text, username varchar(10) GENERATED ALWAYS AS (v) STORED);
+INSERT INTO varchar_generated (v) VALUES ('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_generated"
+-- domain defaults currently omit the destination column
+-- random() prevents constant folding; the default always produces 11 characters
+CREATE DOMAIN varchar_default_domain AS varchar(10)
+  DEFAULT repeat('x', 11 + (random() * 0)::int);
+CREATE TEMP TABLE varchar_domain_default (d varchar_default_domain);
+INSERT INTO varchar_domain_default DEFAULT VALUES;
+ERROR:  value too long for type character varying(10)
+DROP TABLE varchar_domain_default;
+DROP DOMAIN varchar_default_domain;
+-- copied defaults must refer to the new column
+CREATE TEMP TABLE varchar_default_copy (LIKE varchar_default INCLUDING DEFAULTS);
+DROP TABLE varchar_default;
+ALTER TABLE varchar_default_copy RENAME COLUMN username TO copied_name;
+INSERT INTO varchar_default_copy DEFAULT VALUES;
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "copied_name" of relation "varchar_default_copy"
+ALTER TABLE varchar_default_copy ALTER COLUMN copied_name TYPE varchar(12);
+ALTER TABLE varchar_default_copy ALTER COLUMN copied_name TYPE varchar(10);
+INSERT INTO varchar_default_copy DEFAULT VALUES;
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "copied_name" of relation "varchar_default_copy"
+-- diagnostic metadata must not affect equality of inherited defaults
+CREATE TEMP TABLE varchar_parent1 (v varchar(10) DEFAULT 'ok');
+CREATE TEMP TABLE varchar_parent2 (v varchar(10) DEFAULT 'ok');
+CREATE TEMP TABLE varchar_child () INHERITS (varchar_parent1, varchar_parent2);
+NOTICE:  merging multiple inherited definitions of column "v"
+INSERT INTO varchar_child DEFAULT VALUES;
+SELECT * FROM varchar_child;
+ v  
+----
+ ok
+(1 row)
+
+DROP TABLE varchar_child, varchar_parent1, varchar_parent2;
+-- report the destination for column type changes
+ALTER TABLE varchar_source ALTER COLUMN v TYPE varchar(10);
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "v" of relation "varchar_source"
+-- SQL coercion inlining must not discard destination context
+-- roll back the cast so it is never visible to other regression sessions
+BEGIN;
+CREATE FUNCTION pg_temp.varchar_sql_coercion(integer, integer)
+RETURNS varchar LANGUAGE SQL IMMUTABLE STRICT
+RETURN ($1 / ($2 - 9))::text::varchar;
+CREATE CAST (integer AS varchar)
+WITH FUNCTION pg_temp.varchar_sql_coercion(integer, integer) AS ASSIGNMENT;
+CREATE TEMP TABLE varchar_sql_source (n integer);
+INSERT INTO varchar_sql_source VALUES (1), (NULL);
+CREATE TEMP TABLE varchar_sql_target (v varchar(5));
+-- strict coercions must skip the function for a NULL argument
+INSERT INTO varchar_sql_target SELECT n FROM varchar_sql_source WHERE n IS NULL;
+INSERT INTO varchar_sql_target SELECT n FROM varchar_sql_source WHERE n = 1;
+ERROR:  division by zero
+CONTEXT:  SQL function "varchar_sql_coercion" statement 1
+column "v" of relation "varchar_sql_target"
+ROLLBACK;
+DROP TABLE varchar_source, varchar_target, varchar_nested,
+  varchar_default_copy, varchar_generated;
diff --git a/src/test/regress/expected/varchar_1.out b/src/test/regress/expected/varchar_1.out
index 7cb74c752a4..6fe91191336 100644
--- a/src/test/regress/expected/varchar_1.out
+++ b/src/test/regress/expected/varchar_1.out
@@ -17,6 +17,7 @@ INSERT INTO VARCHAR_TBL (f1) VALUES ('');
 -- try varchar's of greater than 1 length
 INSERT INTO VARCHAR_TBL (f1) VALUES ('cd');
 ERROR:  value too long for type character varying(1)
+CONTEXT:  column "f1" of relation "varchar_tbl"
 INSERT INTO VARCHAR_TBL (f1) VALUES ('c     ');
 SELECT * FROM VARCHAR_TBL;
  f1 
@@ -102,6 +103,7 @@ DROP TABLE VARCHAR_TBL;
 --
 INSERT INTO VARCHAR_TBL (f1) VALUES ('abcde');
 ERROR:  value too long for type character varying(4)
+CONTEXT:  column "f1" of relation "varchar_tbl"
 SELECT * FROM VARCHAR_TBL;
   f1  
 ------
@@ -130,3 +132,222 @@ SELECT * FROM pg_input_error_info('abcde', 'varchar(4)');
  value too long for type character varying(4) |        |      | 22001
 (1 row)
 
+-- report the destination column for planning-time and runtime errors
+CREATE TEMP TABLE varchar_target (id int, username varchar(10), email varchar(10));
+INSERT INTO varchar_target (email, username) VALUES ('123456789012', 'ok');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "email" of relation "varchar_target"
+INSERT INTO varchar_target (username) VALUES ('ok'), ('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+CREATE TEMP TABLE varchar_source (v text);
+INSERT INTO varchar_source VALUES ('123456789012');
+INSERT INTO varchar_target VALUES (1, 'ok', 'ok');
+UPDATE varchar_target SET username = (SELECT v FROM varchar_source);
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+-- keep the destination in generic plans and use the current column name
+SET plan_cache_mode = force_generic_plan;
+PREPARE varchar_insert(text) AS
+  INSERT INTO varchar_target VALUES (2, $1, NULL);
+EXECUTE varchar_insert('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+ALTER TABLE varchar_target RENAME COLUMN username TO "User Name";
+EXECUTE varchar_insert('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "User Name" of relation "varchar_target"
+DEALLOCATE varchar_insert;
+RESET plan_cache_mode;
+-- keep the column identity in stored SQL bodies across renames
+CREATE FUNCTION pg_temp.varchar_insert_fn(text) RETURNS void
+LANGUAGE SQL BEGIN ATOMIC
+  INSERT INTO varchar_target VALUES (3, $1, NULL);
+END;
+ALTER TABLE varchar_target RENAME COLUMN "User Name" TO username;
+SELECT pg_temp.varchar_insert_fn('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+SQL function "varchar_insert_fn" statement 1
+DROP FUNCTION pg_temp.varchar_insert_fn(text);
+-- runtime diagnostics and callback cleanup
+DO $$
+DECLARE
+  message text;
+  context text;
+  colname text;
+  tabname text;
+  schemaname text;
+BEGIN
+  BEGIN
+    INSERT INTO varchar_target (email) SELECT v FROM varchar_source;
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS message = MESSAGE_TEXT,
+                            context = PG_EXCEPTION_CONTEXT,
+                            colname = COLUMN_NAME,
+                            tabname = TABLE_NAME,
+                            schemaname = SCHEMA_NAME;
+    RAISE NOTICE 'generic error: %, destination context: %, object fields empty: %',
+      message = 'value too long for type character varying(10)',
+      position('column "email" of relation "varchar_target"' in context) = 1,
+      colname = '' AND tabname = '' AND schemaname = '';
+  END;
+
+  -- Context must not leak after an error, a NULL input, or a successful call.
+  -- Source argument errors must not acquire the destination context.
+  INSERT INTO varchar_target (username) SELECT NULLIF(v, v) FROM varchar_source;
+  BEGIN
+    INSERT INTO varchar_target (username, email)
+      VALUES ((SELECT 'ok'::text FROM varchar_source),
+              pg_catalog.varchar((SELECT v FROM varchar_source), 15, false));
+    RAISE EXCEPTION 'expected a source length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_target"' in context) <> 0 THEN
+      RAISE EXCEPTION 'unexpected assignment context: %', context;
+    END IF;
+  END;
+  BEGIN
+    PERFORM pg_catalog.varchar((SELECT v FROM varchar_source), 14, false);
+    RAISE EXCEPTION 'expected a length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_target"' in context) <> 0 THEN
+      RAISE EXCEPTION 'assignment context leaked: %', context;
+    END IF;
+  END;
+END;
+$$;
+NOTICE:  generic error: t, destination context: t, object fields empty: t
+-- collation and array subscript assignments
+INSERT INTO varchar_target (username) VALUES ('123456789012' COLLATE "C");
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+CREATE TEMP TABLE varchar_nested (a varchar(10)[]);
+INSERT INTO varchar_nested (a) VALUES (ARRAY['ok']);
+UPDATE varchar_nested SET a[1] = (SELECT v FROM varchar_source);
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "a" of relation "varchar_nested"
+-- composite literals and row coercions currently omit the column
+-- direct field assignments report the containing column
+CREATE TYPE varchar_composite AS (v varchar(10));
+CREATE TEMP TABLE varchar_composite_target (c varchar_composite);
+INSERT INTO varchar_composite_target VALUES ('(123456789012)');
+ERROR:  value too long for type character varying(10)
+LINE 1: INSERT INTO varchar_composite_target VALUES ('(123456789012)...
+                                                     ^
+INSERT INTO varchar_composite_target VALUES (ROW('123456789012'));
+ERROR:  value too long for type character varying(10)
+INSERT INTO varchar_composite_target (c.v) VALUES ('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "c" of relation "varchar_composite_target"
+DROP TABLE varchar_composite_target;
+DROP TYPE varchar_composite;
+-- errors in the source expression must not name the destination
+INSERT INTO varchar_target (username)
+  VALUES (pg_catalog.varchar('123456789012', 5, false));
+ERROR:  value too long for type character varying(1)
+-- XMLSERIALIZE coercions belong to the source expression
+-- skip in builds without XML support
+DO $$
+DECLARE
+  context text;
+BEGIN
+  BEGIN
+    PERFORM XMLPARSE(CONTENT '<a/>');
+  EXCEPTION WHEN feature_not_supported THEN
+    RETURN;
+  END;
+  CREATE TEMP TABLE varchar_unbounded (
+    v text DEFAULT XMLSERIALIZE(CONTENT '<a>long</a>' AS varchar(3)));
+  BEGIN
+    INSERT INTO varchar_unbounded
+      VALUES (XMLSERIALIZE(CONTENT '<a>long</a>' AS varchar(3)) COLLATE "C");
+    RAISE EXCEPTION 'expected a length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_unbounded"' in context) <> 0 THEN
+      RAISE EXCEPTION 'source error attributed to column %', context;
+    END IF;
+  END;
+  BEGIN
+    INSERT INTO varchar_unbounded DEFAULT VALUES;
+    RAISE EXCEPTION 'expected a length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_unbounded"' in context) <> 0 THEN
+      RAISE EXCEPTION 'default source error attributed to column %', context;
+    END IF;
+  END;
+  DROP TABLE varchar_unbounded;
+END;
+$$;
+-- column defaults and stored generated columns
+CREATE TEMP TABLE varchar_default (
+  username varchar(10) DEFAULT repeat('x', 11 + (random() * 0)::int));
+INSERT INTO varchar_default DEFAULT VALUES;
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_default"
+CREATE TEMP TABLE varchar_generated (
+  v text, username varchar(10) GENERATED ALWAYS AS (v) STORED);
+INSERT INTO varchar_generated (v) VALUES ('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_generated"
+-- domain defaults currently omit the destination column
+-- random() prevents constant folding; the default always produces 11 characters
+CREATE DOMAIN varchar_default_domain AS varchar(10)
+  DEFAULT repeat('x', 11 + (random() * 0)::int);
+CREATE TEMP TABLE varchar_domain_default (d varchar_default_domain);
+INSERT INTO varchar_domain_default DEFAULT VALUES;
+ERROR:  value too long for type character varying(10)
+DROP TABLE varchar_domain_default;
+DROP DOMAIN varchar_default_domain;
+-- copied defaults must refer to the new column
+CREATE TEMP TABLE varchar_default_copy (LIKE varchar_default INCLUDING DEFAULTS);
+DROP TABLE varchar_default;
+ALTER TABLE varchar_default_copy RENAME COLUMN username TO copied_name;
+INSERT INTO varchar_default_copy DEFAULT VALUES;
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "copied_name" of relation "varchar_default_copy"
+ALTER TABLE varchar_default_copy ALTER COLUMN copied_name TYPE varchar(12);
+ALTER TABLE varchar_default_copy ALTER COLUMN copied_name TYPE varchar(10);
+INSERT INTO varchar_default_copy DEFAULT VALUES;
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "copied_name" of relation "varchar_default_copy"
+-- diagnostic metadata must not affect equality of inherited defaults
+CREATE TEMP TABLE varchar_parent1 (v varchar(10) DEFAULT 'ok');
+CREATE TEMP TABLE varchar_parent2 (v varchar(10) DEFAULT 'ok');
+CREATE TEMP TABLE varchar_child () INHERITS (varchar_parent1, varchar_parent2);
+NOTICE:  merging multiple inherited definitions of column "v"
+INSERT INTO varchar_child DEFAULT VALUES;
+SELECT * FROM varchar_child;
+ v  
+----
+ ok
+(1 row)
+
+DROP TABLE varchar_child, varchar_parent1, varchar_parent2;
+-- report the destination for column type changes
+ALTER TABLE varchar_source ALTER COLUMN v TYPE varchar(10);
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "v" of relation "varchar_source"
+-- SQL coercion inlining must not discard destination context
+-- roll back the cast so it is never visible to other regression sessions
+BEGIN;
+CREATE FUNCTION pg_temp.varchar_sql_coercion(integer, integer)
+RETURNS varchar LANGUAGE SQL IMMUTABLE STRICT
+RETURN ($1 / ($2 - 9))::text::varchar;
+CREATE CAST (integer AS varchar)
+WITH FUNCTION pg_temp.varchar_sql_coercion(integer, integer) AS ASSIGNMENT;
+CREATE TEMP TABLE varchar_sql_source (n integer);
+INSERT INTO varchar_sql_source VALUES (1), (NULL);
+CREATE TEMP TABLE varchar_sql_target (v varchar(5));
+-- strict coercions must skip the function for a NULL argument
+INSERT INTO varchar_sql_target SELECT n FROM varchar_sql_source WHERE n IS NULL;
+INSERT INTO varchar_sql_target SELECT n FROM varchar_sql_source WHERE n = 1;
+ERROR:  division by zero
+CONTEXT:  SQL function "varchar_sql_coercion" statement 1
+column "v" of relation "varchar_sql_target"
+ROLLBACK;
+DROP TABLE varchar_source, varchar_target, varchar_nested,
+  varchar_default_copy, varchar_generated;
diff --git a/src/test/regress/expected/varchar_2.out b/src/test/regress/expected/varchar_2.out
index 9b154c6ca53..ead04cdce10 100644
--- a/src/test/regress/expected/varchar_2.out
+++ b/src/test/regress/expected/varchar_2.out
@@ -17,6 +17,7 @@ INSERT INTO VARCHAR_TBL (f1) VALUES ('');
 -- try varchar's of greater than 1 length
 INSERT INTO VARCHAR_TBL (f1) VALUES ('cd');
 ERROR:  value too long for type character varying(1)
+CONTEXT:  column "f1" of relation "varchar_tbl"
 INSERT INTO VARCHAR_TBL (f1) VALUES ('c     ');
 SELECT * FROM VARCHAR_TBL;
  f1 
@@ -102,6 +103,7 @@ DROP TABLE VARCHAR_TBL;
 --
 INSERT INTO VARCHAR_TBL (f1) VALUES ('abcde');
 ERROR:  value too long for type character varying(4)
+CONTEXT:  column "f1" of relation "varchar_tbl"
 SELECT * FROM VARCHAR_TBL;
   f1  
 ------
@@ -130,3 +132,222 @@ SELECT * FROM pg_input_error_info('abcde', 'varchar(4)');
  value too long for type character varying(4) |        |      | 22001
 (1 row)
 
+-- report the destination column for planning-time and runtime errors
+CREATE TEMP TABLE varchar_target (id int, username varchar(10), email varchar(10));
+INSERT INTO varchar_target (email, username) VALUES ('123456789012', 'ok');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "email" of relation "varchar_target"
+INSERT INTO varchar_target (username) VALUES ('ok'), ('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+CREATE TEMP TABLE varchar_source (v text);
+INSERT INTO varchar_source VALUES ('123456789012');
+INSERT INTO varchar_target VALUES (1, 'ok', 'ok');
+UPDATE varchar_target SET username = (SELECT v FROM varchar_source);
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+-- keep the destination in generic plans and use the current column name
+SET plan_cache_mode = force_generic_plan;
+PREPARE varchar_insert(text) AS
+  INSERT INTO varchar_target VALUES (2, $1, NULL);
+EXECUTE varchar_insert('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+ALTER TABLE varchar_target RENAME COLUMN username TO "User Name";
+EXECUTE varchar_insert('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "User Name" of relation "varchar_target"
+DEALLOCATE varchar_insert;
+RESET plan_cache_mode;
+-- keep the column identity in stored SQL bodies across renames
+CREATE FUNCTION pg_temp.varchar_insert_fn(text) RETURNS void
+LANGUAGE SQL BEGIN ATOMIC
+  INSERT INTO varchar_target VALUES (3, $1, NULL);
+END;
+ALTER TABLE varchar_target RENAME COLUMN "User Name" TO username;
+SELECT pg_temp.varchar_insert_fn('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+SQL function "varchar_insert_fn" statement 1
+DROP FUNCTION pg_temp.varchar_insert_fn(text);
+-- runtime diagnostics and callback cleanup
+DO $$
+DECLARE
+  message text;
+  context text;
+  colname text;
+  tabname text;
+  schemaname text;
+BEGIN
+  BEGIN
+    INSERT INTO varchar_target (email) SELECT v FROM varchar_source;
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS message = MESSAGE_TEXT,
+                            context = PG_EXCEPTION_CONTEXT,
+                            colname = COLUMN_NAME,
+                            tabname = TABLE_NAME,
+                            schemaname = SCHEMA_NAME;
+    RAISE NOTICE 'generic error: %, destination context: %, object fields empty: %',
+      message = 'value too long for type character varying(10)',
+      position('column "email" of relation "varchar_target"' in context) = 1,
+      colname = '' AND tabname = '' AND schemaname = '';
+  END;
+
+  -- Context must not leak after an error, a NULL input, or a successful call.
+  -- Source argument errors must not acquire the destination context.
+  INSERT INTO varchar_target (username) SELECT NULLIF(v, v) FROM varchar_source;
+  BEGIN
+    INSERT INTO varchar_target (username, email)
+      VALUES ((SELECT 'ok'::text FROM varchar_source),
+              pg_catalog.varchar((SELECT v FROM varchar_source), 15, false));
+    RAISE EXCEPTION 'expected a source length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_target"' in context) <> 0 THEN
+      RAISE EXCEPTION 'unexpected assignment context: %', context;
+    END IF;
+  END;
+  BEGIN
+    PERFORM pg_catalog.varchar((SELECT v FROM varchar_source), 14, false);
+    RAISE EXCEPTION 'expected a length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_target"' in context) <> 0 THEN
+      RAISE EXCEPTION 'assignment context leaked: %', context;
+    END IF;
+  END;
+END;
+$$;
+NOTICE:  generic error: t, destination context: t, object fields empty: t
+-- collation and array subscript assignments
+INSERT INTO varchar_target (username) VALUES ('123456789012' COLLATE "C");
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_target"
+CREATE TEMP TABLE varchar_nested (a varchar(10)[]);
+INSERT INTO varchar_nested (a) VALUES (ARRAY['ok']);
+UPDATE varchar_nested SET a[1] = (SELECT v FROM varchar_source);
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "a" of relation "varchar_nested"
+-- composite literals and row coercions currently omit the column
+-- direct field assignments report the containing column
+CREATE TYPE varchar_composite AS (v varchar(10));
+CREATE TEMP TABLE varchar_composite_target (c varchar_composite);
+INSERT INTO varchar_composite_target VALUES ('(123456789012)');
+ERROR:  value too long for type character varying(10)
+LINE 1: INSERT INTO varchar_composite_target VALUES ('(123456789012)...
+                                                     ^
+INSERT INTO varchar_composite_target VALUES (ROW('123456789012'));
+ERROR:  value too long for type character varying(10)
+INSERT INTO varchar_composite_target (c.v) VALUES ('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "c" of relation "varchar_composite_target"
+DROP TABLE varchar_composite_target;
+DROP TYPE varchar_composite;
+-- errors in the source expression must not name the destination
+INSERT INTO varchar_target (username)
+  VALUES (pg_catalog.varchar('123456789012', 5, false));
+ERROR:  value too long for type character varying(1)
+-- XMLSERIALIZE coercions belong to the source expression
+-- skip in builds without XML support
+DO $$
+DECLARE
+  context text;
+BEGIN
+  BEGIN
+    PERFORM XMLPARSE(CONTENT '<a/>');
+  EXCEPTION WHEN feature_not_supported THEN
+    RETURN;
+  END;
+  CREATE TEMP TABLE varchar_unbounded (
+    v text DEFAULT XMLSERIALIZE(CONTENT '<a>long</a>' AS varchar(3)));
+  BEGIN
+    INSERT INTO varchar_unbounded
+      VALUES (XMLSERIALIZE(CONTENT '<a>long</a>' AS varchar(3)) COLLATE "C");
+    RAISE EXCEPTION 'expected a length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_unbounded"' in context) <> 0 THEN
+      RAISE EXCEPTION 'source error attributed to column %', context;
+    END IF;
+  END;
+  BEGIN
+    INSERT INTO varchar_unbounded DEFAULT VALUES;
+    RAISE EXCEPTION 'expected a length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_unbounded"' in context) <> 0 THEN
+      RAISE EXCEPTION 'default source error attributed to column %', context;
+    END IF;
+  END;
+  DROP TABLE varchar_unbounded;
+END;
+$$;
+-- column defaults and stored generated columns
+CREATE TEMP TABLE varchar_default (
+  username varchar(10) DEFAULT repeat('x', 11 + (random() * 0)::int));
+INSERT INTO varchar_default DEFAULT VALUES;
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_default"
+CREATE TEMP TABLE varchar_generated (
+  v text, username varchar(10) GENERATED ALWAYS AS (v) STORED);
+INSERT INTO varchar_generated (v) VALUES ('123456789012');
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "username" of relation "varchar_generated"
+-- domain defaults currently omit the destination column
+-- random() prevents constant folding; the default always produces 11 characters
+CREATE DOMAIN varchar_default_domain AS varchar(10)
+  DEFAULT repeat('x', 11 + (random() * 0)::int);
+CREATE TEMP TABLE varchar_domain_default (d varchar_default_domain);
+INSERT INTO varchar_domain_default DEFAULT VALUES;
+ERROR:  value too long for type character varying(10)
+DROP TABLE varchar_domain_default;
+DROP DOMAIN varchar_default_domain;
+-- copied defaults must refer to the new column
+CREATE TEMP TABLE varchar_default_copy (LIKE varchar_default INCLUDING DEFAULTS);
+DROP TABLE varchar_default;
+ALTER TABLE varchar_default_copy RENAME COLUMN username TO copied_name;
+INSERT INTO varchar_default_copy DEFAULT VALUES;
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "copied_name" of relation "varchar_default_copy"
+ALTER TABLE varchar_default_copy ALTER COLUMN copied_name TYPE varchar(12);
+ALTER TABLE varchar_default_copy ALTER COLUMN copied_name TYPE varchar(10);
+INSERT INTO varchar_default_copy DEFAULT VALUES;
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "copied_name" of relation "varchar_default_copy"
+-- diagnostic metadata must not affect equality of inherited defaults
+CREATE TEMP TABLE varchar_parent1 (v varchar(10) DEFAULT 'ok');
+CREATE TEMP TABLE varchar_parent2 (v varchar(10) DEFAULT 'ok');
+CREATE TEMP TABLE varchar_child () INHERITS (varchar_parent1, varchar_parent2);
+NOTICE:  merging multiple inherited definitions of column "v"
+INSERT INTO varchar_child DEFAULT VALUES;
+SELECT * FROM varchar_child;
+ v  
+----
+ ok
+(1 row)
+
+DROP TABLE varchar_child, varchar_parent1, varchar_parent2;
+-- report the destination for column type changes
+ALTER TABLE varchar_source ALTER COLUMN v TYPE varchar(10);
+ERROR:  value too long for type character varying(10)
+CONTEXT:  column "v" of relation "varchar_source"
+-- SQL coercion inlining must not discard destination context
+-- roll back the cast so it is never visible to other regression sessions
+BEGIN;
+CREATE FUNCTION pg_temp.varchar_sql_coercion(integer, integer)
+RETURNS varchar LANGUAGE SQL IMMUTABLE STRICT
+RETURN ($1 / ($2 - 9))::text::varchar;
+CREATE CAST (integer AS varchar)
+WITH FUNCTION pg_temp.varchar_sql_coercion(integer, integer) AS ASSIGNMENT;
+CREATE TEMP TABLE varchar_sql_source (n integer);
+INSERT INTO varchar_sql_source VALUES (1), (NULL);
+CREATE TEMP TABLE varchar_sql_target (v varchar(5));
+-- strict coercions must skip the function for a NULL argument
+INSERT INTO varchar_sql_target SELECT n FROM varchar_sql_source WHERE n IS NULL;
+INSERT INTO varchar_sql_target SELECT n FROM varchar_sql_source WHERE n = 1;
+ERROR:  division by zero
+CONTEXT:  SQL function "varchar_sql_coercion" statement 1
+column "v" of relation "varchar_sql_target"
+ROLLBACK;
+DROP TABLE varchar_source, varchar_target, varchar_nested,
+  varchar_default_copy, varchar_generated;
diff --git a/src/test/regress/sql/varchar.sql b/src/test/regress/sql/varchar.sql
index c8aaf93e8a8..09324d07b90 100644
--- a/src/test/regress/sql/varchar.sql
+++ b/src/test/regress/sql/varchar.sql
@@ -71,3 +71,193 @@ SELECT * FROM VARCHAR_TBL;
 SELECT pg_input_is_valid('abcd  ', 'varchar(4)');
 SELECT pg_input_is_valid('abcde', 'varchar(4)');
 SELECT * FROM pg_input_error_info('abcde', 'varchar(4)');
+
+-- report the destination column for planning-time and runtime errors
+CREATE TEMP TABLE varchar_target (id int, username varchar(10), email varchar(10));
+INSERT INTO varchar_target (email, username) VALUES ('123456789012', 'ok');
+INSERT INTO varchar_target (username) VALUES ('ok'), ('123456789012');
+
+CREATE TEMP TABLE varchar_source (v text);
+INSERT INTO varchar_source VALUES ('123456789012');
+INSERT INTO varchar_target VALUES (1, 'ok', 'ok');
+UPDATE varchar_target SET username = (SELECT v FROM varchar_source);
+
+-- keep the destination in generic plans and use the current column name
+SET plan_cache_mode = force_generic_plan;
+PREPARE varchar_insert(text) AS
+  INSERT INTO varchar_target VALUES (2, $1, NULL);
+EXECUTE varchar_insert('123456789012');
+ALTER TABLE varchar_target RENAME COLUMN username TO "User Name";
+EXECUTE varchar_insert('123456789012');
+DEALLOCATE varchar_insert;
+RESET plan_cache_mode;
+
+-- keep the column identity in stored SQL bodies across renames
+CREATE FUNCTION pg_temp.varchar_insert_fn(text) RETURNS void
+LANGUAGE SQL BEGIN ATOMIC
+  INSERT INTO varchar_target VALUES (3, $1, NULL);
+END;
+ALTER TABLE varchar_target RENAME COLUMN "User Name" TO username;
+SELECT pg_temp.varchar_insert_fn('123456789012');
+DROP FUNCTION pg_temp.varchar_insert_fn(text);
+
+-- runtime diagnostics and callback cleanup
+DO $$
+DECLARE
+  message text;
+  context text;
+  colname text;
+  tabname text;
+  schemaname text;
+BEGIN
+  BEGIN
+    INSERT INTO varchar_target (email) SELECT v FROM varchar_source;
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS message = MESSAGE_TEXT,
+                            context = PG_EXCEPTION_CONTEXT,
+                            colname = COLUMN_NAME,
+                            tabname = TABLE_NAME,
+                            schemaname = SCHEMA_NAME;
+    RAISE NOTICE 'generic error: %, destination context: %, object fields empty: %',
+      message = 'value too long for type character varying(10)',
+      position('column "email" of relation "varchar_target"' in context) = 1,
+      colname = '' AND tabname = '' AND schemaname = '';
+  END;
+
+  -- Context must not leak after an error, a NULL input, or a successful call.
+  -- Source argument errors must not acquire the destination context.
+  INSERT INTO varchar_target (username) SELECT NULLIF(v, v) FROM varchar_source;
+  BEGIN
+    INSERT INTO varchar_target (username, email)
+      VALUES ((SELECT 'ok'::text FROM varchar_source),
+              pg_catalog.varchar((SELECT v FROM varchar_source), 15, false));
+    RAISE EXCEPTION 'expected a source length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_target"' in context) <> 0 THEN
+      RAISE EXCEPTION 'unexpected assignment context: %', context;
+    END IF;
+  END;
+  BEGIN
+    PERFORM pg_catalog.varchar((SELECT v FROM varchar_source), 14, false);
+    RAISE EXCEPTION 'expected a length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_target"' in context) <> 0 THEN
+      RAISE EXCEPTION 'assignment context leaked: %', context;
+    END IF;
+  END;
+END;
+$$;
+
+-- collation and array subscript assignments
+INSERT INTO varchar_target (username) VALUES ('123456789012' COLLATE "C");
+CREATE TEMP TABLE varchar_nested (a varchar(10)[]);
+INSERT INTO varchar_nested (a) VALUES (ARRAY['ok']);
+UPDATE varchar_nested SET a[1] = (SELECT v FROM varchar_source);
+
+-- composite literals and row coercions currently omit the column
+-- direct field assignments report the containing column
+CREATE TYPE varchar_composite AS (v varchar(10));
+CREATE TEMP TABLE varchar_composite_target (c varchar_composite);
+INSERT INTO varchar_composite_target VALUES ('(123456789012)');
+INSERT INTO varchar_composite_target VALUES (ROW('123456789012'));
+INSERT INTO varchar_composite_target (c.v) VALUES ('123456789012');
+DROP TABLE varchar_composite_target;
+DROP TYPE varchar_composite;
+
+-- errors in the source expression must not name the destination
+INSERT INTO varchar_target (username)
+  VALUES (pg_catalog.varchar('123456789012', 5, false));
+
+-- XMLSERIALIZE coercions belong to the source expression
+-- skip in builds without XML support
+DO $$
+DECLARE
+  context text;
+BEGIN
+  BEGIN
+    PERFORM XMLPARSE(CONTENT '<a/>');
+  EXCEPTION WHEN feature_not_supported THEN
+    RETURN;
+  END;
+  CREATE TEMP TABLE varchar_unbounded (
+    v text DEFAULT XMLSERIALIZE(CONTENT '<a>long</a>' AS varchar(3)));
+  BEGIN
+    INSERT INTO varchar_unbounded
+      VALUES (XMLSERIALIZE(CONTENT '<a>long</a>' AS varchar(3)) COLLATE "C");
+    RAISE EXCEPTION 'expected a length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_unbounded"' in context) <> 0 THEN
+      RAISE EXCEPTION 'source error attributed to column %', context;
+    END IF;
+  END;
+  BEGIN
+    INSERT INTO varchar_unbounded DEFAULT VALUES;
+    RAISE EXCEPTION 'expected a length error';
+  EXCEPTION WHEN string_data_right_truncation THEN
+    GET STACKED DIAGNOSTICS context = PG_EXCEPTION_CONTEXT;
+    IF position('of relation "varchar_unbounded"' in context) <> 0 THEN
+      RAISE EXCEPTION 'default source error attributed to column %', context;
+    END IF;
+  END;
+  DROP TABLE varchar_unbounded;
+END;
+$$;
+
+-- column defaults and stored generated columns
+CREATE TEMP TABLE varchar_default (
+  username varchar(10) DEFAULT repeat('x', 11 + (random() * 0)::int));
+INSERT INTO varchar_default DEFAULT VALUES;
+CREATE TEMP TABLE varchar_generated (
+  v text, username varchar(10) GENERATED ALWAYS AS (v) STORED);
+INSERT INTO varchar_generated (v) VALUES ('123456789012');
+
+-- domain defaults currently omit the destination column
+-- random() prevents constant folding; the default always produces 11 characters
+CREATE DOMAIN varchar_default_domain AS varchar(10)
+  DEFAULT repeat('x', 11 + (random() * 0)::int);
+CREATE TEMP TABLE varchar_domain_default (d varchar_default_domain);
+INSERT INTO varchar_domain_default DEFAULT VALUES;
+DROP TABLE varchar_domain_default;
+DROP DOMAIN varchar_default_domain;
+
+-- copied defaults must refer to the new column
+CREATE TEMP TABLE varchar_default_copy (LIKE varchar_default INCLUDING DEFAULTS);
+DROP TABLE varchar_default;
+ALTER TABLE varchar_default_copy RENAME COLUMN username TO copied_name;
+INSERT INTO varchar_default_copy DEFAULT VALUES;
+ALTER TABLE varchar_default_copy ALTER COLUMN copied_name TYPE varchar(12);
+ALTER TABLE varchar_default_copy ALTER COLUMN copied_name TYPE varchar(10);
+INSERT INTO varchar_default_copy DEFAULT VALUES;
+
+-- diagnostic metadata must not affect equality of inherited defaults
+CREATE TEMP TABLE varchar_parent1 (v varchar(10) DEFAULT 'ok');
+CREATE TEMP TABLE varchar_parent2 (v varchar(10) DEFAULT 'ok');
+CREATE TEMP TABLE varchar_child () INHERITS (varchar_parent1, varchar_parent2);
+INSERT INTO varchar_child DEFAULT VALUES;
+SELECT * FROM varchar_child;
+DROP TABLE varchar_child, varchar_parent1, varchar_parent2;
+
+-- report the destination for column type changes
+ALTER TABLE varchar_source ALTER COLUMN v TYPE varchar(10);
+
+-- SQL coercion inlining must not discard destination context
+-- roll back the cast so it is never visible to other regression sessions
+BEGIN;
+CREATE FUNCTION pg_temp.varchar_sql_coercion(integer, integer)
+RETURNS varchar LANGUAGE SQL IMMUTABLE STRICT
+RETURN ($1 / ($2 - 9))::text::varchar;
+CREATE CAST (integer AS varchar)
+WITH FUNCTION pg_temp.varchar_sql_coercion(integer, integer) AS ASSIGNMENT;
+CREATE TEMP TABLE varchar_sql_source (n integer);
+INSERT INTO varchar_sql_source VALUES (1), (NULL);
+CREATE TEMP TABLE varchar_sql_target (v varchar(5));
+-- strict coercions must skip the function for a NULL argument
+INSERT INTO varchar_sql_target SELECT n FROM varchar_sql_source WHERE n IS NULL;
+INSERT INTO varchar_sql_target SELECT n FROM varchar_sql_source WHERE n = 1;
+ROLLBACK;
+
+DROP TABLE varchar_source, varchar_target, varchar_nested,
+  varchar_default_copy, varchar_generated;
-- 
2.53.0

