Subject: implement CAST(expr AS type FORMAT 'template')

From: jian he <[email-masked]>
Date: Sun, 27 Jul 2025 23:43:48 +0800

hi.

while working on CAST(... DEFAULT ON ERROR), I came across link[1].  I don't
have access to the SQL standard, but based on the information in link[1], for
CAST(val AS type FORMAT 'template'), I make the <cast template> as an A_Const
node in gram.y.

so the attached patch is to implement
     CAST <left paren>
         <cast operand> AS <cast target>
         [ FORMAT <cast template> ]
         <right paren>

The implementation is pretty straightforward.
CAST(val AS type FORMAT 'template')
internally, it will be transformed into a FuncExpr node whose funcid
corresponds to
function name as one of (to_number, to_date, to_timestamp, to_char).
template as a Const node will make life easier.

select proname, prosrc, proallargtypes, proargtypes,
prorettype::regtype, proargnames
from pg_proc
where proname in ('to_number', 'to_date',  'to_timestamp', 'to_char');

based on the query results, only a limited set of type casts are supported with
formatted casts.  so error out early if the source or target type doesn't meet
these conditions.  for example, if the source or target is a composite, array,
or polymorphic type.

demo:
select cast('2018-13-12' as date format 'YYYY-MM-DD'); --error
select cast('2018-13-12' as date format 'YYYY-DD-MM'); --no error
select to_char(cast('2018-13-12' as date format 'YYYY-DD-MM'), 'YYYY-Mon-DD');
returns
2018-Dec-13

[1]: https://wiki.postgresql.org/wiki/PostgreSQL_vs_SQL_Standard#Major_features_simply_not_implemented_yet
From 8ddb3727f6292d47cdd42e657499fbaabf77f55f Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Sun, 27 Jul 2025 23:39:25 +0800
Subject: [PATCH v1 1/1] CAST(val AS type FORMAT 'template')

context: https://wiki.postgresql.org/wiki/PostgreSQL_vs_SQL_Standard#Major_features_simply_not_implemented_yet
---
 src/backend/nodes/nodeFuncs.c          |   2 +
 src/backend/parser/gram.y              |  26 +++
 src/backend/parser/parse_coerce.c      | 306 +++++++++++++++++++++++++
 src/backend/parser/parse_expr.c        |  31 ++-
 src/backend/parser/parse_utilcmd.c     |   1 +
 src/backend/utils/adt/ruleutils.c      |  71 ++++++
 src/include/nodes/parsenodes.h         |   1 +
 src/include/parser/parse_coerce.h      |   8 +
 src/test/regress/expected/horology.out |  94 ++++++++
 src/test/regress/expected/misc.out     | 131 +++++++++++
 src/test/regress/expected/numeric.out  |  49 +++-
 src/test/regress/sql/horology.sql      |  26 +++
 src/test/regress/sql/misc.sql          |  36 +++
 src/test/regress/sql/numeric.sql       |  11 +-
 14 files changed, 783 insertions(+), 10 deletions(-)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 7bc823507f1..91560bd1844 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4464,6 +4464,8 @@ raw_expression_tree_walker_impl(Node *node,
 
 				if (WALK(tc->arg))
 					return true;
+				if (WALK(tc->format))
+					return true;
 				if (WALK(tc->typeName))
 					return true;
 			}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 73345bb3c70..42962be3845 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -156,6 +156,8 @@ static RawStmt *makeRawStmt(Node *stmt, int stmt_location);
 static void updateRawStmtEnd(RawStmt *rs, int end_location);
 static Node *makeColumnRef(char *colname, List *indirection,
 						   int location, core_yyscan_t yyscanner);
+static Node *makeFormattedTypeCast(Node *arg, Node *format,
+								   TypeName *typename, int location);
 static Node *makeTypeCast(Node *arg, TypeName *typename, int location);
 static Node *makeStringConstCast(char *str, int location, TypeName *typename);
 static Node *makeIntConst(int val, int location);
@@ -15945,6 +15947,17 @@ func_expr_common_subexpr:
 				}
 			| CAST '(' a_expr AS Typename ')'
 				{ $$ = makeTypeCast($3, $5, @1); }
+			| CAST '(' a_expr AS Typename FORMAT a_expr ')'
+				{
+					$$ = makeFormattedTypeCast($3, $7, $5, @1);
+					if (!IsA($7, A_Const) ||
+						castNode(A_Const, $7)->val.node.type != T_String)
+						ereport(ERROR,
+								errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+								errmsg("only string constants are supported in CAST FORMAT template specification"),
+								parser_errposition(@7));
+				}
+
 			| EXTRACT '(' extract_list ')'
 				{
 					$$ = (Node *) makeFuncCall(SystemFuncName("extract"),
@@ -18832,12 +18845,25 @@ makeColumnRef(char *colname, List *indirection,
 	return (Node *) c;
 }
 
+static Node *
+makeFormattedTypeCast(Node *arg, Node *format, TypeName *typename, int location)
+{
+	TypeCast   *n = makeNode(TypeCast);
+
+	n->arg = arg;
+	n->format = format;
+	n->typeName = typename;
+	n->location = location;
+	return (Node *) n;
+}
+
 static Node *
 makeTypeCast(Node *arg, TypeName *typename, int location)
 {
 	TypeCast   *n = makeNode(TypeCast);
 
 	n->arg = arg;
+	n->format = NULL;
 	n->typeName = typename;
 	n->location = location;
 	return (Node *) n;
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index 0b5b81c7f27..979539bfc33 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -130,6 +130,66 @@ coerce_to_target_type(ParseState *pstate, Node *expr, Oid exprtype,
 	return result;
 }
 
+/*
+ * For CAST(A AS TYPE FORMAT 'template'),
+ * generate a FuncExpr representing the underlying function call.
+ * See coerce_to_target_type for normal type coerce.
+ */
+Node *
+coerce_to_target_type_fmt(ParseState *pstate, Node *expr, Node *format,
+						  Oid exprtype, Oid targettype, int32 targettypmod,
+						  CoercionContext ccontext, CoercionForm cformat,
+						  int location)
+{
+	Node	   *result;
+	Node	   *origexpr;
+
+	if (!can_coerce_type(1, &exprtype, &targettype, ccontext))
+		return NULL;
+
+	/*
+	 * If the input has a CollateExpr at the top, strip it off, perform the
+	 * coercion, and put a new one back on.  This is annoying since it
+	 * duplicates logic in coerce_type, but if we don't do this then it's too
+	 * hard to tell whether coerce_type actually changed anything, and we
+	 * *must* know that to avoid possibly calling hide_coercion_node on
+	 * something that wasn't generated by coerce_type.  Note that if there are
+	 * multiple stacked CollateExprs, we just discard all but the topmost.
+	 * Also, if the target type isn't collatable, we discard the CollateExpr.
+	 */
+	origexpr = expr;
+	while (expr && IsA(expr, CollateExpr))
+		expr = (Node *) ((CollateExpr *) expr)->arg;
+
+	result = coerce_type_fmt(pstate, expr, format, exprtype,
+							 targettype, targettypmod,
+							 ccontext, cformat, location);
+
+	/*
+	 * If the target is a fixed-length type, it may need a length coercion as
+	 * well as a type coercion.  If we find ourselves adding both, force the
+	 * inner coercion node to implicit display form.
+	 */
+	result = coerce_type_typmod(result,
+								targettype, targettypmod,
+								ccontext, cformat, location,
+								(result != expr && !IsA(result, Const)));
+
+	if (expr != origexpr && type_is_collatable(targettype))
+	{
+		/* Reinstall top CollateExpr */
+		CollateExpr *coll = (CollateExpr *) origexpr;
+		CollateExpr *newcoll = makeNode(CollateExpr);
+
+		newcoll->arg = (Expr *) result;
+		newcoll->collOid = coll->collOid;
+		newcoll->location = coll->location;
+		result = (Node *) newcoll;
+	}
+
+	return result;
+}
+
 
 /*
  * coerce_type()
@@ -546,6 +606,252 @@ coerce_type(ParseState *pstate, Node *node,
 }

+static Oid
+get_fmt_function(Oid targetTypeId)
+{
+	Oid			funcId = InvalidOid;
+
+	switch (targetTypeId)
+	{
+		case INT4OID:
+			funcId = fmgr_internal_function("int4_to_char");
+			break;
+		case INT8OID:
+			funcId = fmgr_internal_function("int8_to_char");
+			break;
+		case NUMERICOID:
+			funcId = fmgr_internal_function("numeric_to_char");
+			break;
+		case FLOAT4OID:
+			funcId = fmgr_internal_function("float4_to_char");
+			break;
+		case FLOAT8OID:
+			funcId = fmgr_internal_function("float8_to_char");
+			break;
+		case TIMESTAMPOID:
+			funcId = fmgr_internal_function("timestamp_to_char");
+			break;
+		case TIMESTAMPTZOID:
+			funcId = fmgr_internal_function("timestamptz_to_char");
+			break;
+		case INTERVALOID:
+			funcId = fmgr_internal_function("interval_to_char");
+			break;
+		default:
+			elog(ERROR, "unrecognized type: %d", (int) targetTypeId);
+			break;
+	}
+	return funcId;
+}
+Node *
+coerce_type_fmt(ParseState *pstate, Node *node, Node *format,
+				Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
+				CoercionContext ccontext, CoercionForm cformat, int location)
+{
+	Node	   *result;
+	Const	   *newcon = NULL;
+	Const	   *fmtcon = NULL;
+	Oid			funcId;
+	Oid			baseTypeId;
+	int32		baseTypeMod;
+	Oid			inputBaseTypeId;
+	char		t_typcategory;
+	char		s_typcategory;
+	FuncExpr   *fexpr;
+	Type		textType;
+	List	   *args;
+
+	baseTypeMod = targetTypeMod;
+	baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
+	inputBaseTypeId = getBaseType(inputTypeId);
+
+	s_typcategory = TypeCategory(inputBaseTypeId);
+	t_typcategory = TypeCategory(baseTypeId);
+
+	if (targetTypeId == inputTypeId ||
+		node == NULL)
+	{
+		/* no conversion needed */
+		return node;
+	}
+
+	if (baseTypeId != NUMERICOID &&
+		baseTypeId != TIMESTAMPTZOID &&
+		baseTypeId != DATEOID &&
+		t_typcategory != TYPCATEGORY_STRING)
+	{
+		ereport(ERROR,
+				errcode(ERRCODE_CANNOT_COERCE),
+				errmsg("cannot cast type %s to %s",
+						format_type_be(inputTypeId),
+						format_type_be(targetTypeId)),
+				errhint("Formatted type cast target type can only be timestamptz, text, numeric or date");
+				parser_coercion_errposition(pstate, location, node));
+	}
+
+	if (inputBaseTypeId != INT4OID &&
+		inputBaseTypeId != INT8OID &&
+		inputBaseTypeId != NUMERICOID &&
+		inputBaseTypeId != FLOAT4OID &&
+		inputBaseTypeId != FLOAT8OID &&
+		inputBaseTypeId != TIMESTAMPOID &&
+		inputBaseTypeId != TIMESTAMPTZOID &&
+		inputBaseTypeId != INTERVALOID &&
+		inputBaseTypeId != UNKNOWNOID &&
+		s_typcategory != TYPCATEGORY_STRING)
+	{
+		ereport(ERROR,
+				errcode(ERRCODE_CANNOT_COERCE),
+				errmsg("cannot cast type %s to %s",
+						format_type_be(inputTypeId),
+						format_type_be(targetTypeId)),
+				errhint("Formatted type cast source type must be catgeory of numeric, string, datetime, or timespan");
+				parser_coercion_errposition(pstate, location, node));
+	}
+
+
+	if (baseTypeId == NUMERICOID)
+		funcId = fmgr_internal_function("numeric_to_number");
+	else if (baseTypeId == TIMESTAMPTZOID)
+		funcId = fmgr_internal_function("to_timestamp");
+	else if (baseTypeId == DATEOID)
+		funcId = fmgr_internal_function("to_date");
+	else
+		funcId =  get_fmt_function(inputBaseTypeId);	/* to_char variant */
+
+	Assert(OidIsValid(funcId));
+
+	textType = typeidType(TEXTOID);
+	if (inputTypeId == UNKNOWNOID && IsA(node, Const))
+	{
+		/*
+		 * We assume here that UNKNOWN's internal representation is the same as
+		 * CSTRING.
+		 */
+		Const	   *con = (Const *) node;
+
+		newcon = makeNode(Const);
+		newcon->consttype = TEXTOID;
+		newcon->consttypmod = -1;
+		newcon->constcollid = typeTypeCollation(textType);
+		newcon->constlen = typeLen(textType);
+		newcon->constbyval = typeByVal(textType);
+		newcon->constisnull = con->constisnull;
+		newcon->location = exprLocation(node);
+
+		if (con->constisnull)
+			newcon->constvalue = (Datum) 0;
+		else
+		{
+			newcon->constvalue = stringTypeDatum(textType,
+												 DatumGetCString(con->constvalue),
+												 -1);
+			/*
+			 * If it's a varlena value, force it to be in non-expanded (non-toasted)
+			 * format; this avoids any possible dependency on external values and
+			 * improves consistency of representation.
+			*/
+			newcon->constvalue =
+				PointerGetDatum(PG_DETOAST_DATUM(newcon->constvalue));
+		}
+	}
+
+	Assert(IsA(format, Const));
+	if (exprType(format) == UNKNOWNOID)
+	{
+		Const	   *con = (Const *) format;
+		fmtcon = makeNode(Const);
+		fmtcon->consttype = TEXTOID;
+		fmtcon->consttypmod = -1;
+		fmtcon->constcollid = typeTypeCollation(textType);
+		fmtcon->constlen = typeLen(textType);
+		fmtcon->constbyval = typeByVal(textType);
+		fmtcon->constisnull = con->constisnull;
+		fmtcon->location = exprLocation(format);
+
+		/* format string can not be null */
+		Assert(!con->constisnull);
+		fmtcon->constvalue = stringTypeDatum(textType,
+											 DatumGetCString(con->constvalue),
+											 -1);
+		fmtcon->constvalue =
+			PointerGetDatum(PG_DETOAST_DATUM(fmtcon->constvalue));
+	}
+	else
+	{
+		Assert(exprType(format) == TEXTOID);
+		fmtcon = (Const *) copyObject(format);
+	}
+
+	if (IsA(node, Param) &&
+		pstate != NULL && pstate->p_coerce_param_hook != NULL)
+	{
+		/*
+		 * Allow the CoerceParamHook to decide what happens.  It can return a
+		 * transformed node (very possibly the same Param node), or return
+		 * NULL to indicate we should proceed with normal coercion.
+		 */
+		result = pstate->p_coerce_param_hook(pstate,
+											 (Param *) node,
+											 targetTypeId,
+											 targetTypeMod,
+											 location);
+		if (result)
+			return result;
+	}
+
+	if (IsA(node, CollateExpr))
+	{
+		/*
+		 * If we have a COLLATE clause, we have to push the coercion
+		 * underneath the COLLATE; or discard the COLLATE if the target type
+		 * isn't collatable.  This is really ugly, but there is little choice
+		 * because the above hacks on Consts and Params wouldn't happen
+		 * otherwise.  This kluge has consequences in coerce_to_target_type.
+		 */
+		CollateExpr *coll = (CollateExpr *) node;
+
+		result = coerce_type_fmt(pstate, (Node *) coll->arg, format,
+								 inputTypeId, targetTypeId, targetTypeMod,
+								 ccontext, cformat, location);
+		if (type_is_collatable(targetTypeId))
+		{
+			CollateExpr *newcoll = makeNode(CollateExpr);
+
+			newcoll->arg = (Expr *) result;
+			newcoll->collOid = coll->collOid;
+			newcoll->location = coll->location;
+			result = (Node *) newcoll;
+		}
+		return result;
+	}
+
+	if(newcon != NULL)
+		args = list_make1(newcon);
+	else
+		args = list_make1(node);
+	args = lappend(args, fmtcon);
+
+	fexpr = makeFuncExpr(funcId, targetTypeId, args,
+						 InvalidOid, InvalidOid, cformat);
+	fexpr->location = location;
+	result = (Node *) fexpr;
+
+	/*
+	 * If domain, coerce to the domain type and relabel with domain type ID,
+	 * hiding the previous coercion node.
+	*/
+	if (targetTypeId != baseTypeId)
+		result = coerce_to_domain(result, baseTypeId, baseTypeMod,
+								  targetTypeId,
+								  ccontext, cformat, location,
+								  true);
+
+	ReleaseSysCache(textType);
+
+	return result;
+}
+
 /*
  * can_coerce_type()
  *		Can input_typeids be coerced to target_typeids?
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index d66276801c6..f9694ad48e5 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -2706,6 +2706,7 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
 	Node	   *result;
 	Node	   *arg = tc->arg;
 	Node	   *expr;
+	Node	   *format = NULL;
 	Oid			inputType;
 	Oid			targetType;
 	int32		targetTypmod;
@@ -2727,6 +2728,12 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
 		int32		targetBaseTypmod;
 		Oid			elementType;
 
+		if(tc->format)
+			ereport(ERROR,
+					errcode(ERRCODE_CANNOT_COERCE),
+					errmsg("formmatted type cast does not apply to array type");
+					parser_coercion_errposition(pstate, exprLocation(arg), arg));
+
 		/*
 		 * If target is a domain over array, work with the base array type
 		 * here.  Below, we'll cast the array type to the domain.  In the
@@ -2754,6 +2761,13 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
 	if (inputType == InvalidOid)
 		return expr;			/* do nothing if NULL input */
 
+	if(tc->format)
+	{
+		format = transformExprRecurse(pstate, tc->format);
+		Assert(IsA(format, Const));
+		Assert(!((Const *) format)->constisnull);
+	}
+
 	/*
 	 * Location of the coercion is preferentially the location of the :: or
 	 * CAST symbol, but if there is none then use the location of the type
@@ -2763,11 +2777,18 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
 	if (location < 0)
 		location = tc->typeName->location;
 
-	result = coerce_to_target_type(pstate, expr, inputType,
-								   targetType, targetTypmod,
-								   COERCION_EXPLICIT,
-								   COERCE_EXPLICIT_CAST,
-								   location);
+	if (format != NULL)
+		result = coerce_to_target_type_fmt(pstate, expr, format, inputType,
+										   targetType, targetTypmod,
+										   COERCION_EXPLICIT,
+										   COERCE_EXPLICIT_CAST,
+										   location);
+	else
+		result = coerce_to_target_type(pstate, expr, inputType,
+									   targetType, targetTypmod,
+									   COERCION_EXPLICIT,
+									   COERCE_EXPLICIT_CAST,
+									   location);
 	if (result == NULL)
 		ereport(ERROR,
 				(errcode(ERRCODE_CANNOT_COERCE),
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index a414bfd6252..8b31f697fa7 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -682,6 +682,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 		castnode = makeNode(TypeCast);
 		castnode->typeName = SystemTypeName("regclass");
 		castnode->arg = (Node *) snamenode;
+		castnode->format = NULL;
 		castnode->location = -1;
 		funccallnode = makeFuncCall(SystemFuncName("nextval"),
 									list_make1(castnode),
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 98fd300c35a..4fc79385c7c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -474,6 +474,8 @@ static bool looks_like_function(Node *node);
 static void get_oper_expr(OpExpr *expr, deparse_context *context);
 static void get_func_expr(FuncExpr *expr, deparse_context *context,
 						  bool showimplicit);
+static bool get_fmt_coercion_expr(FuncExpr *expr, deparse_context *context,
+								  Oid resulttype, int32 resulttypmod);
 static void get_agg_expr(Aggref *aggref, deparse_context *context,
 						 Aggref *original_aggref);
 static void get_agg_expr_helper(Aggref *aggref, deparse_context *context,
@@ -10840,6 +10842,10 @@ get_func_expr(FuncExpr *expr, deparse_context *context,
 		/* Get the typmod if this is a length-coercion function */
 		(void) exprIsLengthCoercion((Node *) expr, &coercedTypmod);
 
+		if (get_fmt_coercion_expr(expr, context,
+								  rettype, coercedTypmod))
+			return;
+
 		get_coercion_expr(arg, context,
 						  rettype, coercedTypmod,
 						  (Node *) expr);
@@ -10896,6 +10902,71 @@ get_func_expr(FuncExpr *expr, deparse_context *context,
 	appendStringInfoChar(buf, ')');
 }
 
+/*
+ * get_fmt_coercion_expr
+ *
+ * Parse back expression: CAST (expr AS type FORMAT 'fmt')
+ */
+static bool
+get_fmt_coercion_expr(FuncExpr *expr, deparse_context *context,
+					  Oid resulttype, int32 resulttypmod)
+
+{
+	Node	   *arg;
+	Const	   *second_arg;
+	FuncExpr 	*func;
+	char		*funcname;
+	Oid			procnspid;
+	StringInfo	buf = context->buf;
+
+	func = expr;
+	if (func->funcformat != COERCE_EXPLICIT_CAST)
+		return false;
+
+	if (list_length(func->args) != 2)
+		return false;
+
+	arg = linitial(func->args);
+	second_arg = (Const *) lsecond(func->args);
+
+	if (!IsA(second_arg, Const) ||
+		second_arg->consttype != TEXTOID ||
+		second_arg->constisnull)
+		return false;
+
+	procnspid = get_func_namespace(func->funcid);
+	if (!IsCatalogNamespace(procnspid))
+		return false;
+
+	funcname = get_func_name(func->funcid);
+	if (strcmp(funcname, "to_char") && strcmp(funcname, "to_date") &&
+		strcmp(funcname, "to_number") && strcmp(funcname, "to_timestamp"))
+		return false;
+
+	appendStringInfoString(buf, "CAST(");
+
+	if (!PRETTY_PAREN(context))
+		appendStringInfoChar(buf, '(');
+	get_rule_expr_paren(arg, context, false, (Node *) func);
+	if (!PRETTY_PAREN(context))
+		appendStringInfoChar(buf, ')');
+
+	/*
+	 * Never emit resulttype(arg) functional notation. A pg_proc entry could
+	 * take precedence, and a resulttype in pg_temp would require schema
+	 * qualification that format_type_with_typemod() would usually omit. We've
+	 * standardized on arg::resulttype, but CAST(arg AS resulttype) notation
+	 * would work fine.
+	 */
+	appendStringInfo(buf, " AS %s FORMAT ",
+					 format_type_with_typemod(resulttype, resulttypmod));
+
+	get_const_expr((Const *) second_arg, context, -1);
+	appendStringInfoChar(buf, ')');
+
+	return true;
+}
+
 /*
  * get_agg_expr			- Parse back an Aggref node
  */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..b71c4135ae5 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -395,6 +395,7 @@ typedef struct TypeCast
 {
 	NodeTag		type;
 	Node	   *arg;			/* the expression being casted */
+	Node	   *format;			/* the cast format template Const*/
 	TypeName   *typeName;		/* the target type */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } TypeCast;
diff --git a/src/include/parser/parse_coerce.h b/src/include/parser/parse_coerce.h
index 8d775c72c59..282f559c4e1 100644
--- a/src/include/parser/parse_coerce.h
+++ b/src/include/parser/parse_coerce.h
@@ -43,11 +43,19 @@ extern Node *coerce_to_target_type(ParseState *pstate,
 								   CoercionContext ccontext,
 								   CoercionForm cformat,
 								   int location);
+extern Node *coerce_to_target_type_fmt(ParseState *pstate,
+									   Node *expr,Node *format,
+									   Oid exprtype, Oid targettype,
+									   int32 targettypmod, CoercionContext ccontext,
+									   CoercionForm cformat, int location);
 extern bool can_coerce_type(int nargs, const Oid *input_typeids, const Oid *target_typeids,
 							CoercionContext ccontext);
 extern Node *coerce_type(ParseState *pstate, Node *node,
 						 Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
 						 CoercionContext ccontext, CoercionForm cformat, int location);
+Node *coerce_type_fmt(ParseState *pstate, Node *node, Node *format,
+					  Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
+					  CoercionContext ccontext, CoercionForm cformat, int location);
 extern Node *coerce_to_domain(Node *arg, Oid baseTypeId, int32 baseTypeMod,
 							  Oid typeId,
 							  CoercionContext ccontext, CoercionForm cformat, int location,
diff --git a/src/test/regress/expected/horology.out b/src/test/regress/expected/horology.out
index 5ae93d8e8a5..c6b8b65b6dc 100644
--- a/src/test/regress/expected/horology.out
+++ b/src/test/regress/expected/horology.out
@@ -3110,6 +3110,13 @@ SELECT to_timestamp('15 "text between quote marks" 98 54 45',
  Thu Jan 01 15:54:45 1998 PST
 (1 row)
 
+SELECT cast('15 "text between quote marks" 98 54 45' as timestamptz format
+                    E'HH24 "\\"text between quote marks\\"" YY MI SS');
+         timestamptz          
+------------------------------
+ Thu Jan 01 15:54:45 1998 PST
+(1 row)
+
 SELECT to_timestamp('05121445482000', 'MMDDHH24MISSYYYY');
          to_timestamp         
 ------------------------------
@@ -3341,12 +3348,24 @@ SELECT to_timestamp('2011-12-18 11:38 MSK', 'YYYY-MM-DD HH12:MI TZ');  -- dyntz
  Sat Dec 17 23:38:00 2011 PST
 (1 row)
 
+SELECT cast('2011-12-18 11:38 MSK' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');
+         timestamptz          
+------------------------------
+ Sat Dec 17 23:38:00 2011 PST
+(1 row)
+
 SELECT to_timestamp('2011-12-18 00:00 LMT', 'YYYY-MM-DD HH24:MI TZ');  -- dyntz
          to_timestamp         
 ------------------------------
  Sat Dec 17 23:52:58 2011 PST
 (1 row)
 
+SELECT cast('2011-12-18 00:00 LMT' as timestamptz format 'YYYY-MM-DD HH24:MI TZ');  -- dyntz
+         timestamptz          
+------------------------------
+ Sat Dec 17 23:52:58 2011 PST
+(1 row)
+
 SELECT to_timestamp('2011-12-18 11:38ESTFOO24', 'YYYY-MM-DD HH12:MITZFOOSS');
          to_timestamp         
 ------------------------------
@@ -3380,9 +3399,15 @@ SELECT to_timestamp('2011-12-18 11:38 +01:30', 'YYYY-MM-DD HH12:MI OF');
 SELECT to_timestamp('2011-12-18 11:38 +xyz', 'YYYY-MM-DD HH12:MI OF');  -- error
 ERROR:  invalid value "xy" for "OF"
 DETAIL:  Value must be an integer.
+SELECT cast('2011-12-18 11:38 +xyz' as timestamptz format 'YYYY-MM-DD HH12:MI OF');  -- error
+ERROR:  invalid value "xy" for "OF"
+DETAIL:  Value must be an integer.
 SELECT to_timestamp('2011-12-18 11:38 +01:xyz', 'YYYY-MM-DD HH12:MI OF');  -- error
 ERROR:  invalid value "xy" for "OF"
 DETAIL:  Value must be an integer.
+SELECT cast('2011-12-18 11:38 +01:xyz' as timestamptz format 'YYYY-MM-DD HH12:MI OF');  -- error
+ERROR:  invalid value "xy" for "OF"
+DETAIL:  Value must be an integer.
 SELECT to_timestamp('2018-11-02 12:34:56.025', 'YYYY-MM-DD HH24:MI:SS.MS');
            to_timestamp           
 ----------------------------------
@@ -3466,6 +3491,27 @@ SELECT i, to_timestamp('2018-11-02 12:34:56.123456', 'YYYY-MM-DD HH24:MI:SS.FF'
  6 | Fri Nov 02 12:34:56.123456 2018 PDT
 (6 rows)
 
+SELECT cast('2018-11-02 12:34:56.123456' as timestamptz(1) format 'YYYY-MM-DD HH24:MI:SS.FF6')
+UNION ALL
+SELECT cast('2018-11-02 12:34:56.123456' as timestamptz(2) format 'YYYY-MM-DD HH24:MI:SS.FF6')
+UNION ALL
+SELECT cast('2018-11-02 12:34:56.123456' as timestamptz(3) format 'YYYY-MM-DD HH24:MI:SS.FF6')
+UNION ALL
+SELECT cast('2018-11-02 12:34:56.123456' as timestamptz(4) format 'YYYY-MM-DD HH24:MI:SS.FF6')
+UNION ALL
+SELECT cast('2018-11-02 12:34:56.123456' as timestamptz(5) format 'YYYY-MM-DD HH24:MI:SS.FF6')
+UNION ALL
+SELECT cast('2018-11-02 12:34:56.123456' as timestamptz(6) format 'YYYY-MM-DD HH24:MI:SS.FF6');
+             timestamptz             
+-------------------------------------
+ Fri Nov 02 12:34:56.1 2018 PDT
+ Fri Nov 02 12:34:56.12 2018 PDT
+ Fri Nov 02 12:34:56.123 2018 PDT
+ Fri Nov 02 12:34:56.1235 2018 PDT
+ Fri Nov 02 12:34:56.12346 2018 PDT
+ Fri Nov 02 12:34:56.123456 2018 PDT
+(6 rows)
+
 SELECT i, to_timestamp('2018-11-02 12:34:56.123456789', 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
 ERROR:  date/time field value out of range: "2018-11-02 12:34:56.123456789"
 SELECT i, to_timestamp('20181102123456123456', 'YYYYMMDDHH24MISSFF' || i) FROM generate_series(1, 6) i;
@@ -3485,18 +3531,36 @@ SELECT to_date('1 4 1902', 'Q MM YYYY');  -- Q is ignored
  04-01-1902
 (1 row)
 
+SELECT cast('1 4 1902' as date format 'Q MM YYYY');  -- Q is ignored
+    date    
+------------
+ 04-01-1902
+(1 row)
+
 SELECT to_date('3 4 21 01', 'W MM CC YY');
   to_date   
 ------------
  04-15-2001
 (1 row)
 
+SELECT cast('3 4 21 01' as date format 'W MM CC YY');
+    date    
+------------
+ 04-15-2001
+(1 row)
+
 SELECT to_date('2458872', 'J');
   to_date   
 ------------
  01-23-2020
 (1 row)
 
+SELECT cast('2458872' as date format 'J');
+    date    
+------------
+ 01-23-2020
+(1 row)
+
 --
 -- Check handling of BC dates
 --
@@ -3832,12 +3896,24 @@ SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
  2012-12-12 12:00:00 PST
 (1 row)
 
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD HH:MI:SS TZ');
+          text           
+-------------------------
+ 2012-12-12 12:00:00 PST
+(1 row)
+
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS tz');
          to_char         
 -------------------------
  2012-12-12 12:00:00 pst
 (1 row)
 
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD HH:MI:SS tz');
+          text           
+-------------------------
+ 2012-12-12 12:00:00 pst
+(1 row)
+
 --
 -- Check behavior with SQL-style fixed-GMT-offset time zone (cf bug #8572)
 --
@@ -3867,18 +3943,36 @@ SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
  2012-12-12 12:00:00 -01:30
 (1 row)
 
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD HH:MI:SS TZ');
+            text            
+----------------------------
+ 2012-12-12 12:00:00 -01:30
+(1 row)
+
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD SSSS');
      to_char      
 ------------------
  2012-12-12 43200
 (1 row)
 
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD SSSS');
+       text       
+------------------
+ 2012-12-12 43200
+(1 row)
+
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD SSSSS');
      to_char      
 ------------------
  2012-12-12 43200
 (1 row)
 
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD SSSSS');
+       text       
+------------------
+ 2012-12-12 43200
+(1 row)
+
 SET TIME ZONE '+2';
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
          to_char         
diff --git a/src/test/regress/expected/misc.out b/src/test/regress/expected/misc.out
index 6e816c57f1f..17a161deb94 100644
--- a/src/test/regress/expected/misc.out
+++ b/src/test/regress/expected/misc.out
@@ -396,3 +396,134 @@ SELECT *, (equipment(CAST((h.*) AS hobbies_r))).name FROM hobbies_r h;
 --
 -- rewrite rules
 --
+select cast('1' as text format 1); --error
+ERROR:  only string constants are supported in CAST FORMAT template specification
+LINE 1: select cast('1' as text format 1);
+                                       ^
+select cast('1' as text format '1'::text); --error
+ERROR:  only string constants are supported in CAST FORMAT template specification
+LINE 1: select cast('1' as text format '1'::text);
+                                       ^
+select cast(array[1] as text format 'YYYY'); --error
+ERROR:  formmatted type cast does not apply to array type
+LINE 1: select cast(array[1] as text format 'YYYY');
+                    ^
+--type check
+select cast('1' as timestamp format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type unknown to timestamp without time zone
+LINE 1: select cast('1' as timestamp format 'YYYY-MM-DD');
+               ^
+HINT:  Formatted type cast target type can only be timestamptz, text, numeric or date
+select cast('1' as timestamp[] format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type unknown to timestamp without time zone[]
+LINE 1: select cast('1' as timestamp[] format 'YYYY-MM-DD');
+               ^
+HINT:  Formatted type cast target type can only be timestamptz, text, numeric or date
+select cast('1' as bool format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type unknown to boolean
+LINE 1: select cast('1' as bool format 'YYYY-MM-DD');
+               ^
+HINT:  Formatted type cast target type can only be timestamptz, text, numeric or date
+select cast('1' as json format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type unknown to json
+LINE 1: select cast('1' as json format 'YYYY-MM-DD');
+               ^
+HINT:  Formatted type cast target type can only be timestamptz, text, numeric or date
+select cast('1'::json as text format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type json to text
+LINE 1: select cast('1'::json as text format 'YYYY-MM-DD');
+               ^
+HINT:  Formatted type cast source type must be catgeory of numeric, string, datetime, or timespan
+--domain check
+create domain d1 as date check (value <> '0001-01-01');
+select cast('1' as d1 format 'YYYY-MM-DD'); --error
+ERROR:  value for domain d1 violates check constraint "d1_check"
+select cast('1' as d1 format 'MM-DD'); --ok
+      d1       
+---------------
+ 01-01-0001 BC
+(1 row)
+
+select cast('1' as date format 'YYYY-MM-DD');
+    date    
+------------
+ 01-01-0001
+(1 row)
+
+select cast('1' as date format 'YYYY-MM-DD') = to_date('1', 'YYYY-MM-DD') as expect_true;
+ expect_true 
+-------------
+ t
+(1 row)
+
+select cast('2012-13-12' as date format 'YYYY-MM-DD'); --ok
+ERROR:  date/time field value out of range: "2012-13-12"
+create table tcast(a text);
+create index s1 on tcast(cast(a as date format 'YYYY-MM-DD')); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+create index s1 on tcast(to_date(a, 'YYYY-MM-DD')); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+create view tcast_v1 as select cast(a as date format 'YYYY-MM-DD') from tcast;
+select pg_get_viewdef('tcast_v1', false);
+                   pg_get_viewdef                   
+----------------------------------------------------
+  SELECT CAST((a) AS date FORMAT 'YYYY-MM-DD') AS a+
+    FROM tcast;
+(1 row)
+
+select pg_get_viewdef('tcast_v1', true);
+                  pg_get_viewdef                  
+--------------------------------------------------
+  SELECT CAST(a AS date FORMAT 'YYYY-MM-DD') AS a+
+    FROM tcast;
+(1 row)
+
+select cast('2012-13-12' as date format 'YYYY-DD-MM') is not null as expect_true;
+ expect_true 
+-------------
+ t
+(1 row)
+
+--null value check
+select cast(NULL as date format 'YYYY-MM-DD');
+ date 
+------
+ 
+(1 row)
+
+select cast(NULL as numeric format 'YYYY-MM-DD');
+ numeric 
+---------
+        
+(1 row)
+
+select cast(NULL as timestamptz format 'YYYY-MM-DD');
+ timestamptz 
+-------------
+ 
+(1 row)
+
+select cast(NULL::interval AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
+select cast(NULL::timestamp AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
+select cast(NULL::timestamptz AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
+select cast(NULL::numeric AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
diff --git a/src/test/regress/expected/numeric.out b/src/test/regress/expected/numeric.out
index c58e232a263..b48fe4f3037 100644
--- a/src/test/regress/expected/numeric.out
+++ b/src/test/regress/expected/numeric.out
@@ -2270,6 +2270,12 @@ SELECT to_number('-34,338,492.654,878', '99G999G999D999G999');
  -34338492.654878
 (1 row)
 
+SELECT cast('-34,338,492.654,878' as numeric format '99G999G999D999G999');
+     numeric      
+------------------
+ -34338492.654878
+(1 row)
+
 SELECT to_number('<564646.654564>', '999999.999999PR');
    to_number    
 ----------------
@@ -2300,6 +2306,12 @@ SELECT to_number('5 4 4 4 4 8 . 7 8', '9 9 9 9 9 9 . 9 9');
  544448.78
 (1 row)
 
+SELECT cast('5 4 4 4 4 8 . 7 8' as numeric format'9 9 9 9 9 9 . 9 9');
+  numeric  
+-----------
+ 544448.78
+(1 row)
+
 SELECT to_number('.01', 'FM9.99');
  to_number 
 -----------
@@ -2372,6 +2384,12 @@ SELECT to_number('$1,234.56','L99,999.99');
    1234.56
 (1 row)
 
+SELECT cast('$1,234.56' as numeric format 'L99,999.99');
+ numeric 
+---------
+ 1234.56
+(1 row)
+
 SELECT to_number('1234.56','L99,999.99');
  to_number 
 -----------
@@ -2390,21 +2408,34 @@ SELECT to_number('42nd', '99th');
         42
 (1 row)
 
+SELECT cast('42nd' as numeric format '99th');
+ numeric 
+---------
+      42
+(1 row)
+
 SELECT to_number('123456', '99999V99');
         to_number        
 -------------------------
  1234.560000000000000000
 (1 row)
 
+SELECT cast('123456' as numeric format '99999V99');
+         numeric         
+-------------------------
+ 1234.560000000000000000
+(1 row)
+
 -- Test for correct conversion between numbers and Roman numerals
 WITH rows AS
   (SELECT i, to_char(i, 'RN') AS roman FROM generate_series(1, 3999) AS i)
 SELECT
-  bool_and(to_number(roman, 'RN') = i) as valid
+  bool_and(to_number(roman, 'RN') = i) as valid,
+  bool_and(cast(roman as numeric format 'RN') = i) as valid
 FROM rows;
- valid 
--------
- t
+ valid | valid 
+-------+-------
+ t     | t
 (1 row)
 
 -- Some additional tests for RN input
@@ -2414,6 +2445,12 @@ SELECT to_number('CvIiI', 'rn');
        108
 (1 row)
 
+SELECT cast('CvIiI' as numeric format 'rn');
+ numeric 
+---------
+     108
+(1 row)
+
 SELECT to_number('MMXX  ', 'RN');
  to_number 
 -----------
@@ -2441,8 +2478,12 @@ SELECT to_number('M CC', 'RN');
 -- error cases
 SELECT to_number('viv', 'RN');
 ERROR:  invalid Roman numeral
+SELECT cast('viv' as numeric format 'RN');
+ERROR:  invalid Roman numeral
 SELECT to_number('DCCCD', 'RN');
 ERROR:  invalid Roman numeral
+SELECT cast('DCCCD' as numeric format 'RN');
+ERROR:  invalid Roman numeral
 SELECT to_number('XIXL', 'RN');
 ERROR:  invalid Roman numeral
 SELECT to_number('MCCM', 'RN');
diff --git a/src/test/regress/sql/horology.sql b/src/test/regress/sql/horology.sql
index 8978249a5dc..9fb50016c79 100644
--- a/src/test/regress/sql/horology.sql
+++ b/src/test/regress/sql/horology.sql
@@ -476,6 +476,9 @@ SELECT to_timestamp('1,582nd VIII 21', 'Y,YYYth FMRM DD');
 SELECT to_timestamp('15 "text between quote marks" 98 54 45',
                     E'HH24 "\\"text between quote marks\\"" YY MI SS');
 
+SELECT cast('15 "text between quote marks" 98 54 45' as timestamptz format
+                    E'HH24 "\\"text between quote marks\\"" YY MI SS');
+
 SELECT to_timestamp('05121445482000', 'MMDDHH24MISSYYYY');
 
 SELECT to_timestamp('2000January09Sunday', 'YYYYFMMonthDDFMDay');
@@ -542,7 +545,9 @@ SELECT to_timestamp('2011-12-18 11:38 EST', 'YYYY-MM-DD HH12:MI TZ');
 SELECT to_timestamp('2011-12-18 11:38 -05', 'YYYY-MM-DD HH12:MI TZ');
 SELECT to_timestamp('2011-12-18 11:38 +01:30', 'YYYY-MM-DD HH12:MI TZ');
 SELECT to_timestamp('2011-12-18 11:38 MSK', 'YYYY-MM-DD HH12:MI TZ');  -- dyntz
+SELECT cast('2011-12-18 11:38 MSK' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');
 SELECT to_timestamp('2011-12-18 00:00 LMT', 'YYYY-MM-DD HH24:MI TZ');  -- dyntz
+SELECT cast('2011-12-18 00:00 LMT' as timestamptz format 'YYYY-MM-DD HH24:MI TZ');  -- dyntz
 SELECT to_timestamp('2011-12-18 11:38ESTFOO24', 'YYYY-MM-DD HH12:MITZFOOSS');
 SELECT to_timestamp('2011-12-18 11:38-05FOO24', 'YYYY-MM-DD HH12:MITZFOOSS');
 SELECT to_timestamp('2011-12-18 11:38 JUNK', 'YYYY-MM-DD HH12:MI TZ');  -- error
@@ -551,7 +556,9 @@ SELECT to_timestamp('2011-12-18 11:38 ...', 'YYYY-MM-DD HH12:MI TZ');  -- error
 SELECT to_timestamp('2011-12-18 11:38 -05', 'YYYY-MM-DD HH12:MI OF');
 SELECT to_timestamp('2011-12-18 11:38 +01:30', 'YYYY-MM-DD HH12:MI OF');
 SELECT to_timestamp('2011-12-18 11:38 +xyz', 'YYYY-MM-DD HH12:MI OF');  -- error
+SELECT cast('2011-12-18 11:38 +xyz' as timestamptz format 'YYYY-MM-DD HH12:MI OF');  -- error
 SELECT to_timestamp('2011-12-18 11:38 +01:xyz', 'YYYY-MM-DD HH12:MI OF');  -- error
+SELECT cast('2011-12-18 11:38 +01:xyz' as timestamptz format 'YYYY-MM-DD HH12:MI OF');  -- error
 
 SELECT to_timestamp('2018-11-02 12:34:56.025', 'YYYY-MM-DD HH24:MI:SS.MS');
 
@@ -562,12 +569,26 @@ SELECT i, to_timestamp('2018-11-02 12:34:56.123', 'YYYY-MM-DD HH24:MI:SS.FF' ||
 SELECT i, to_timestamp('2018-11-02 12:34:56.1234', 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
 SELECT i, to_timestamp('2018-11-02 12:34:56.12345', 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
 SELECT i, to_timestamp('2018-11-02 12:34:56.123456', 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
+SELECT cast('2018-11-02 12:34:56.123456' as timestamptz(1) format 'YYYY-MM-DD HH24:MI:SS.FF6')
+UNION ALL
+SELECT cast('2018-11-02 12:34:56.123456' as timestamptz(2) format 'YYYY-MM-DD HH24:MI:SS.FF6')
+UNION ALL
+SELECT cast('2018-11-02 12:34:56.123456' as timestamptz(3) format 'YYYY-MM-DD HH24:MI:SS.FF6')
+UNION ALL
+SELECT cast('2018-11-02 12:34:56.123456' as timestamptz(4) format 'YYYY-MM-DD HH24:MI:SS.FF6')
+UNION ALL
+SELECT cast('2018-11-02 12:34:56.123456' as timestamptz(5) format 'YYYY-MM-DD HH24:MI:SS.FF6')
+UNION ALL
+SELECT cast('2018-11-02 12:34:56.123456' as timestamptz(6) format 'YYYY-MM-DD HH24:MI:SS.FF6');
 SELECT i, to_timestamp('2018-11-02 12:34:56.123456789', 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
 SELECT i, to_timestamp('20181102123456123456', 'YYYYMMDDHH24MISSFF' || i) FROM generate_series(1, 6) i;
 
 SELECT to_date('1 4 1902', 'Q MM YYYY');  -- Q is ignored
+SELECT cast('1 4 1902' as date format 'Q MM YYYY');  -- Q is ignored
 SELECT to_date('3 4 21 01', 'W MM CC YY');
+SELECT cast('3 4 21 01' as date format 'W MM CC YY');
 SELECT to_date('2458872', 'J');
+SELECT cast('2458872' as date format 'J');
 
 --
 -- Check handling of BC dates
@@ -677,7 +698,9 @@ SELECT to_date('2147483647 01', 'CC YY');
 
 -- to_char's TZ format code produces zone abbrev if known
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD HH:MI:SS TZ');
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS tz');
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD HH:MI:SS tz');
 
 --
 -- Check behavior with SQL-style fixed-GMT-offset time zone (cf bug #8572)
@@ -692,8 +715,11 @@ SELECT '2012-12-12 12:00'::timestamptz;
 SELECT '2012-12-12 12:00 America/New_York'::timestamptz;
 
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD HH:MI:SS TZ');
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD SSSS');
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD SSSS');
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD SSSSS');
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD SSSSS');
 
 SET TIME ZONE '+2';
 
diff --git a/src/test/regress/sql/misc.sql b/src/test/regress/sql/misc.sql
index 165a2e175fb..8d9db74173c 100644
--- a/src/test/regress/sql/misc.sql
+++ b/src/test/regress/sql/misc.sql
@@ -273,3 +273,39 @@ SELECT *, (equipment(CAST((h.*) AS hobbies_r))).name FROM hobbies_r h;
 --
 -- rewrite rules
 --
+
+select cast('1' as text format 1); --error
+select cast('1' as text format '1'::text); --error
+select cast(array[1] as text format 'YYYY'); --error
+
+--type check
+select cast('1' as timestamp format 'YYYY-MM-DD'); --error
+select cast('1' as timestamp[] format 'YYYY-MM-DD'); --error
+select cast('1' as bool format 'YYYY-MM-DD'); --error
+select cast('1' as json format 'YYYY-MM-DD'); --error
+select cast('1'::json as text format 'YYYY-MM-DD'); --error
+
+--domain check
+create domain d1 as date check (value <> '0001-01-01');
+select cast('1' as d1 format 'YYYY-MM-DD'); --error
+select cast('1' as d1 format 'MM-DD'); --ok
+
+select cast('1' as date format 'YYYY-MM-DD');
+select cast('1' as date format 'YYYY-MM-DD') = to_date('1', 'YYYY-MM-DD') as expect_true;
+select cast('2012-13-12' as date format 'YYYY-MM-DD'); --ok
+create table tcast(a text);
+create index s1 on tcast(cast(a as date format 'YYYY-MM-DD')); --error
+create index s1 on tcast(to_date(a, 'YYYY-MM-DD')); --error
+create view tcast_v1 as select cast(a as date format 'YYYY-MM-DD') from tcast;
+select pg_get_viewdef('tcast_v1', false);
+select pg_get_viewdef('tcast_v1', true);
+select cast('2012-13-12' as date format 'YYYY-DD-MM') is not null as expect_true;
+
+--null value check
+select cast(NULL as date format 'YYYY-MM-DD');
+select cast(NULL as numeric format 'YYYY-MM-DD');
+select cast(NULL as timestamptz format 'YYYY-MM-DD');
+select cast(NULL::interval AS TEXT format 'YYYY-MM-DD');
+select cast(NULL::timestamp AS TEXT format 'YYYY-MM-DD');
+select cast(NULL::timestamptz AS TEXT format 'YYYY-MM-DD');
+select cast(NULL::numeric AS TEXT format 'YYYY-MM-DD');
diff --git a/src/test/regress/sql/numeric.sql b/src/test/regress/sql/numeric.sql
index 640c6d92f4c..1092317815b 100644
--- a/src/test/regress/sql/numeric.sql
+++ b/src/test/regress/sql/numeric.sql
@@ -1066,11 +1066,13 @@ SELECT to_char('100'::numeric, 'f"ool\\"999');
 SET lc_numeric = 'C';
 SELECT to_number('-34,338,492', '99G999G999');
 SELECT to_number('-34,338,492.654,878', '99G999G999D999G999');
+SELECT cast('-34,338,492.654,878' as numeric format '99G999G999D999G999');
 SELECT to_number('<564646.654564>', '999999.999999PR');
 SELECT to_number('0.00001-', '9.999999S');
 SELECT to_number('5.01-', 'FM9.999999S');
 SELECT to_number('5.01-', 'FM9.999999MI');
 SELECT to_number('5 4 4 4 4 8 . 7 8', '9 9 9 9 9 9 . 9 9');
+SELECT cast('5 4 4 4 4 8 . 7 8' as numeric format'9 9 9 9 9 9 . 9 9');
 SELECT to_number('.01', 'FM9.99');
 SELECT to_number('.0', '99999999.99999999');
 SELECT to_number('0', '99.99');
@@ -1083,27 +1085,34 @@ SELECT to_number('123456','999G999');
 SELECT to_number('$1234.56','L9,999.99');
 SELECT to_number('$1234.56','L99,999.99');
 SELECT to_number('$1,234.56','L99,999.99');
+SELECT cast('$1,234.56' as numeric format 'L99,999.99');
 SELECT to_number('1234.56','L99,999.99');
 SELECT to_number('1,234.56','L99,999.99');
 SELECT to_number('42nd', '99th');
+SELECT cast('42nd' as numeric format '99th');
 SELECT to_number('123456', '99999V99');
+SELECT cast('123456' as numeric format '99999V99');
 
 -- Test for correct conversion between numbers and Roman numerals
 WITH rows AS
   (SELECT i, to_char(i, 'RN') AS roman FROM generate_series(1, 3999) AS i)
 SELECT
-  bool_and(to_number(roman, 'RN') = i) as valid
+  bool_and(to_number(roman, 'RN') = i) as valid,
+  bool_and(cast(roman as numeric format 'RN') = i) as valid
 FROM rows;
 
 -- Some additional tests for RN input
 SELECT to_number('CvIiI', 'rn');
+SELECT cast('CvIiI' as numeric format 'rn');
 SELECT to_number('MMXX  ', 'RN');
 SELECT to_number('  XIV', '  RN');
 SELECT to_number('  XIV  ', '  RN');
 SELECT to_number('M CC', 'RN');
 -- error cases
 SELECT to_number('viv', 'RN');
+SELECT cast('viv' as numeric format 'RN');
 SELECT to_number('DCCCD', 'RN');
+SELECT cast('DCCCD' as numeric format 'RN');
 SELECT to_number('XIXL', 'RN');
 SELECT to_number('MCCM', 'RN');
 SELECT to_number('MMMM', 'RN');
-- 
2.34.1

---

From: Vik Fearing <[email-masked]>
Date: Sun, 27 Jul 2025 20:31:24 +0200

> CAST(val AS type FORMAT 'template'), I make the <cast template> as an A_Const

Why does it have to be an A_const?  Shouldn't any a_expr work there?

>           [ FORMAT <cast template> ]

This is correct syntax.  Thanks for working on it!

> template as a Const node will make life easier.

This doesn't seem very postgres-y to me.  Wouldn't it be better to add 
something like castformatfuncid to pg_cast?  That way any types that 
have that would just call that.  It would allow extensions to add 
formatted casting to their types, for example.

> these conditions.  for example, if the source or target is a composite, array,

The standard is strict on what types can be cast to another, but I see 
no reason not to be more generic.

-- 

Vik Fearing

---

From: jian he <[email-masked]>
Date: Mon, 28 Jul 2025 16:41:29 +0800

> Why does it have to be an A_const?  Shouldn't any a_expr work there?

you are right. a_expr should work.
the attached patch changed accordingly.

so now
select cast(NULL as date format NULL::date); ---error
select cast(NULL as date format lower('a')); --no error, returns NULL

> formatted casting to their types, for example.

select oid, castsource::regtype, casttarget::regtype,
castfunc::regproc, castcontext, castmethod
from pg_cast
where casttarget::regtype::text in ('text') or
castsource::regtype::text in ('text');

As you can see from the query output, cast from other type to text or
cast from text to other type is not in the pg_cast catalog entry.
there are in type input/output functions. it will be represented as a
CoerceViaIO node.
see function find_coercion_pathway (src/backend/parser/parse_coerce.c
line:3577).

adding these pg_cast entries seems tricky.
for example:
(assume castsource as numeric, casttarget as text)
will
(castsource as numeric, casttarget as text, castfunc as numeric_out,
castformatfunc as numeric_to_char)
ever work?
but numeric_out' result type is cstring.
so I tend to think adding castformatfunc to pg_cast will not work.
From bd4ae95df52319fd2bb50f653cc1ed49884e2ce7 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Mon, 28 Jul 2025 16:19:32 +0800
Subject: [PATCH v2 1/1] CAST(val AS type FORMAT 'template')

context: https://wiki.postgresql.org/wiki/PostgreSQL_vs_SQL_Standard#Major_features_simply_not_implemented_yet
discussion: https://postgr.es/m/CACJufxGqm7cYQ5C65Eoh1z-f+aMdhv9_7V=[email-masked]
---
 src/backend/nodes/nodeFuncs.c          |   2 +
 src/backend/parser/gram.y              |  17 ++
 src/backend/parser/parse_coerce.c      | 318 +++++++++++++++++++++++++
 src/backend/parser/parse_expr.c        |  27 ++-
 src/backend/parser/parse_utilcmd.c     |   1 +
 src/backend/utils/adt/ruleutils.c      |  71 ++++++
 src/include/nodes/parsenodes.h         |   1 +
 src/include/parser/parse_coerce.h      |   8 +
 src/test/regress/expected/horology.out |  94 ++++++++
 src/test/regress/expected/misc.out     | 166 +++++++++++++
 src/test/regress/expected/numeric.out  |  49 +++-
 src/test/regress/sql/horology.sql      |  26 ++
 src/test/regress/sql/misc.sql          |  47 ++++
 src/test/regress/sql/numeric.sql       |  11 +-
 14 files changed, 828 insertions(+), 10 deletions(-)

 				if (WALK(tc->arg))
 					return true;
+				if (WALK(tc->format))
+					return true;
 				if (WALK(tc->typeName))
 					return true;
 			}
index 73345bb3c70..8918adeae00 100644
@@ -15945,6 +15947,8 @@ func_expr_common_subexpr:
+				{ $$ = makeFormattedTypeCast($3, $7, $5, @1); }
@@ -18832,12 +18836,25 @@ makeColumnRef(char *colname, List *indirection,
 	return (Node *) c;
 }
 
+static Node *
 
 	n->arg = arg;
+	n->format = NULL;
 	n->typeName = typename;
 	n->location = location;
 	return (Node *) n;
index 0b5b81c7f27..ba171013ee7 100644
 
+/*
 
 /*
  * coerce_type()
@@ -546,6 +606,264 @@ coerce_type(ParseState *pstate, Node *node,
 }

+static Oid
+	Node	   *fmt;
+	Node	   *source = NULL;
+	if (IsA(format, Const) && exprType(format) == UNKNOWNOID)
+		fmt = (Node *) fmtcon;
+		if (TypeCategory(exprType(format)) != TYPCATEGORY_STRING)
+					errmsg("FORMAT template is not string type"),
+					parser_errposition(pstate, exprLocation(format)));
+		if (expression_returns_set(format))
+					errcode(ERRCODE_DATATYPE_MISMATCH),
+					errmsg("FORMAT template expression must not return a set"),
+					parser_errposition(pstate, exprLocation(format)));
+		fmt = format;
+		source = (Node *) newcon;
+		source = node;
+	args = list_make1(source);
+	args = lappend(args, fmt);
index d66276801c6..f4a3b0e1219 100644
 
+		if(tc->format)
+			ereport(ERROR,
@@ -2754,6 +2761,9 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
 
+	if(tc->format)	
@@ -2763,11 +2773,18 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)

 
+/*

 

 

 

 
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD SSSSS');
+       text       
+------------------
+ 2012-12-12 43200
+(1 row)
+
 SET TIME ZONE '+2';
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
          to_char         
diff --git a/src/test/regress/expected/misc.out b/src/test/regress/expected/misc.out
index 6e816c57f1f..673205e6aac 100644
--- a/src/test/regress/expected/misc.out
+++ b/src/test/regress/expected/misc.out
@@ -396,3 +396,169 @@ SELECT *, (equipment(CAST((h.*) AS hobbies_r))).name FROM hobbies_r h;
 --
 -- rewrite rules
 --
+select cast('1' as text format 1); --error
+ERROR:  FORMAT template is not string type
+LINE 1: select cast('1' as text format 1);
+                                       ^
+select cast('1' as text format '1'::text); --error
+ERROR:  unrecognized type: 705
+select cast(array[1] as text format 'YYYY'); --error
+ERROR:  formmatted type cast does not apply to array type
+LINE 1: select cast(array[1] as text format 'YYYY');
+                    ^
+--type check
+select cast('1' as timestamp format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type unknown to timestamp without time zone
+LINE 1: select cast('1' as timestamp format 'YYYY-MM-DD');
+               ^
+HINT:  Formatted type cast target type can only be timestamptz, text, numeric or date
+select cast('1' as timestamp[] format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type unknown to timestamp without time zone[]
+LINE 1: select cast('1' as timestamp[] format 'YYYY-MM-DD');
+               ^
+HINT:  Formatted type cast target type can only be timestamptz, text, numeric or date
+select cast('1' as bool format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type unknown to boolean
+LINE 1: select cast('1' as bool format 'YYYY-MM-DD');
+               ^
+HINT:  Formatted type cast target type can only be timestamptz, text, numeric or date
+select cast('1' as json format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type unknown to json
+LINE 1: select cast('1' as json format 'YYYY-MM-DD');
+               ^
+HINT:  Formatted type cast target type can only be timestamptz, text, numeric or date
+select cast('1'::json as text format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type json to text
+LINE 1: select cast('1'::json as text format 'YYYY-MM-DD');
+               ^
+HINT:  Formatted type cast source type must be catgeory of numeric, string, datetime, or timespan
+--domain check
+create domain d1 as date check (value <> '0001-01-01');
+select cast('1' as d1 format 'YYYY-MM-DD'); --error
+ERROR:  value for domain d1 violates check constraint "d1_check"
+select cast('1' as d1 format 'MM-DD'); --ok
+      d1       
+---------------
+ 01-01-0001 BC
+(1 row)
+
+select cast('1' as date); --error
+ERROR:  invalid input syntax for type date: "1"
+LINE 1: select cast('1' as date);
+                    ^
+select cast('1' as date format 'YYYY-MM-DD');
+    date    
+------------
+ 01-01-0001
+(1 row)
+
+select cast('1' as date format 'YYYY-MM-DD') = to_date('1', 'YYYY-MM-DD') as expect_true;
+ expect_true 
+-------------
+ t
+(1 row)
+
+select cast('2012-13-12' as date format 'YYYY-MM-DD'); --error
+ERROR:  date/time field value out of range: "2012-13-12"
+create table tcast(col1 text, col2 text, col3 date, col4 timestamptz);
+insert into tcast(col1, col2) values('2022-12-13', 'YYYY-MM-DD'), ('2022-12-01', 'YYYY-DD-MM');
+select cast(col1 as date format col2) from tcast;
+    col1    
+------------
+ 12-13-2022
+ 01-12-2022
+(2 rows)
+
+select cast(col1 as date format col3) from tcast; --error
+ERROR:  FORMAT template is not string type
+LINE 1: select cast(col1 as date format col3) from tcast;
+                                        ^
+select cast(col1 as date format col3::text) from tcast; --ok
+ col1 
+------
+ 
+ 
+(2 rows)
+
+CREATE FUNCTION volatile_const() RETURNS TEXT AS $$ BEGIN RETURN 'YYYY-MM-DD'; END; $$ LANGUAGE plpgsql VOLATILE;
+CREATE FUNCTION stable_const() RETURNS TEXT AS $$ BEGIN RETURN 'YYYY-MM-DD'; END; $$ LANGUAGE plpgsql STABLE;
+select cast(col1 as date format volatile_const()) from tcast;
+    col1    
+------------
+ 12-13-2022
+ 12-01-2022
+(2 rows)
+
+select cast(col1 as date format stable_const()) from tcast;
+    col1    
+------------
+ 12-13-2022
+ 12-01-2022
+(2 rows)
+
+create index s1 on tcast(cast(col1 as date format 'YYYY-MM-DD')); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+create view tcast_v1 as select cast(col1 as date format 'YYYY-MM-DD') from tcast;
+select pg_get_viewdef('tcast_v1', false);
+                      pg_get_viewdef                      
+----------------------------------------------------------
+  SELECT CAST((col1) AS date FORMAT 'YYYY-MM-DD') AS col1+
+    FROM tcast;
+(1 row)
+
+select pg_get_viewdef('tcast_v1', true);
+                     pg_get_viewdef                     
+--------------------------------------------------------
+  SELECT CAST(col1 AS date FORMAT 'YYYY-MM-DD') AS col1+
+    FROM tcast;
+(1 row)
+
+select cast('2012-13-12' as date format 'YYYY-DD-MM') is not null as expect_true;
+ expect_true 
+-------------
+ t
+(1 row)
+
+--null value check
+select cast(NULL as date format 'YYYY-MM-DD');
+ date 
+------
+ 
+(1 row)
+
+select cast(NULL as numeric format 'YYYY-MM-DD');
+ numeric 
+---------
+        
+(1 row)
+
+select cast(NULL as timestamptz format 'YYYY-MM-DD');
+ timestamptz 
+-------------
+ 
+(1 row)
+
+select cast(NULL::interval AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
+select cast(NULL::timestamp AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
+select cast(NULL::timestamptz AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
+select cast(NULL::numeric AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
diff --git a/src/test/regress/expected/numeric.out b/src/test/regress/expected/numeric.out
index c58e232a263..b48fe4f3037 100644
--- a/src/test/regress/expected/numeric.out
+++ b/src/test/regress/expected/numeric.out
@@ -2270,6 +2270,12 @@ SELECT to_number('-34,338,492.654,878', '99G999G999D999G999');
  -34338492.654878
 (1 row)

 

 

 

 

 

 SET TIME ZONE '+2';
 
diff --git a/src/test/regress/sql/misc.sql b/src/test/regress/sql/misc.sql
index 165a2e175fb..8d6bdea7654 100644
--- a/src/test/regress/sql/misc.sql
+++ b/src/test/regress/sql/misc.sql
@@ -273,3 +273,50 @@ SELECT *, (equipment(CAST((h.*) AS hobbies_r))).name FROM hobbies_r h;
 --
 -- rewrite rules
 --
+
+select cast('1' as text format 1); --error
+select cast('1' as text format '1'::text); --error
+select cast(array[1] as text format 'YYYY'); --error
+
+--type check
+select cast('1' as timestamp format 'YYYY-MM-DD'); --error
+select cast('1' as timestamp[] format 'YYYY-MM-DD'); --error
+select cast('1' as bool format 'YYYY-MM-DD'); --error
+select cast('1' as json format 'YYYY-MM-DD'); --error
+select cast('1'::json as text format 'YYYY-MM-DD'); --error
+
+--domain check
+create domain d1 as date check (value <> '0001-01-01');
+select cast('1' as d1 format 'YYYY-MM-DD'); --error
+select cast('1' as d1 format 'MM-DD'); --ok
+
+select cast('1' as date); --error
+select cast('1' as date format 'YYYY-MM-DD');
+select cast('1' as date format 'YYYY-MM-DD') = to_date('1', 'YYYY-MM-DD') as expect_true;
+select cast('2012-13-12' as date format 'YYYY-MM-DD'); --error
+
+create table tcast(col1 text, col2 text, col3 date, col4 timestamptz);
+insert into tcast(col1, col2) values('2022-12-13', 'YYYY-MM-DD'), ('2022-12-01', 'YYYY-DD-MM');
+select cast(col1 as date format col2) from tcast;
+select cast(col1 as date format col3) from tcast; --error
+select cast(col1 as date format col3::text) from tcast; --ok
+
+CREATE FUNCTION volatile_const() RETURNS TEXT AS $$ BEGIN RETURN 'YYYY-MM-DD'; END; $$ LANGUAGE plpgsql VOLATILE;
+CREATE FUNCTION stable_const() RETURNS TEXT AS $$ BEGIN RETURN 'YYYY-MM-DD'; END; $$ LANGUAGE plpgsql STABLE;
+select cast(col1 as date format volatile_const()) from tcast;
+select cast(col1 as date format stable_const()) from tcast;
+
+create index s1 on tcast(cast(col1 as date format 'YYYY-MM-DD')); --error
+create view tcast_v1 as select cast(col1 as date format 'YYYY-MM-DD') from tcast;
+select pg_get_viewdef('tcast_v1', false);
+select pg_get_viewdef('tcast_v1', true);
+select cast('2012-13-12' as date format 'YYYY-DD-MM') is not null as expect_true;
+
+--null value check
+select cast(NULL as date format 'YYYY-MM-DD');
+select cast(NULL as numeric format 'YYYY-MM-DD');
+select cast(NULL as timestamptz format 'YYYY-MM-DD');
+select cast(NULL::interval AS TEXT format 'YYYY-MM-DD');
+select cast(NULL::timestamp AS TEXT format 'YYYY-MM-DD');
+select cast(NULL::timestamptz AS TEXT format 'YYYY-MM-DD');
+select cast(NULL::numeric AS TEXT format 'YYYY-MM-DD');
diff --git a/src/test/regress/sql/numeric.sql b/src/test/regress/sql/numeric.sql
index 640c6d92f4c..1092317815b 100644
--- a/src/test/regress/sql/numeric.sql
+++ b/src/test/regress/sql/numeric.sql
@@ -1066,11 +1066,13 @@ SELECT to_char('100'::numeric, 'f"ool\\"999');
 SET lc_numeric = 'C';
 SELECT to_number('-34,338,492', '99G999G999');
 SELECT to_number('-34,338,492.654,878', '99G999G999D999G999');
+SELECT cast('-34,338,492.654,878' as numeric format '99G999G999D999G999');
 SELECT to_number('<564646.654564>', '999999.999999PR');
 SELECT to_number('0.00001-', '9.999999S');
 SELECT to_number('5.01-', 'FM9.999999S');
 SELECT to_number('5.01-', 'FM9.999999MI');
 SELECT to_number('5 4 4 4 4 8 . 7 8', '9 9 9 9 9 9 . 9 9');
+SELECT cast('5 4 4 4 4 8 . 7 8' as numeric format'9 9 9 9 9 9 . 9 9');
 SELECT to_number('.01', 'FM9.99');
 SELECT to_number('.0', '99999999.99999999');
 SELECT to_number('0', '99.99');
@@ -1083,27 +1085,34 @@ SELECT to_number('123456','999G999');
 SELECT to_number('$1234.56','L9,999.99');
 SELECT to_number('$1234.56','L99,999.99');
 SELECT to_number('$1,234.56','L99,999.99');
+SELECT cast('$1,234.56' as numeric format 'L99,999.99');
 SELECT to_number('1234.56','L99,999.99');
 SELECT to_number('1,234.56','L99,999.99');
 SELECT to_number('42nd', '99th');
+SELECT cast('42nd' as numeric format '99th');
 SELECT to_number('123456', '99999V99');
+SELECT cast('123456' as numeric format '99999V99');

---

From: Vik Fearing <[email-masked]>
Date: Mon, 28 Jul 2025 12:47:47 +0200

> see function find_coercion_pathway (src/backend/parser/parse_coerce.c

This is the same issue I came across when I tried to implement it 
several years ago.

> but numeric_out' result type is cstring.

I had been imagining another castcontext that would only specify the 
castfunc when the FORMAT claused is used, otherwise the current method 
of passing through IO would be used.

> so I tend to think adding castformatfunc to pg_cast will not work.

Perhaps not, but we need to find a way to make this generic so that 
custom types can define formatting rules for themselves.

-- 

Vik Fearing

---

From: jian he <[email-masked]>
Date: Tue, 29 Jul 2025 11:13:53 +0800

> custom types can define formatting rules for themselves.

We can introduce another column in pg_proc, proformat
hope it's not crazy as it is.

select proname, prosrc, proformat from pg_proc where proformat;
   proname    |       prosrc        | proformat
--------------+---------------------+-----------
 to_char      | timestamptz_to_char | t
 to_char      | numeric_to_char     | t
 to_char      | int4_to_char        | t
 to_char      | int8_to_char        | t
 to_char      | float4_to_char      | t
 to_char      | float8_to_char      | t
 to_number    | numeric_to_number   | t
 to_timestamp | to_timestamp        | t
 to_date      | to_date             | t
 to_char      | interval_to_char    | t
 to_char      | timestamp_to_char   | t

proformat is true means this function is a formatter function.
formatter function requirement:
* first argument or the return type must be TEXT.
* the second argument must be a type of TEXT.
* function should not return a set.
* keyword FORMAT must be specified while CREATE FUNCTION.
* prokind should be PROKIND_FUNCTION, normal function.
* input argument should be two. because I am not sure how to handle
multiple format templates.
like, CAST('A' AS TEXT FORMAT format1 format2).

for example:
CREATE FUNCTION test(TEXT, TEXT) RETURNS JSON AS $$ BEGIN RETURN '1';
END; $$ LANGUAGE plpgsql VOLATILE FORMAT;
this function "test" format text based on second argument(template)
and return json type.

POC attached.
what do you think?
From c944169304c3922c3cc76166ccd7a54cfcf595ba Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Tue, 29 Jul 2025 11:10:24 +0800
Subject: [PATCH v3 1/1] CAST(val AS type FORMAT 'template')

context: https://wiki.postgresql.org/wiki/PostgreSQL_vs_SQL_Standard#Major_features_simply_not_implemented_yet
discussion: https://postgr.es/m/CACJufxGqm7cYQ5C65Eoh1z-f+aMdhv9_7V=[email-masked]
---
 src/backend/catalog/pg_aggregate.c     |   1 +
 src/backend/catalog/pg_proc.c          |  61 ++++++
 src/backend/commands/functioncmds.c    |  22 +-
 src/backend/commands/typecmds.c        |   4 +
 src/backend/nodes/nodeFuncs.c          |   2 +
 src/backend/parser/gram.y              |  21 ++
 src/backend/parser/parse_coerce.c      | 266 +++++++++++++++++++++++++
 src/backend/parser/parse_expr.c        |  27 ++-
 src/backend/parser/parse_utilcmd.c     |   1 +
 src/backend/utils/adt/ruleutils.c      |  70 +++++++
 src/backend/utils/cache/lsyscache.c    |  19 ++
 src/include/catalog/pg_proc.dat        |  22 +-
 src/include/catalog/pg_proc.h          |   6 +-
 src/include/nodes/parsenodes.h         |   1 +
 src/include/parser/parse_coerce.h      |   8 +
 src/include/utils/lsyscache.h          |   1 +
 src/test/regress/expected/horology.out |  94 +++++++++
 src/test/regress/expected/misc.out     | 169 ++++++++++++++++
 src/test/regress/expected/numeric.out  |  49 ++++-
 src/test/regress/sql/horology.sql      |  26 +++
 src/test/regress/sql/misc.sql          |  47 +++++
 src/test/regress/sql/numeric.sql       |  11 +-
 22 files changed, 905 insertions(+), 23 deletions(-)

diff --git a/src/backend/catalog/pg_aggregate.c b/src/backend/catalog/pg_aggregate.c
index a05f8a87c1f..10eb6ff4ce4 100644
--- a/src/backend/catalog/pg_aggregate.c
+++ b/src/backend/catalog/pg_aggregate.c
@@ -627,6 +627,7 @@ AggregateCreate(const char *aggName,
 							 false, /* security invoker (currently not
 									 * definable for agg) */
 							 false, /* isLeakProof */
+							 false, /* format */
 							 false, /* isStrict (not needed for agg) */
 							 PROVOLATILE_IMMUTABLE, /* volatility (not needed
 													 * for agg) */
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 5fdcf24d5f8..58d13dc4543 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -16,6 +16,7 @@
 
 #include "access/htup_details.h"
 #include "access/table.h"
+#include "access/tableam.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
@@ -38,6 +39,7 @@
 #include "tcop/tcopprot.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/regproc.h"
 #include "utils/rel.h"
@@ -109,6 +111,7 @@ ProcedureCreate(const char *procedureName,
 				char prokind,
 				bool security_definer,
 				bool isLeakProof,
+				bool isFormat,
 				bool isStrict,
 				char volatility,
 				char parallel,
@@ -336,6 +339,7 @@ ProcedureCreate(const char *procedureName,
 	values[Anum_pg_proc_prokind - 1] = CharGetDatum(prokind);
 	values[Anum_pg_proc_prosecdef - 1] = BoolGetDatum(security_definer);
 	values[Anum_pg_proc_proleakproof - 1] = BoolGetDatum(isLeakProof);
+	values[Anum_pg_proc_proformat - 1] = BoolGetDatum(isFormat);
 	values[Anum_pg_proc_proisstrict - 1] = BoolGetDatum(isStrict);
 	values[Anum_pg_proc_proretset - 1] = BoolGetDatum(returnsSet);
 	values[Anum_pg_proc_provolatile - 1] = CharGetDatum(volatility);
@@ -382,6 +386,63 @@ ProcedureCreate(const char *procedureName,
 	rel = table_open(ProcedureRelationId, RowExclusiveLock);
 	tupDesc = RelationGetDescr(rel);
 
+	if (isFormat)
+	{
+		if (parameterCount != 2 || parameterTypes->values[1] != TEXTOID)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("type format function should hav only two argument and the last argument type must be TEXTOID"));
+		if (parameterTypes->values[0] != TEXTOID && returnType != TEXTOID)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("type format function first input argument should be TEXTOID or the return type as TEXTOID"));
+		if (parameterTypes->values[1] == TEXTOID)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("type format function second input argument must be TEXTOID"));
+		if (returnsSet)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("type format function must not return a set"));
+		if (parameterModes != PointerGetDatum(NULL))
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("type format function proargmodes should be NULL"));
+		if (prokind != PROKIND_FUNCTION)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("type format function can only be normal function"));
+	}
+
+	/* unlikely be reachable, since type format function is rare */
+	if (isFormat)
+	{
+		SysScanDesc	sscan;
+		ScanKeyData scankey;
+		HeapTuple	protup;
+
+		ScanKeyInit(&scankey,
+					Anum_pg_proc_proformat,
+					BTEqualStrategyNumber, F_BOOLEQ,
+					BoolGetDatum(true));
+		sscan = systable_beginscan(rel, ProcedureProformatIndexId, true,
+								   SnapshotSelf, 1, &scankey);
+		while (HeapTupleIsValid(protup = systable_getnext(sscan)))
+		{
+			Form_pg_proc procform = (Form_pg_proc) GETSTRUCT(protup);
+			if (procform->proformat &&
+				procform->proargtypes.values[0] == parameterTypes->values[0] &&
+				procform->proargtypes.values[1] == TEXTOID &&
+				procform->prorettype == returnType)
+				ereport(ERROR,
+						errcode(ERRCODE_DUPLICATE_FUNCTION),
+						errmsg("formatter function \"%s\" already exists with for type formatter %s",
+								get_func_name(procform->oid	),
+								format_type_be(parameterTypes->values[0])));
+		}
+		systable_endscan(sscan);
+	}
+
 	/* Check for pre-existing definition */
 	oldtup = SearchSysCache3(PROCNAMEARGSNSP,
 							 PointerGetDatum(procedureName),
diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c
index 0335e982b31..aadcc1dd8c3 100644
--- a/src/backend/commands/functioncmds.c
+++ b/src/backend/commands/functioncmds.c
@@ -519,6 +519,7 @@ compute_common_attribute(ParseState *pstate,
 						 DefElem **strict_item,
 						 DefElem **security_item,
 						 DefElem **leakproof_item,
+						 DefElem **format_item,
 						 List **set_items,
 						 DefElem **cost_item,
 						 DefElem **rows_item,
@@ -559,6 +560,14 @@ compute_common_attribute(ParseState *pstate,
 
 		*leakproof_item = defel;
 	}
+	else if (strcmp(defel->defname, "format") == 0)
+	{
+		if (is_procedure)
+			goto procedure_error;
+		if (*format_item)
+			errorConflictingDefElem(defel, pstate);
+		*format_item = defel;
+	}
 	else if (strcmp(defel->defname, "set") == 0)
 	{
 		*set_items = lappend(*set_items, defel->arg);
@@ -737,6 +746,7 @@ compute_function_attributes(ParseState *pstate,
 							bool *strict_p,
 							bool *security_definer,
 							bool *leakproof_p,
+							bool *format_p,
 							ArrayType **proconfig,
 							float4 *procost,
 							float4 *prorows,
@@ -752,6 +762,7 @@ compute_function_attributes(ParseState *pstate,
 	DefElem    *strict_item = NULL;
 	DefElem    *security_item = NULL;
 	DefElem    *leakproof_item = NULL;
+	DefElem    *format_item = NULL;
 	List	   *set_items = NIL;
 	DefElem    *cost_item = NULL;
 	DefElem    *rows_item = NULL;
@@ -798,6 +809,7 @@ compute_function_attributes(ParseState *pstate,
 										  &strict_item,
 										  &security_item,
 										  &leakproof_item,
+										  &format_item,
 										  &set_items,
 										  &cost_item,
 										  &rows_item,
@@ -828,6 +840,8 @@ compute_function_attributes(ParseState *pstate,
 		*security_definer = boolVal(security_item->arg);
 	if (leakproof_item)
 		*leakproof_p = boolVal(leakproof_item->arg);
+	if (format_item)
+		*format_p = boolVal(format_item->arg);
 	if (set_items)
 		*proconfig = update_proconfig_value(NULL, set_items);
 	if (cost_item)
@@ -1052,7 +1066,8 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 	bool		isWindowFunc,
 				isStrict,
 				security,
-				isLeakProof;
+				isLeakProof,
+				isFormat;
 	char		volatility;
 	ArrayType  *proconfig;
 	float4		procost;
@@ -1080,6 +1095,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 	isStrict = false;
 	security = false;
 	isLeakProof = false;
+	isFormat = false;
 	volatility = PROVOLATILE_VOLATILE;
 	proconfig = NULL;
 	procost = -1;				/* indicates not set */
@@ -1094,6 +1110,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 								&as_clause, &language, &transformDefElem,
 								&isWindowFunc, &volatility,
 								&isStrict, &security, &isLeakProof,
+								&isFormat,
 								&proconfig, &procost, &prorows,
 								&prosupport, &parallel);
 
@@ -1285,6 +1302,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 						   stmt->is_procedure ? PROKIND_PROCEDURE : (isWindowFunc ? PROKIND_WINDOW : PROKIND_FUNCTION),
 						   security,
 						   isLeakProof,
+						   isFormat,
 						   isStrict,
 						   volatility,
 						   parallel,
@@ -1370,6 +1388,7 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
 	DefElem    *strict_item = NULL;
 	DefElem    *security_def_item = NULL;
 	DefElem    *leakproof_item = NULL;
+	DefElem    *format_item = NULL;
 	List	   *set_items = NIL;
 	DefElem    *cost_item = NULL;
 	DefElem    *rows_item = NULL;
@@ -1414,6 +1433,7 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
 									 &strict_item,
 									 &security_def_item,
 									 &leakproof_item,
+									 &format_item,
 									 &set_items,
 									 &cost_item,
 									 &rows_item,
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 26d985193ae..ccd5c8a14a5 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1809,6 +1809,7 @@ makeRangeConstructors(const char *name, Oid namespace,
 								 PROKIND_FUNCTION,
 								 false, /* security_definer */
 								 false, /* leakproof */
+								 false, /* format */
 								 false, /* isStrict */
 								 PROVOLATILE_IMMUTABLE, /* volatility */
 								 PROPARALLEL_SAFE,	/* parallel safety */
@@ -1875,6 +1876,7 @@ makeMultirangeConstructors(const char *name, Oid namespace,
 							 PROKIND_FUNCTION,
 							 false, /* security_definer */
 							 false, /* leakproof */
+							 false, /* format */
 							 true,	/* isStrict */
 							 PROVOLATILE_IMMUTABLE, /* volatility */
 							 PROPARALLEL_SAFE,	/* parallel safety */
@@ -1920,6 +1922,7 @@ makeMultirangeConstructors(const char *name, Oid namespace,
 							 PROKIND_FUNCTION,
 							 false, /* security_definer */
 							 false, /* leakproof */
+							 false, /* format */
 							 true,	/* isStrict */
 							 PROVOLATILE_IMMUTABLE, /* volatility */
 							 PROPARALLEL_SAFE,	/* parallel safety */
@@ -1959,6 +1962,7 @@ makeMultirangeConstructors(const char *name, Oid namespace,
 							 PROKIND_FUNCTION,
 							 false, /* security_definer */
 							 false, /* leakproof */
+							 false, /* format */
 							 true,	/* isStrict */
 							 PROVOLATILE_IMMUTABLE, /* volatility */
 							 PROPARALLEL_SAFE,	/* parallel safety */
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 7bc823507f1..91560bd1844 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4464,6 +4464,8 @@ raw_expression_tree_walker_impl(Node *node,
 
 				if (WALK(tc->arg))
 					return true;
+				if (WALK(tc->format))
+					return true;
 				if (WALK(tc->typeName))
 					return true;
 			}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 73345bb3c70..b21298f75a6 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -156,6 +156,8 @@ static RawStmt *makeRawStmt(Node *stmt, int stmt_location);
 static void updateRawStmtEnd(RawStmt *rs, int end_location);
 static Node *makeColumnRef(char *colname, List *indirection,
 						   int location, core_yyscan_t yyscanner);
+static Node *makeFormattedTypeCast(Node *arg, Node *format,
+								   TypeName *typename, int location);
 static Node *makeTypeCast(Node *arg, TypeName *typename, int location);
 static Node *makeStringConstCast(char *str, int location, TypeName *typename);
 static Node *makeIntConst(int val, int location);
@@ -8828,6 +8830,10 @@ common_func_opt_item:
 				{
 					$$ = makeDefElem("leakproof", (Node *) makeBoolean(true), @1);
 				}
+			| FORMAT
+				{
+					$$ = makeDefElem("format", (Node *) makeBoolean(true), @1);
+				}
 			| NOT LEAKPROOF
 				{
 					$$ = makeDefElem("leakproof", (Node *) makeBoolean(false), @1);
@@ -15945,6 +15951,8 @@ func_expr_common_subexpr:
 				}
 			| CAST '(' a_expr AS Typename ')'
 				{ $$ = makeTypeCast($3, $5, @1); }
+			| CAST '(' a_expr AS Typename FORMAT a_expr ')'
+				{ $$ = makeFormattedTypeCast($3, $7, $5, @1); }
 			| EXTRACT '(' extract_list ')'
 				{
 					$$ = (Node *) makeFuncCall(SystemFuncName("extract"),
@@ -18832,12 +18840,25 @@ makeColumnRef(char *colname, List *indirection,
 	return (Node *) c;
 }
 
+static Node *
 
 	n->arg = arg;
+	n->format = NULL;
 	n->typeName = typename;
 	n->location = location;
 	return (Node *) n;
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index 0b5b81c7f27..9eee9da6447 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -14,6 +14,9 @@
  */
 #include "postgres.h"
 
+#include "access/table.h"
+#include "access/tableam.h"
+#include "access/heapam.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.h"
 #include "catalog/pg_inherits.h"
@@ -130,6 +133,66 @@ coerce_to_target_type(ParseState *pstate, Node *expr, Oid exprtype,
 	return result;
 }
 
+/*
 
 /*
  * coerce_type()
@@ -545,6 +608,209 @@ coerce_type(ParseState *pstate, Node *node,
 	return NULL;				/* keep compiler quiet */
 }
 
+Node *
+	Relation	pg_proc;
+	SysScanDesc	sscan;
+	ScanKeyData scankey;
+	HeapTuple	tuple;
+		inputBaseTypeId = TEXTOID;
+	pg_proc = table_open(ProcedureRelationId, AccessShareLock);
+	ScanKeyInit(&scankey,
+				Anum_pg_proc_proformat,
+				BTEqualStrategyNumber, F_BOOLEQ,
+				BoolGetDatum(true));
+	sscan = systable_beginscan(pg_proc, ProcedureProformatIndexId, true,
+							   SnapshotSelf, 1, &scankey);
+	while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+		Form_pg_proc procform = (Form_pg_proc) GETSTRUCT(tuple);
+		if (!procform->proformat)
+			continue;
+		if (procform->proargtypes.values[0] != inputBaseTypeId)
+			continue;
+		if (procform->proargtypes.values[1] != TEXTOID)
+			continue;
+		if (procform->prorettype != baseTypeId)
+			continue;
+		funcId = procform->oid;
+	systable_endscan(sscan);
+	table_close(pg_proc, AccessShareLock);
+	if (!OidIsValid(funcId))
+		if (inputTypeId == UNKNOWNOID)
+			inputTypeId = TEXTOID;
+				errmsg("cannot use FORMAT template cast type %s to %s",
+				errhint("Formatted type cast function casting %s to %s does not exists",
 
 /*
  * can_coerce_type()
index d66276801c6..ef7d103a998 100644
 
+		if(tc->format)
+			ereport(ERROR,
 
+	if(tc->format)
 
index 98fd300c35a..bde75c45028 100644
 
@@ -10896,6 +10902,70 @@ get_func_expr(FuncExpr *expr, deparse_context *context,
 
+/*
+ * get_fmt_coercion_expr
+ *
+ * Parse back expression: CAST (expr AS type FORMAT 'fmt')
+ */
+static bool
+get_fmt_coercion_expr(FuncExpr *expr, deparse_context *context,
+					  Oid resulttype, int32 resulttypmod)
+
+{
+	Node	   *arg;
+	Const	   *second_arg;
+	FuncExpr 	*func;
+	StringInfo	buf = context->buf;
+
+	func = expr;
+	if (func->funcformat != COERCE_EXPLICIT_CAST)
+		return false;
+
+	if (func->funcvariadic)
+		return false;
+
+	if (list_length(func->args) != 2)
+		return false;
+
+	arg = linitial(func->args);
+	second_arg = (Const *) lsecond(func->args);
+
+	if (exprType(arg) != TEXTOID &&
+		func->funcresulttype != TEXTOID)
+		return false;
+
+	if (!IsA(second_arg, Const) ||
+		second_arg->consttype != TEXTOID ||
+		second_arg->constisnull)
+		return false;
+
+	if (!get_func_retformat(func->funcid))
+		return false;
+
+	appendStringInfoString(buf, "CAST(");
+
+	if (!PRETTY_PAREN(context))
+		appendStringInfoChar(buf, '(');
+	get_rule_expr_paren(arg, context, false, (Node *) func);
+	if (!PRETTY_PAREN(context))
+		appendStringInfoChar(buf, ')');
+
+	/*
+	 * Never emit resulttype(arg) functional notation. A pg_proc entry could
+	 * take precedence, and a resulttype in pg_temp would require schema
+	 * qualification that format_type_with_typemod() would usually omit. We've
+	 * standardized on arg::resulttype, but CAST(arg AS resulttype) notation
+	 * would work fine.
+	 */
+	appendStringInfo(buf, " AS %s FORMAT ",
+					 format_type_with_typemod(resulttype, resulttypmod));
+
+	get_const_expr((Const *) second_arg, context, -1);
+	appendStringInfoChar(buf, ')');
+
+	return true;
+}
+
 /*
  * get_agg_expr			- Parse back an Aggref node
  */
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index c460a72b75d..9ace1ff99e6 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1806,6 +1806,25 @@ get_func_rettype(Oid funcid)
 	return result;
 }
 
+/*
+ * get_func_retformat
+ *		Given procedure id return the function's proformat flag.
+ */
+bool
+get_func_retformat(Oid funcid)
+{
+	HeapTuple	tp;
+	bool		result;
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for function %u", funcid);
+
+	result = ((Form_pg_proc) GETSTRUCT(tp))->proformat;
+	ReleaseSysCache(tp);
+	return result;
+}
+
 /*
  * get_func_nargs
  *		Given procedure id, return the number of arguments.
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3ee8fed7e53..f57d8747706 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4781,33 +4781,33 @@
 # formatting
 { oid => '1770', descr => 'format timestamp with time zone to text',
   proname => 'to_char', provolatile => 's', prorettype => 'text',
-  proargtypes => 'timestamptz text', prosrc => 'timestamptz_to_char' },
+  proargtypes => 'timestamptz text', prosrc => 'timestamptz_to_char', proformat => 't' },
 { oid => '1772', descr => 'format numeric to text',
   proname => 'to_char', provolatile => 's', prorettype => 'text',
-  proargtypes => 'numeric text', prosrc => 'numeric_to_char' },
+  proargtypes => 'numeric text', prosrc => 'numeric_to_char', proformat => 't' },
 { oid => '1773', descr => 'format int4 to text',
-  proname => 'to_char', provolatile => 's', prorettype => 'text',
+  proname => 'to_char', provolatile => 's', prorettype => 'text', proformat => 't',
   proargtypes => 'int4 text', prosrc => 'int4_to_char' },
 { oid => '1774', descr => 'format int8 to text',
-  proname => 'to_char', provolatile => 's', prorettype => 'text',
+  proname => 'to_char', provolatile => 's', prorettype => 'text', proformat => 't',
   proargtypes => 'int8 text', prosrc => 'int8_to_char' },
 { oid => '1775', descr => 'format float4 to text',
-  proname => 'to_char', provolatile => 's', prorettype => 'text',
+  proname => 'to_char', provolatile => 's', prorettype => 'text', proformat => 't',
   proargtypes => 'float4 text', prosrc => 'float4_to_char' },
 { oid => '1776', descr => 'format float8 to text',
-  proname => 'to_char', provolatile => 's', prorettype => 'text',
+  proname => 'to_char', provolatile => 's', prorettype => 'text', proformat => 't',
   proargtypes => 'float8 text', prosrc => 'float8_to_char' },
 { oid => '1777', descr => 'convert text to numeric',
-  proname => 'to_number', provolatile => 's', prorettype => 'numeric',
+  proname => 'to_number', provolatile => 's', prorettype => 'numeric', proformat => 't',
   proargtypes => 'text text', prosrc => 'numeric_to_number' },
 { oid => '1778', descr => 'convert text to timestamp with time zone',
-  proname => 'to_timestamp', provolatile => 's', prorettype => 'timestamptz',
+  proname => 'to_timestamp', provolatile => 's', prorettype => 'timestamptz', proformat => 't',
   proargtypes => 'text text', prosrc => 'to_timestamp' },
 { oid => '1780', descr => 'convert text to date',
   proname => 'to_date', provolatile => 's', prorettype => 'date',
-  proargtypes => 'text text', prosrc => 'to_date' },
+  proargtypes => 'text text', prosrc => 'to_date', proformat => 't' },
 { oid => '1768', descr => 'format interval to text',
-  proname => 'to_char', provolatile => 's', prorettype => 'text',
+  proname => 'to_char', provolatile => 's', prorettype => 'text', proformat => 't',
   proargtypes => 'interval text', prosrc => 'interval_to_char' },
 
 { oid => '1282', descr => 'quote an identifier for usage in a querystring',
@@ -6433,7 +6433,7 @@
   proname => 'isfinite', prorettype => 'bool', proargtypes => 'timestamp',
   prosrc => 'timestamp_finite' },
 { oid => '2049', descr => 'format timestamp to text',
-  proname => 'to_char', provolatile => 's', prorettype => 'text',
+  proname => 'to_char', provolatile => 's', prorettype => 'text', proformat => 't',
   proargtypes => 'timestamp text', prosrc => 'timestamp_to_char' },
 { oid => '2052',
   proname => 'timestamp_eq', proleakproof => 't', prorettype => 'bool',
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index d7353e7a088..7c0ceb89aa0 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -64,6 +64,9 @@ CATALOG(pg_proc,1255,ProcedureRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(81,Proce
 	/* is it a leakproof function? */
 	bool		proleakproof BKI_DEFAULT(f);
 
+	/* is it a leakproof function? */
+	bool		proformat BKI_DEFAULT(f);
+
 	/* strict with respect to NULLs? */
 	bool		proisstrict BKI_DEFAULT(t);
 
@@ -139,7 +142,7 @@ DECLARE_TOAST(pg_proc, 2836, 2837);
 
 DECLARE_UNIQUE_INDEX_PKEY(pg_proc_oid_index, 2690, ProcedureOidIndexId, pg_proc, btree(oid oid_ops));
 DECLARE_UNIQUE_INDEX(pg_proc_proname_args_nsp_index, 2691, ProcedureNameArgsNspIndexId, pg_proc, btree(proname name_ops, proargtypes oidvector_ops, pronamespace oid_ops));
-
+DECLARE_INDEX(pg_proc_proformat_index, 2775, ProcedureProformatIndexId, pg_proc, btree(proformat bool_ops));
 MAKE_SYSCACHE(PROCOID, pg_proc_oid_index, 128);
 MAKE_SYSCACHE(PROCNAMEARGSNSP, pg_proc_proname_args_nsp_index, 128);
 
@@ -202,6 +205,7 @@ extern ObjectAddress ProcedureCreate(const char *procedureName,
 									 char prokind,
 									 bool security_definer,
 									 bool isLeakProof,
+									 bool isFormat,
 									 bool isStrict,
 									 char volatility,
 									 char parallel,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..b71c4135ae5 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -395,6 +395,7 @@ typedef struct TypeCast
 {
 	NodeTag		type;
 	Node	   *arg;			/* the expression being casted */
+	Node	   *format;			/* the cast format template Const*/
 	TypeName   *typeName;		/* the target type */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } TypeCast;
diff --git a/src/include/parser/parse_coerce.h b/src/include/parser/parse_coerce.h
index 8d775c72c59..282f559c4e1 100644
--- a/src/include/parser/parse_coerce.h
+++ b/src/include/parser/parse_coerce.h
@@ -43,11 +43,19 @@ extern Node *coerce_to_target_type(ParseState *pstate,
 								   CoercionContext ccontext,
 								   CoercionForm cformat,
 								   int location);
+extern Node *coerce_to_target_type_fmt(ParseState *pstate,
+									   Node *expr,Node *format,
+									   Oid exprtype, Oid targettype,
+									   int32 targettypmod, CoercionContext ccontext,
+									   CoercionForm cformat, int location);
 extern bool can_coerce_type(int nargs, const Oid *input_typeids, const Oid *target_typeids,
 							CoercionContext ccontext);
 extern Node *coerce_type(ParseState *pstate, Node *node,
 						 Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
 						 CoercionContext ccontext, CoercionForm cformat, int location);
+Node *coerce_type_fmt(ParseState *pstate, Node *node, Node *format,
+					  Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
+					  CoercionContext ccontext, CoercionForm cformat, int location);
 extern Node *coerce_to_domain(Node *arg, Oid baseTypeId, int32 baseTypeMod,
 							  Oid typeId,
 							  CoercionContext ccontext, CoercionForm cformat, int location,
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index fa7c7e0323b..a1e6a5703b0 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -128,6 +128,7 @@ extern RegProcedure get_oprjoin(Oid opno);
 extern char *get_func_name(Oid funcid);
 extern Oid	get_func_namespace(Oid funcid);
 extern Oid	get_func_rettype(Oid funcid);
+extern bool get_func_retformat(Oid funcid);
 extern int	get_func_nargs(Oid funcid);
 extern Oid	get_func_signature(Oid funcid, Oid **argtypes, int *nargs);
 extern Oid	get_func_variadictype(Oid funcid);
diff --git a/src/test/regress/expected/horology.out b/src/test/regress/expected/horology.out
index 5ae93d8e8a5..c6b8b65b6dc 100644
--- a/src/test/regress/expected/horology.out
+++ b/src/test/regress/expected/horology.out
@@ -3110,6 +3110,13 @@ SELECT to_timestamp('15 "text between quote marks" 98 54 45',
  Thu Jan 01 15:54:45 1998 PST
 (1 row)

 

 

 

 
index 6e816c57f1f..e26f29b23de 100644
@@ -396,3 +396,172 @@ SELECT *, (equipment(CAST((h.*) AS hobbies_r))).name FROM hobbies_r h;
+ERROR:  cannot use FORMAT template cast type text to text
+HINT:  Formatted type cast function casting text to text does not exists
+ERROR:  cannot use FORMAT template cast type text to timestamp without time zone
+HINT:  Formatted type cast function casting text to timestamp without time zone does not exists
+ERROR:  cannot use FORMAT template cast type text to timestamp without time zone[]
+HINT:  Formatted type cast function casting text to timestamp without time zone[] does not exists
+ERROR:  cannot use FORMAT template cast type text to boolean
+HINT:  Formatted type cast function casting text to boolean does not exists
+ERROR:  cannot use FORMAT template cast type text to json
+HINT:  Formatted type cast function casting text to json does not exists
+ERROR:  cannot use FORMAT template cast type json to text
+HINT:  Formatted type cast function casting json to text does not exists

 

 

 

 

 

 SET TIME ZONE '+2';

---

From: "David G. Johnston" <[email-masked]>
Date: Mon, 28 Jul 2025 20:54:04 -0700

> select proname, prosrc, proformat from pg_proc where proformat;

My first impression of this choice was not good.

How about changing the specification for create type.  Right now input
functions must declare either 1 or 3 arguments.  Let’s also allow for 2 and
4-argument functions where the 2nd or 4th is where the format is passed.
If a data type input function lacks one of those signatures it is a runtime
error if a format clause is attached to its cast expression.  For output,
we go from having zero input arguments to zero or one, with the same
resolution behavior.

Pass null for the format if the clause is missing or the cast is done via
the :: operator, or any other context format is not able to be specified.

The slight variation to this would be to specify these 2/4 and 1-arg
functions as optional “format_in” and “format_out” optional properties
(like typmod_in).  The format-aware code can look for these which will end
up having the full implementation while the current IO functions would
simply stub out calls, passing null as the format. (Or maybe some variation
that looks similar to typmod handling…which I haven’t looked at.)

David J.

---

From: jian he <[email-masked]>
Date: Fri, 1 Aug 2025 16:22:52 +0800

On Tue, Jul 29, 2025 at 11:54 AM David G. Johnston
<[email-masked]> wrote:
>
> The slight variation to this would be to specify these 2/4 and 1-arg functions as optional “format_in” and “format_out” optional properties (like typmod_in).  The format-aware code can look for these which will end up having the full implementation while the current IO functions would simply stub out calls, passing null as the format. (Or maybe some variation that looks similar to typmod handling…which I haven’t looked at.)
>

This may also work.
typmod_in, typmod_out, which is associated with typmod, which is used
in many places.
The only use case for (typformatin, typformatout) is CAST expression.
so we also need to consider the overhead of adding
two oid columns (typformatin, typformatout) to pg_type.

another question is:
should we first implement CAST(expr AS type FORMAT 'template') for limited types
(to_date, to_char, to_number, to_timestamptz)
or first try to make it more generic?

---

From: Vik Fearing <[email-masked]>
Date: Fri, 1 Aug 2025 12:34:02 +0200

> or first try to make it more generic?

My fear is that if we don't, it will never get done.

-- 

Vik Fearing

---

From: jian he <[email-masked]>
Date: Mon, 4 Aug 2025 11:10:06 +0800

hi.
one more question:

For binary coercible type casts, no formatted related function for it,
should we error out?
For example, should the following error out or return text '1'.

select cast('1'::text as text format 'YYYY'::text);

---

From: "David G. Johnston" <[email-masked]>
Date: Sun, 3 Aug 2025 20:36:00 -0700

> select cast('1'::text as text format 'YYYY'::text);

I'm hoping the standard says (or allows us to) error out here.

text as a type has no semantics on which to associate a format so it
should be an error to attempt to do so.  Not a silent no-op.

I was under the impression that for format to be allowed in the expression
one of the two data types involved has to be text and the other must not be
text.

IME we are actually implementing a formatting option for text serialization
and deserialization here, not a cast (we are just borrowing existing syntax
that is serviceable).  Hence the absence of these entries in pg_cast and
why the fit into pg_type seems so reasonable.

The existence of the various "to_char" and "to_date" functions reflects the
historical lack of a dedicated syntax for this kind of (de-)serialization.
But it seems unwise to bias ourselves to how the new syntax/feature should
be implemented just because these functions exist.  At least one design
should be done pretending they don't and see what comes out of it.  Their
code can always be moved or reused in whatever we come up with; forcing
them to be used directly, as-is, within the new solution adds an
unnecessary constraint.

David J.

---

From: Corey Huinker <[email-masked]>
Date: Mon, 4 Aug 2025 01:38:18 -0400

> I'm hoping the standard says (or allows us to) error out here.

We have some influence in that, I believe.

> should be an error to attempt to do so.  Not a silent no-op.

+1

> one of the two data types involved has to be text and the other must not be

I hadn't understood that, but also hadn't thought of a case where it might
be wanted until just now. What if someone wanted a cast from JSONB to their
custom type, and the format was a specific keypath to extract from the
JSONB? It's true that could be accomplished by first extracting the keypath
and then CASTing that expression, but the same is true for text->date,
regexing a YYYY-MM-DD into the locale default.

>
> IME we are actually implementing a formatting option for text
> serialization and deserialization here, not a cast (we are just borrowing
> existing syntax that is serviceable).  Hence the absence of these entries
> in pg_cast and why the fit into pg_type seems so reasonable.
>
> The existence of the various "to_char" and "to_date" functions reflects
> the historical lack of a dedicated syntax for this kind of
> (de-)serialization.  But it seems unwise to bias ourselves to how the new
> syntax/feature should be implemented just because these functions exist.
> At least one design should be done pretending they don't and see what comes
> out of it.  Their code can always be moved or reused in whatever we come up
> with; forcing them to be used directly, as-is, within the new solution adds
> an unnecessary constraint.
>

I agree. I'd like the more generic solution, but I don't want to get in the
way of getting it done, especially if we can change the internals later
with no user impact.

But, once this is implemented, does it then make sense to then parse
to_char() and to_date() into casts?

---

From: Corey Huinker <[email-masked]>
Date: Mon, 4 Aug 2025 01:39:53 -0400

> or first try to make it more generic?
That was my plan, essentially rewriting these into safe versions of the
existing to_date/to_timestamp/etc functions, but much has changed since
then, so while it still seems like a good intermediate step, it may be a
distraction as others have stated elsewhere in the thread.

---

From: "David G. Johnston" <[email-masked]>
Date: Sun, 3 Aug 2025 22:55:08 -0700

On Sunday, August 3, 2025, Corey Huinker <[email-masked]> wrote:

>> I was under the impression that for format to be allowed in the
>> expression one of the two data types involved has to be text and the other
>> must not be text.
>>
>

Feels like the same basic answer.  Create cast has a single (because it’s
one-way) function accepting between 1 and 3 arguments.  Change it to accept
between 1 and 4 arguments and the 4th is where the format expression gets
passed.  If a format expression is present and the function doesn’t have a
4th argument, error.

> to_char() and to_date() into casts?

I have no principled reason but I wouldn’t bother to turn these calls into
casts nor do I think turning casts into these specific function calls by
name is a good idea.  Leave the legacy stuff in place for compatibility,
unchanged from its present form, and do the new stuff anew.

David J.

---

From: Vik Fearing <[email-masked]>
Date: Mon, 4 Aug 2025 14:57:47 +0200

On 04/08/2025 07:55, David G. Johnston wrote:
>     On Sun, Aug 3, 2025 at 11:36 PM David G. Johnston
>         I was under the impression that for format to be allowed in
>         the expression one of the two data types involved has to be
>         text and the other must not be text.
>
>
>     I hadn't understood that, but also hadn't thought of a case where
>     it might be wanted until just now. What if someone wanted a cast
>     from JSONB to their custom type, and the format was a specific
>     keypath to extract from the JSONB? It's true that could be
>     accomplished by first extracting the keypath and then CASTing that
>     expression, but the same is true for text->date, regexing a
>     YYYY-MM-DD into the locale default.
>
>
> Feels like the same basic answer.  Create cast has a single (because 
> it’s one-way) function accepting between 1 and 3 arguments.  Change it 
> to accept between 1 and 4 arguments and the 4th is where the format 
> expression gets passed.  If a format expression is present and the 
> function doesn’t have a 4th argument, error.

This is my position as well.

+1

-- 

Vik Fearing

---

From: jian he <[email-masked]>
Date: Tue, 12 Aug 2025 14:37:10 +0800

hi.
please check the attached v4 patch.

1. For binary-coercible casts, if the format template is specified,
raise an error.
Example:
SELECT CAST('1'::text AS text FORMAT 'YYYY'::text); -- error
2. limited implementation — currently only supports to_char, to_date,
to_number, and to_timestamp.
3. coerce_to_target_type function is used in many places, refactoring
add another
argument seems not practical. So, I introduced a new function
coerce_to_target_type_fmt. Similarly, since coerce_type is difficult to
refactor too, I created a new function coerce_type_fmt.

At this stage, we have not modified any pg_cast entries.  Adding to_char,
to_date, etc., into pg_cast has implications that require more consideration
(see [1]).
Also for this patch, including these functions in pg_cast is not really
necessary to achieve the intended behavior.

[1] https://postgr.es/m/CACJufxF4OW=[email-masked]
From ef719a5fcf2e96d7fc756a021aebd8caf5b0b385 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Tue, 12 Aug 2025 14:23:02 +0800
Subject: [PATCH v4 1/1] CAST(val AS type FORMAT 'template')
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

1. For binary-coercible casts, if a format template is specified, raise an error.
Example:
SELECT CAST('1'::text AS text FORMAT 'YYYY'::text); -- error
2. limited implementation — currently only supports to_char, to_date, to_number, and to_timestamp.
3. coerce_to_target_type function is used in many places, so adding another
argument is not practical. So, I introduced a new function
coerce_to_target_type_fmt. Similarly, since coerce_type is difficult to
refactor, we use function coerce_type_fmt.

At this stage, we have not modified any pg_cast entries.  Adding to_char,
to_date, etc., into pg_cast has implications that require more consideration
(see [1]).
Also for this patch, including these functions in pg_cast is not really
necessary to achieve the intended behavior.

context: https://wiki.postgresql.org/wiki/PostgreSQL_vs_SQL_Standard#Major_features_simply_not_implemented_yet
discussion: https://postgr.es/m/CACJufxGqm7cYQ5C65Eoh1z-f+aMdhv9_7V=[email-masked]
---
 src/backend/nodes/nodeFuncs.c          |   2 +
 src/backend/parser/gram.y              |  17 ++
 src/backend/parser/parse_coerce.c      | 335 +++++++++++++++++++++++++
 src/backend/parser/parse_expr.c        |  27 +-
 src/backend/parser/parse_utilcmd.c     |   1 +
 src/backend/utils/adt/ruleutils.c      |  71 ++++++
 src/include/nodes/parsenodes.h         |   1 +
 src/include/parser/parse_coerce.h      |   8 +
 src/test/regress/expected/horology.out |  94 +++++++
 src/test/regress/expected/misc.out     | 198 +++++++++++++++
 src/test/regress/expected/numeric.out  |  49 +++-
 src/test/regress/sql/horology.sql      |  26 ++
 src/test/regress/sql/misc.sql          |  51 ++++
 src/test/regress/sql/numeric.sql       |  11 +-
 14 files changed, 881 insertions(+), 10 deletions(-)

 				if (WALK(tc->arg))
 					return true;
+				if (WALK(tc->format))
+					return true;
 				if (WALK(tc->typeName))
 					return true;
 			}
index db43034b9db..ef08bba36ef 100644
@@ -15933,6 +15935,8 @@ func_expr_common_subexpr:
@@ -18820,12 +18824,25 @@ makeColumnRef(char *colname, List *indirection,
 	return (Node *) c;
 }
 
+static Node *
 
 	n->arg = arg;
+	n->format = NULL;
 	n->typeName = typename;
 	n->location = location;
 	return (Node *) n;
index 0b5b81c7f27..28be9b94637 100644
 
+/*
+ * See coerce_to_target_type for type coerce don't involve format template
 
 /*
  * coerce_type()
@@ -546,6 +606,281 @@ coerce_type(ParseState *pstate, Node *node,
 }

+static Oid
+	Oid			targetBaseTypeId;
+	targetBaseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
+	t_typcategory = TypeCategory(targetBaseTypeId);
+	s_typcategory = TypeCategory(inputTypeId);
+				errmsg("can not use FORMAT template for binary coerceable type cast"),
+	if (targetBaseTypeId != NUMERICOID &&
+		targetBaseTypeId != TIMESTAMPTZOID &&
+		targetBaseTypeId != DATEOID &&
+				errmsg("cannot cast type %s to %s using formatted template",
+				errhint("Only timestamptz, text, numeric and date data type are supported for formatted type casting currently"),
+				errmsg("cannot cast type %s to %s using formatted template",
+				errhint("Only catgeory of numeric, string, datetime, and timespan source data type are supported for formatted type casting");
+	 * Unknown resolve to text Const eventually, but currently text cast to text
+	 * with formatted template is not supported
+	if (t_typcategory == TYPCATEGORY_STRING &&
+		inputBaseTypeId == UNKNOWNOID)
+				errmsg("cannot cast type %s to %s using formatted template",
+						"text",
+	if (targetBaseTypeId == NUMERICOID)
+	else if (targetBaseTypeId == TIMESTAMPTZOID)
+	else if (targetBaseTypeId == DATEOID)
+	/* FIXME: don't understand this part */
+	if (targetTypeId != targetBaseTypeId)
+		result = coerce_to_domain(result, targetBaseTypeId, baseTypeMod,
 
+		if(tc->format)
+			ereport(ERROR,
 
+	if(tc->format)
 
index afcf54169c3..b1c19e6b105 100644
index 3d6e6bdbfd2..187a776d963 100644
@@ -10808,6 +10810,10 @@ get_func_expr(FuncExpr *expr, deparse_context *context,
 
@@ -10864,6 +10870,71 @@ get_func_expr(FuncExpr *expr, deparse_context *context,
 
+/*

 

 

 

 
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD SSSSS');
+       text       
+------------------
+ 2012-12-12 43200
+(1 row)
+
 SET TIME ZONE '+2';
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
          to_char         
diff --git a/src/test/regress/expected/misc.out b/src/test/regress/expected/misc.out
index 6e816c57f1f..fc2ec8c5e3a 100644
--- a/src/test/regress/expected/misc.out
+++ b/src/test/regress/expected/misc.out
@@ -396,3 +396,201 @@ SELECT *, (equipment(CAST((h.*) AS hobbies_r))).name FROM hobbies_r h;
 --
 -- rewrite rules
 --
+select cast('1' as text format 1); --error
+ERROR:  FORMAT template is not string type
+LINE 1: select cast('1' as text format 1);
+                                       ^
+select cast('1' as text format '1'::text); --error
+ERROR:  cannot cast type text to text using formatted template
+LINE 1: select cast('1' as text format '1'::text);
+               ^
+select cast('1'::text as text format '1'::text); --error
+ERROR:  can not use FORMAT template for binary coerceable type cast
+LINE 1: select cast('1'::text as text format '1'::text);
+                                             ^
+select cast(array[1] as text format 'YYYY'); --error
+ERROR:  formmatted type cast does not apply to array type
+LINE 1: select cast(array[1] as text format 'YYYY');
+                    ^
+select cast('1' as date format 'YYYY-MM-DD');
+    date    
+------------
+ 01-01-0001
+(1 row)
+
+select cast('2012-13-12' as date format 'YYYY-DD-MM') as date;
+    date    
+------------
+ 12-13-2012
+(1 row)
+
+select cast('2012-13-12' as timestamptz format 'YYYY-DD-MM') as date;
+             date             
+------------------------------
+ Thu Dec 13 00:00:00 2012 PST
+(1 row)
+
+select cast('2012-13-12' as date format 'YYYY-MM-DD'); --error
+ERROR:  date/time field value out of range: "2012-13-12"
+--type check
+select cast('1' as timestamp format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type unknown to timestamp without time zone using formatted template
+LINE 1: select cast('1' as timestamp format 'YYYY-MM-DD');
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting currently
+select cast('1' as timestamp[] format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type unknown to timestamp without time zone[] using formatted template
+LINE 1: select cast('1' as timestamp[] format 'YYYY-MM-DD');
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting currently
+select cast('1' as bool format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type unknown to boolean using formatted template
+LINE 1: select cast('1' as bool format 'YYYY-MM-DD');
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting currently
+select cast('1' as json format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type unknown to json using formatted template
+LINE 1: select cast('1' as json format 'YYYY-MM-DD');
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting currently
+select cast('1'::json as text format 'YYYY-MM-DD'); --error
+ERROR:  cannot cast type json to text using formatted template
+LINE 1: select cast('1'::json as text format 'YYYY-MM-DD');
+               ^
+HINT:  Only catgeory of numeric, string, datetime, and timespan source data type are supported for formatted type casting
+select cast('1'::text collate "C" as date format 'YYYY-MM-DD');
+    date    
+------------
+ 01-01-0001
+(1 row)
+
+select cast('1' as date format 'YYYY-MM-DD') = to_date('1', 'YYYY-MM-DD') as expect_true;
+ expect_true 
+-------------
+ t
+(1 row)
+
+--domain check
+create domain d1 as date check (value <> '0001-01-01');
+select cast('1' as d1 format 'YYYY-MM-DD'); --error
+ERROR:  value for domain d1 violates check constraint "d1_check"
+select cast('1' as d1 format 'MM-DD'); --ok
+      d1       
+---------------
+ 01-01-0001 BC
+(1 row)
+
+create table tcast(col1 text, col2 text, col3 date, col4 timestamptz);
+insert into tcast(col1, col2) values('2022-12-13', 'YYYY-MM-DD'), ('2022-12-01', 'YYYY-DD-MM');
+select cast(col1 as date format col2) from tcast;
+    col1    
+------------
+ 12-13-2022
+ 01-12-2022
+(2 rows)
+
+select cast(col1 as date format col3) from tcast; --error
+ERROR:  FORMAT template is not string type
+LINE 1: select cast(col1 as date format col3) from tcast;
+                                        ^
+select cast(col1 as date format col3::text) from tcast; --ok
+ col1 
+------
+ 
+ 
+(2 rows)
+
+CREATE FUNCTION stable_const() RETURNS TEXT AS $$ BEGIN RETURN 'YYYY-MM-DD'; END; $$ LANGUAGE plpgsql STABLE;
+select cast(col1 as date format stable_const()) from tcast;
+    col1    
+------------
+ 12-13-2022
+ 12-01-2022
+(2 rows)
+
+create index s1 on tcast(cast(col1 as date format 'YYYY-MM-DD')); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+create index s1 on tcast(cast(col1 as date format stable_const())); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+create view tcast_v1 as select cast(col1 as date format 'YYYY-MM-DD') from tcast;
+select pg_get_viewdef('tcast_v1', false);
+                      pg_get_viewdef                      
+----------------------------------------------------------
+  SELECT CAST((col1) AS date FORMAT 'YYYY-MM-DD') AS col1+
+    FROM tcast;
+(1 row)
+
+select pg_get_viewdef('tcast_v1', true);
+                     pg_get_viewdef                     
+--------------------------------------------------------
+  SELECT CAST(col1 AS date FORMAT 'YYYY-MM-DD') AS col1+
+    FROM tcast;
+(1 row)
+
+--null value check
+select cast(NULL::text as date format 'YYYY-MM-DD');
+ date 
+------
+ 
+(1 row)
+
+select cast(NULL::text as numeric format 'YYYY-MM-DD');
+ numeric 
+---------
+        
+(1 row)
+
+select cast(NULL::text as timestamptz format 'YYYY-MM-DD');
+ timestamptz 
+-------------
+ 
+(1 row)
+
+select cast(NULL::bigint AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
+select cast(NULL::int AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
+select cast(NULL::numeric AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
+select cast(NULL::float8 AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
+select cast(NULL::float4 AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
+select cast(NULL::interval AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
+select cast(NULL::timestamp AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
+select cast(NULL::timestamptz AS TEXT format 'YYYY-MM-DD');
+ text 
+------
+ 
+(1 row)
+
diff --git a/src/test/regress/expected/numeric.out b/src/test/regress/expected/numeric.out
index c58e232a263..b48fe4f3037 100644
--- a/src/test/regress/expected/numeric.out
+++ b/src/test/regress/expected/numeric.out
@@ -2270,6 +2270,12 @@ SELECT to_number('-34,338,492.654,878', '99G999G999D999G999');
  -34338492.654878
 (1 row)

 

 

 

 

 

 SET TIME ZONE '+2';
 
index 165a2e175fb..e7646d6a2a2 100644
@@ -273,3 +273,54 @@ SELECT *, (equipment(CAST((h.*) AS hobbies_r))).name FROM hobbies_r h;
+select cast('1'::text as text format '1'::text); --error
+select cast('2012-13-12' as date format 'YYYY-DD-MM') as date;
+select cast('2012-13-12' as timestamptz format 'YYYY-DD-MM') as date;
+select cast('1'::text collate "C" as date format 'YYYY-MM-DD');
+create index s1 on tcast(cast(col1 as date format stable_const())); --error
+select cast(NULL::text as date format 'YYYY-MM-DD');
+select cast(NULL::text as numeric format 'YYYY-MM-DD');
+select cast(NULL::text as timestamptz format 'YYYY-MM-DD');
+select cast(NULL::bigint AS TEXT format 'YYYY-MM-DD');
+select cast(NULL::int AS TEXT format 'YYYY-MM-DD');
+select cast(NULL::float8 AS TEXT format 'YYYY-MM-DD');
+select cast(NULL::float4 AS TEXT format 'YYYY-MM-DD');

---

From: Corey Huinker <[email-masked]>
Date: Mon, 23 Mar 2026 16:20:14 -0400

>
>
> Typos
> - parse_expr.c:2745: "formmatted" → "formatted"
> - parse_coerce.c:156: "Cocerce" → "Coerce"
>
> In `coerce_type_with_format()` (parse_coerce.c):
>
> 1. The check `if (fmtcategory != TYPCATEGORY_STRING && fmtcategory !=
> TYPCATEGORY_UNKNOWN)`
> could be moved earlier. It doesn't depend on any other
> calculations, so failing fast here would avoid unnecessary work.
>
> 2. consider using` list_make2(node, format)` instead of `list_make1() +
> lappend()`.
>
> --
> Regards,
> Man Zeng

Surya and I did a pair-review of this. In addition to the notes above
(which we agree with), we have the following notes:

+       if (inputBaseTypeId == targetBaseTypeId)
+               ereport(ERROR,
+                               errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                               errmsg("cannot cast type %s to %s while
using a format template",
+                                          format_type_be(inputBaseTypeId),
+
 format_type_be(targetBaseTypeId)),
+                               errdetail("binary coercible type cast is
not supported while using a format template"),
+                               parser_coercion_errposition(pstate,
location, node));

This could use a bit more explanation in a comment - is it because there is
no plausible type that can take a FORMAT and be cast to itself?

+       if (s_typcategory != TYPCATEGORY_NUMERIC &&
+               s_typcategory != TYPCATEGORY_STRING &&
+               s_typcategory != TYPCATEGORY_DATETIME &&
+               s_typcategory != TYPCATEGORY_TIMESPAN)

In situations like this, the committers have shown a strong preference for
switch() statements. Though it may make more sense to package this if into
a static function is_formattable_type() or similar.

+       if (t_typcategory == TYPCATEGORY_STRING)
+               funcname = list_make2(makeString("pg_catalog"),
+
makeString("to_char"));
...
+       else
+               elog(ERROR, "failed to find conversion function from %s to
%s while using a format template",
+                        format_type_be(inputTypeId),
format_type_be(targetTypeId));

Similar thing here, committers will probably want to see a switch.

+create function imm_const() returns text as $$ begin return 'YYYY-MM-DD';
end; $$ language plpgsql immutable;;
+select cast(col1 as date format imm_const()) from tcast;

double-;

Overall, the test coverage appears complete and the close mirroring of
existing tests makes the intention very clear.

---

From: Zsolt Parragi <[email-masked]>
Date: Tue, 24 Mar 2026 06:30:57 +0000

Hello!

There's some inconsistency in deparse, it displays to_char for intervals:

CREATE VIEW test_interval_format AS
  SELECT CAST('1 year 2 months'::interval AS text FORMAT 'YYYY-MM') AS
fmt_interval;
CREATE VIEW test_timestamp_format AS
  SELECT CAST('2024-01-15 00:00:00'::timestamp AS text FORMAT
'YYYY-MM-DD') AS fmt_timestamp;

SELECT pg_get_viewdef('test_interval_format'::regclass);
SELECT pg_get_viewdef('test_timestamp_format'::regclass);

I also see a similar to_char leak in the error message - wouldn't this
confuse users, as they never wrote to_char?

postgres=# SELECT cast('2012-12-12 12:00'::timetz as text format
'YYYY-MM-DD HH:MI:SS TZ');
2026-03-24 06:27:45.792 UTC [5917] ERROR:  function
pg_catalog.to_char(time with time zone, text) does not exist
2026-03-24 06:27:45.792 UTC [5917] DETAIL:  No function of that name
accepts the given argument types.
2026-03-24 06:27:45.792 UTC [5917] HINT:  You might need to add
explicit type casts.
2026-03-24 06:27:45.792 UTC [5917] STATEMENT:  SELECT cast('2012-12-12
12:00'::timetz as text format 'YYYY-MM-DD HH:MI:SS TZ');
ERROR:  function pg_catalog.to_char(time with time zone, text) does not exist
DETAIL:  No function of that name accepts the given argument types.
HINT:  You might need to add explicit type casts.

---

From: jian he <[email-masked]>
Date: Mon, 30 Mar 2026 16:07:51 +0800

On Tue, Mar 24, 2026 at 4:20 AM Corey Huinker <[email-masked]> wrote:
>
> Surya and I did a pair-review of this. In addition to the notes above (which we agree with), we have the following notes:
>
> +                               errmsg("cannot cast type %s to %s while using a format template",
> +                                          format_type_be(targetBaseTypeId)),
> +                               errdetail("binary coercible type cast is not supported while using a format template"),
> This could use a bit more explanation in a comment - is it because there is no plausible type that can take a FORMAT and be cast to itself?
>

We might be able to do it in the future; it's currently not allowed.
I have used the ERRCODE_FEATURE_NOT_SUPPORTED error code.

We should not use
``> +       if (inputBaseTypeId == targetBaseTypeId)``
We need to use IsBinaryCoercible.

src1=# SELECT CAST('52'::int2 as numeric FORMAT '9 9 9 9 9 9 . 9 9');
ERROR:  function pg_catalog.to_number(smallint, text) does not exist
DETAIL:  No function of that name accepts the given argument types.
HINT:  You might need to add explicit type casts.

The above CAST FORMAT error message is not ideal, therefore we need
stricter type restrictions for source and target data types,
and more type checking.
The target type must exactly match the function's result type.
to_char, to_date, to_number, to_timestamp.

> In situations like this, the committers have shown a strong preference for switch() statements. Though it may make more sense to package this if into a static function is_formattable_type() or similar.
>

we need more restriction on CAST FORMAT source type and target type.
pg_type->typcategory is not reliable for disallowing source/target types,
So, I use switch() to enumerate all allowed data types and the
switch() default branch handles ereport(ERROR).

--
jian
https://www.enterprisedb.com/
From 28c00853b0fa76d08a9d0503eeccaa94feb6e2f3 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Mon, 30 Mar 2026 16:00:00 +0800
Subject: [PATCH v6 1/1] CAST(expr AS type FORMAT 'template')

This enables the CAST(expression AS type FORMAT template) syntax.

For Binary-coercible casts: Specifying a format template for binary-coercible
casts (e.g., text to text) will now raise an error.

No pg_cast entries have been modified at this stage. Adding these
formatting functions to pg_cast has complex implications requiring further
discussion (see [1]) and is not strictly necessary to achieve the current
behavior.

Under the hood, CAST FORMAT is transformed into a FuncExpr node.  Since only
function to_char, to_date, to_number, and to_timestamp support formatting, this
feature is currently limited to the input and result types compatible with these
functions.

[1]: https://postgr.es/m/CACJufxF4OW=[email-masked]
context: https://wiki.postgresql.org/wiki/PostgreSQL_vs_SQL_Standard#Major_features_simply_not_implemented_yet
discussion: https://postgr.es/m/CACJufxGqm7cYQ5C65Eoh1z-f+aMdhv9_7V=[email-masked]
commitfest: https://commitfest.postgresql.org/patch/5957
---
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     |  19 +
 src/backend/parser/parse_agg.c                |  11 +
 src/backend/parser/parse_coerce.c             | 326 +++++++++++++++++-
 src/backend/parser/parse_expr.c               |  26 +-
 src/backend/parser/parse_func.c               |   3 +
 src/backend/parser/parse_utilcmd.c            |   1 +
 src/backend/utils/adt/ruleutils.c             |  20 ++
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/parse_coerce.h             |  11 +
 src/include/parser/parse_node.h               |   1 +
 src/test/regress/expected/cast.out            | 312 +++++++++++++++++
 .../regress/expected/collate.linux.utf8.out   |  39 +++
 src/test/regress/expected/horology.out        | 130 +++++++
 src/test/regress/expected/interval.out        |  12 +
 src/test/regress/expected/numeric.out         | 147 +++++++-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/cast.sql                 | 107 ++++++
 src/test/regress/sql/collate.linux.utf8.sql   |   8 +
 src/test/regress/sql/horology.sql             |  42 +++
 src/test/regress/sql/interval.sql             |   2 +
 src/test/regress/sql/numeric.sql              |  26 +-
 22 files changed, 1234 insertions(+), 14 deletions(-)
 create mode 100644 src/test/regress/expected/cast.out
 create mode 100644 src/test/regress/sql/cast.sql

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 6a850349cf7..2e33c92345d 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4502,6 +4502,8 @@ raw_expression_tree_walker_impl(Node *node,
 
 				if (WALK(tc->arg))
 					return true;
+				if (WALK(tc->format))
+					return true;
 				if (WALK(tc->typeName))
 					return true;
 			}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0fea726cdd5..e3d042cd8ae 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -157,6 +157,9 @@ static RawStmt *makeRawStmt(Node *stmt, int stmt_location);
 static void updateRawStmtEnd(RawStmt *rs, int end_location);
 static Node *makeColumnRef(char *colname, List *indirection,
 						   int location, core_yyscan_t yyscanner);
+static Node *makeFormattedTypeCast(Node *arg, TypeName *typename,
+								   Node *format,
+								   int location);
 static Node *makeTypeCast(Node *arg, TypeName *typename, int location);
 static Node *makeStringConstCast(char *str, int location, TypeName *typename);
 static Node *makeIntConst(int val, int location);
@@ -16687,6 +16690,8 @@ func_expr_common_subexpr:
 				}
 			| CAST '(' a_expr AS Typename ')'
 				{ $$ = makeTypeCast($3, $5, @1); }
+			| CAST '(' a_expr AS Typename FORMAT a_expr ')'
+				{ $$ = makeFormattedTypeCast($3, $5, $7, @1); }
 			| EXTRACT '(' extract_list ')'
 				{
 					$$ = (Node *) makeFuncCall(SystemFuncName("extract"),
@@ -19841,12 +19846,26 @@ makeColumnRef(char *colname, List *indirection,
 	return (Node *) c;
 }
 
+static Node *
+makeFormattedTypeCast(Node *arg, TypeName *typename, Node *format, int location)
+{
 
 	n->arg = arg;
+	n->format = NULL;
 	n->typeName = typename;
 	n->location = location;
 	return (Node *) n;
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 6076e9373c1..5beec4b4518 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -593,6 +593,14 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 
 			break;
 
+		case EXPR_KIND_TYPECAST_FORMAT:
+			if (isAgg)
+				err = _("aggregate functions are not allowed in CAST FORMAT expressions");
+			else
+				err = _("grouping operations are not allowed in CAST FORMAT expressions");
+
+			break;
+
 			/*
 			 * There is intentionally no default: case here, so that the
 			 * compiler will warn if we add a new ParseExprKind without
@@ -1035,6 +1043,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			err = _("window functions are not allowed in property definition expressions");
 			break;
+		case EXPR_KIND_TYPECAST_FORMAT:
+			err = _("window functions are not allowed in CAST FORMAT expressions");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index 913ca53666f..02e8dda993a 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -23,6 +23,7 @@
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_coerce.h"
+#include "parser/parse_func.h"
 #include "parser/parse_relation.h"
 #include "parser/parse_type.h"
 #include "utils/builtins.h"
@@ -74,6 +75,8 @@ static bool typeIsOfTypedTable(Oid reltypeId, Oid reloftypeId);
  * targettypmod - desired result typmod
  * ccontext, cformat - context indicators to control coercions
  * location - parse location of the coercion request, or -1 if unknown/implicit
+ * format - cast format template expression, this typically is NULL, but it's
+ * for CAST(expr as type FORMAT 'format_expr') construct.
  */
 Node *
 coerce_to_target_type(ParseState *pstate, Node *expr, Oid exprtype,
@@ -81,6 +84,22 @@ coerce_to_target_type(ParseState *pstate, Node *expr, Oid exprtype,
 					  CoercionContext ccontext,
 					  CoercionForm cformat,
 					  int location)
+{
+	return coerce_to_target_type_extended(pstate, expr, exprtype,
+										  targettype, targettypmod,
+										  ccontext,
+										  cformat,
+										  location,
+										  NULL);
+}
+
+Node *
+coerce_to_target_type_extended(ParseState *pstate, Node *expr, Oid exprtype,
+							   Oid targettype, int32 targettypmod,
+							   CoercionContext ccontext,
+							   CoercionForm cformat,
+							   int location,
+							   Node *format)
 {
 	Node	   *result;
 	Node	   *origexpr;
@@ -102,9 +121,10 @@ coerce_to_target_type(ParseState *pstate, Node *expr, Oid exprtype,
 	while (expr && IsA(expr, CollateExpr))
 		expr = (Node *) ((CollateExpr *) expr)->arg;
 
-	result = coerce_type(pstate, expr, exprtype,
-						 targettype, targettypmod,
-						 ccontext, cformat, location);
+	result = coerce_type_extended(pstate, expr, exprtype,
+								  targettype, targettypmod,
+								  ccontext, cformat, location,
+								  format);
 
 	/*
 	 * If the target is a fixed-length type, it may need a length coercion as
@@ -131,6 +151,243 @@ coerce_to_target_type(ParseState *pstate, Node *expr, Oid exprtype,
 	return result;
 }
 
+ /*
+  * coerce_type_with_format()
+  *
+  * Coerce the CAST(expr AS type FORMAT 'fmt') construct to the target type.
+  *
+  * This is a subroutine of coerce_type_extended. The caller must ensure that
+  * the coercion is possible via can_coerce_type.
+  *
+  * We cannot simply construct a FuncCall node and rely on transformFuncCall,
+  * because the source expression and format template have already been
+  * transformed. Invoking transformExprRecurse again would be incorrect.
+  * Instead, we construct a FuncCall node and let ParseFuncOrColumn produce
+  * the final FuncExpr node.
+  */
+static Node *
+coerce_type_with_format(ParseState *pstate, Node *node, Node *fmt,
+						Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
+						CoercionContext ccontext, CoercionForm cformat, int location)
+{
+	Node	   *format;
+	Node	   *funcexpr;
+	FuncCall   *fn;
+	List	   *funcname = NIL;
+	List	   *args = NIL;
+	TYPCATEGORY s_typcategory;
+	TYPCATEGORY t_typcategory;
+
+	int32		targetBaseTypeMod = targetTypeMod;
+	Oid			targetBaseTypeId = getBaseTypeAndTypmod(targetTypeId,
+														&targetBaseTypeMod);
+	Oid			inputBaseTypeId = getBaseType(inputTypeId);
+	Oid			formatBaseTypeId = getBaseType(exprType(fmt));
+	TYPCATEGORY fmtcategory = TypeCategory(formatBaseTypeId);
+	Node	   *arg = node;
+
+	if (fmtcategory != TYPCATEGORY_STRING && fmtcategory != TYPCATEGORY_UNKNOWN)
+		ereport(ERROR,
+				errcode(ERRCODE_CANNOT_COERCE),
+				errmsg("CAST FORMAT expression is not of type text"),
+				parser_errposition(pstate, exprLocation(fmt)));
+
+	s_typcategory = TypeCategory(inputBaseTypeId);
+	t_typcategory = TypeCategory(targetBaseTypeId);
+
+	/*
+	 * For CAST FORMAT, we explicitly coerce the source expression to TEXT and
+	 * later pass it to FuncCall node, then ParseFuncOrColumn can use it.
+	 */
+	if (IsA(node, Const) && inputTypeId == UNKNOWNOID)
+	{
+		Const	   *con = (Const *) node;
+		Const	   *newcon = makeNode(Const);
+		Type		textType = typeidType(TEXTOID);
+
+		newcon->consttype = TEXTOID;
+		newcon->consttypmod = -1;
+		newcon->constcollid = typeTypeCollation(textType);
+		newcon->constlen = typeLen(textType);
+		newcon->constbyval = typeByVal(textType);
+		newcon->constisnull = con->constisnull;
+		newcon->location = exprLocation(node);
+
+		newcon->constvalue = stringTypeDatum(textType,
+											 DatumGetCString(con->constvalue),
+											 -1);
+		if (!newcon->constisnull && newcon->constlen == -1)
+			newcon->constvalue =
+				PointerGetDatum(PG_DETOAST_DATUM(newcon->constvalue));
+
+		ReleaseSysCache(textType);
+
+		arg = (Node *) newcon;
+		inputBaseTypeId = TEXTOID;
+		inputTypeId = TEXTOID;
+	}
+
+	/*
+	 * It's not allowed to take a FORMAT and be cast to itself. This may
+	 * change in the future.
+	 */
+	if (IsBinaryCoercible(inputBaseTypeId, targetBaseTypeId))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot cast type %s to %s while using a format template",
+					   format_type_be(inputBaseTypeId),
+					   format_type_be(targetBaseTypeId)),
+				errdetail("binary coercible type cast is not supported while using a format template"),
+				parser_coercion_errposition(pstate, location, node));
+
+	switch (inputBaseTypeId)
+	{
+		case INT2OID:
+		case INT4OID:
+		case INT8OID:
+		case FLOAT4OID:
+		case FLOAT8OID:
+		case NUMERICOID:
+		case DATEOID:
+		case TIMEOID:
+		case TIMESTAMPOID:
+		case TIMESTAMPTZOID:
+		case INTERVALOID:
+		case NAMEOID:
+		case TEXTOID:
+		case BPCHAROID:
+		case VARCHAROID:
+			break;
+		default:
+
+			/*
+			 * TODO: We should ideally avoid erroring out if inputBaseTypeId
+			 * is binary-coercible to the above types, but iterating
+			 * IsBinaryCoercible() for each type is too expensive.
+			 */
+
+			/*
+			 * FIXME: errhint is wrong, since timetz is a category of
+			 * datetime. But it's not allowed.
+			 */
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot cast type %s to %s using formatted template",
+						   format_type_be(inputTypeId),
+						   format_type_be(targetTypeId)),
+					errdetail("Only categories of numeric, string, datetime, and timespan source data types are supported for formatted type casting"),
+					parser_coercion_errposition(pstate, location, node));
+			break;
+	}
+
+	switch (targetBaseTypeId)
+	{
+		case NUMERICOID:
+		case TIMESTAMPTZOID:
+		case DATEOID:
+		case NAMEOID:
+		case TEXTOID:
+		case BPCHAROID:
+		case VARCHAROID:
+			break;
+		default:
+
+			/*
+			 * TODO: We should ideally avoid erroring out if targetBaseTypeId
+			 * is binary-coercible to the above types, but iterating
+			 * IsBinaryCoercible() for each type is too expensive.
+			 */
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot cast type %s to %s using formatted template",
+						   format_type_be(inputTypeId),
+						   format_type_be(targetTypeId)),
+					errhint("Only timestamptz, text, numeric and date data type are supported for formatted type casting"),
+					parser_coercion_errposition(pstate, location, node));
+			break;
+	}
+
+	/*
+	 * Internally, CAST FORMAT delegates to functions (e.g., to_char, to_date)
+	 * where the format string parameter is typed as TEXT. Consequently, the
+	 * FORMAT clause requires explicit coercion to TEXT.
+	 */
+	format = coerce_to_target_type(pstate, fmt,
+								   exprType(fmt), TEXTOID,
+								   exprTypmod(fmt),
+								   ccontext, cformat,
+								   exprLocation(fmt));
+
+
+	/*
+	 * For erroring out case like: CAST(NULL::int2 as numeric FORMAT '9');
+	 *
+	 * This is a necessary hack! The code above only checks the source and
+	 * target types individually, rather than validating their combination.
+	 * This works because all these formatting functions (to_char, to_date,
+	 * to_number, to_timestamp) have distinct type categories for their inputs
+	 * versus their outputs.
+	 */
+	if (s_typcategory == t_typcategory)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot cast type %s to %s using formatted template",
+					   format_type_be(inputTypeId),
+					   format_type_be(targetTypeId)));
+
+	switch (targetBaseTypeId)
+	{
+		case DATEOID:
+			funcname = list_make2(makeString("pg_catalog"),
+								  makeString("to_date"));
+			break;
+		case NUMERICOID:
+			funcname = list_make2(makeString("pg_catalog"),
+								  makeString("to_number"));
+			break;
+		case TIMESTAMPTZOID:
+			funcname = list_make2(makeString("pg_catalog"),
+								  makeString("to_timestamp"));
+			break;
+		case NAMEOID:
+		case TEXTOID:
+		case BPCHAROID:
+		case VARCHAROID:
+			funcname = list_make2(makeString("pg_catalog"),
+								  makeString("to_char"));
+			break;
+		default:
+			elog(ERROR, "failed to find conversion function from %s to %s while using a format template",
+				 format_type_be(inputTypeId),
+				 format_type_be(targetTypeId));
+			break;
+	}
+
+	args = list_make2(arg, format);
+	fn = makeFuncCall(funcname, args, COERCE_SQL_SYNTAX, -1);
+
+	funcexpr = ParseFuncOrColumn(pstate,
+								 fn->funcname,
+								 fn->args,
+								 NULL,
+								 fn,
+								 false,
+								 fn->location);
+
+	/*
+	 * For CAST FORMAT, we do not enforce the exact source type, allowing
+	 * certain categories of types instead. Therefore, the produced FuncExpr
+	 * must be coerced to the exact target type. This is also necessary
+	 * because the target type might be a domain.
+	 */
+	return coerce_to_target_type(pstate,
+								 funcexpr, exprType(funcexpr),
+								 targetTypeId, targetTypeMod,
+								 ccontext,
+								 cformat,
+								 location);
+}
+
 
 /*
  * coerce_type()
@@ -158,6 +415,18 @@ Node *
 coerce_type(ParseState *pstate, Node *node,
 			Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
 			CoercionContext ccontext, CoercionForm cformat, int location)
+{
+	return coerce_type_extended(pstate, node,
+								inputTypeId, targetTypeId, targetTypeMod,
+								ccontext, cformat, location,
+								NULL);
+}
+
+Node *
+coerce_type_extended(ParseState *pstate, Node *node,
+					 Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
+					 CoercionContext ccontext, CoercionForm cformat, int location,
+					 Node *fmt)
 {
 	Node	   *result;
 	CoercionPathType pathtype;
@@ -166,6 +435,22 @@ coerce_type(ParseState *pstate, Node *node,
 	if (targetTypeId == inputTypeId ||
 		node == NULL)
 	{
+		if (fmt != NULL)
+		{
+			if (inputTypeId == UNKNOWNOID)
+				inputTypeId = TEXTOID;
+
+			if (targetTypeId == UNKNOWNOID)
+				targetTypeId = TEXTOID;
+
+			ereport(ERROR,
+					errcode(ERRCODE_CANNOT_COERCE),
+					errmsg("cannot cast type %s to %s while using a format template",
+						   format_type_be(inputTypeId),
+						   format_type_be(targetTypeId)),
+					errdetail("binary coercible type cast is not supported while using a format template"));
+		}
+
 		/* no conversion needed */
 		return node;
 	}
@@ -175,6 +460,18 @@ coerce_type(ParseState *pstate, Node *node,
 		targetTypeId == ANYCOMPATIBLEOID ||
 		targetTypeId == ANYCOMPATIBLENONARRAYOID)
 	{
+		if (fmt != NULL)
+		{
+			if (inputTypeId == UNKNOWNOID)
+				inputTypeId = TEXTOID;
+
+			ereport(ERROR,
+					errcode(ERRCODE_CANNOT_COERCE),
+					errmsg("cannot cast type %s to %s while using a format template",
+						   format_type_be(inputTypeId),
+						   format_type_be(targetTypeId)));
+		}
+
 		/*
 		 * Assume can_coerce_type verified that implicit coercion is okay.
 		 *
@@ -197,6 +494,18 @@ coerce_type(ParseState *pstate, Node *node,
 		targetTypeId == ANYCOMPATIBLERANGEOID ||
 		targetTypeId == ANYCOMPATIBLEMULTIRANGEOID)
 	{
+		if (fmt != NULL)
+		{
+			if (inputTypeId == UNKNOWNOID)
+				inputTypeId = TEXTOID;
+
+			ereport(ERROR,
+					errcode(ERRCODE_CANNOT_COERCE),
+					errmsg("cannot cast type %s to %s while using a format template",
+						   format_type_be(inputTypeId),
+						   format_type_be(targetTypeId)));
+		}
+
 		/*
 		 * Assume can_coerce_type verified that implicit coercion is okay.
 		 *
@@ -230,6 +539,17 @@ coerce_type(ParseState *pstate, Node *node,
 			return node;
 		}
 	}
+
+	if (fmt != NULL)
+	{
+		result = coerce_type_with_format(pstate, node, fmt,
+										 inputTypeId,
+										 targetTypeId, targetTypeMod,
+										 ccontext, cformat,
+										 location);
+		return result;
+	}
+
 	if (inputTypeId == UNKNOWNOID && IsA(node, Const))
 	{
 		/*
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 312dfdc182a..9156f149343 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -579,6 +579,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_GENERATED_COLUMN:
 		case EXPR_KIND_CYCLE_MARK:
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
+		case EXPR_KIND_TYPECAST_FORMAT:
 			/* okay */
 			break;
 
@@ -1880,6 +1881,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			err = _("cannot use subquery in property definition expression");
 			break;
+		case EXPR_KIND_TYPECAST_FORMAT:
+			err = _("cannot use subquery in CAST FORMAT expression");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -2726,6 +2730,7 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
 	Node	   *result;
 	Node	   *arg = tc->arg;
 	Node	   *expr;
+	Node	   *format = NULL;
 	Oid			inputType;
 	Oid			targetType;
 	int32		targetTypmod;
@@ -2747,6 +2752,12 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
 		int32		targetBaseTypmod;
 		Oid			elementType;
 
+		if (tc->format)
+			ereport(ERROR,
+					errcode(ERRCODE_CANNOT_COERCE),
+					errmsg("formatted type cast is not supported for array type"),
+					parser_coercion_errposition(pstate, exprLocation(arg), arg));
+
 		/*
 		 * If target is a domain over array, work with the base array type
 		 * here.  Below, we'll cast the array type to the domain.  In the
@@ -2774,6 +2785,8 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
 	if (inputType == InvalidOid)
 		return expr;			/* do nothing if NULL input */
 
+	format = transformExpr(pstate, tc->format, EXPR_KIND_TYPECAST_FORMAT);
+
 	/*
 	 * Location of the coercion is preferentially the location of the :: or
 	 * CAST symbol, but if there is none then use the location of the type
@@ -2783,11 +2796,12 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
 	if (location < 0)
 		location = tc->typeName->location;
 
-	result = coerce_to_target_type(pstate, expr, inputType,
-								   targetType, targetTypmod,
-								   COERCION_EXPLICIT,
-								   COERCE_EXPLICIT_CAST,
-								   location);
+	result = coerce_to_target_type_extended(pstate, expr, inputType,
+											targetType, targetTypmod,
+											COERCION_EXPLICIT,
+											COERCE_EXPLICIT_CAST,
+											location,
+											format);
 	if (result == NULL)
 		ereport(ERROR,
 				(errcode(ERRCODE_CANNOT_COERCE),
@@ -3241,6 +3255,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "CYCLE";
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			return "property definition expression";
+		case EXPR_KIND_TYPECAST_FORMAT:
+			return "CAST FORMAT expression";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 8dbd41a3548..614ab69b239 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2786,6 +2786,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			err = _("set-returning functions are not allowed in property definition expressions");
 			break;
+		case EXPR_KIND_TYPECAST_FORMAT:
+			err = _("set-returning functions are not allowed in CAST FORMAT expressions");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 9a918e14aa7..6157490e5ef 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -686,6 +686,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 		castnode = makeNode(TypeCast);
 		castnode->typeName = SystemTypeName("regclass");
 		castnode->arg = (Node *) snamenode;
+		castnode->format = NULL;
 		castnode->location = -1;
 		funccallnode = makeFuncCall(SystemFuncName("nextval"),
 									list_make1(castnode),
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 7bc12589e40..a31c55f5744 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -11972,6 +11972,26 @@ get_func_sql_syntax(FuncExpr *expr, deparse_context *context)
 			get_rule_expr((Node *) lsecond(expr->args), context, false);
 			appendStringInfoString(buf, "))");
 			return true;
+		case F_TO_CHAR_INT4_TEXT:
+		case F_TO_CHAR_INT8_TEXT:
+		case F_TO_CHAR_FLOAT4_TEXT:
+		case F_TO_CHAR_FLOAT8_TEXT:
+		case F_TO_CHAR_NUMERIC_TEXT:
+		case F_TO_CHAR_INTERVAL_TEXT:
+		case F_TO_CHAR_TIMESTAMP_TEXT:
+		case F_TO_CHAR_TIMESTAMPTZ_TEXT:
+		case F_TO_NUMBER:
+		case F_TO_TIMESTAMP_TEXT_TEXT:
+		case F_TO_DATE:
+			/* CAST FORMAT */
+			appendStringInfoString(buf, "CAST( ");
+			get_rule_expr((Node *) linitial(expr->args), context, false);
+			appendStringInfoString(buf, " AS ");
+			appendStringInfoString(buf, format_type_be(expr->funcresulttype));
+			appendStringInfoString(buf, " FORMAT ");
+			get_rule_expr((Node *) lsecond(expr->args), context, false);
+			appendStringInfoChar(buf, ')');
+			return true;
 	}
 	return false;
 }
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index df431220ac5..b79515a9b98 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -397,6 +397,7 @@ typedef struct TypeCast
 	NodeTag		type;
 	Node	   *arg;			/* the expression being casted */
 	TypeName   *typeName;		/* the target type */
+	Node	   *format;			/* the cast format template expression */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } TypeCast;
 
diff --git a/src/include/parser/parse_coerce.h b/src/include/parser/parse_coerce.h
index aabacd49b65..147c15c8e5a 100644
--- a/src/include/parser/parse_coerce.h
+++ b/src/include/parser/parse_coerce.h
@@ -43,11 +43,22 @@ extern Node *coerce_to_target_type(ParseState *pstate,
 								   CoercionContext ccontext,
 								   CoercionForm cformat,
 								   int location);
+extern Node *coerce_to_target_type_extended(ParseState *pstate,
+											Node *expr, Oid exprtype,
+											Oid targettype, int32 targettypmod,
+											CoercionContext ccontext,
+											CoercionForm cformat,
+											int location,
+											Node *format);
 extern bool can_coerce_type(int nargs, const Oid *input_typeids, const Oid *target_typeids,
 							CoercionContext ccontext);
 extern Node *coerce_type(ParseState *pstate, Node *node,
 						 Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
 						 CoercionContext ccontext, CoercionForm cformat, int location);
+extern Node *coerce_type_extended(ParseState *pstate, Node *node,
+								  Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
+								  CoercionContext ccontext, CoercionForm cformat, int location,
+								  Node *format);
 extern Node *coerce_to_domain(Node *arg, Oid baseTypeId, int32 baseTypeMod,
 							  Oid typeId,
 							  CoercionContext ccontext, CoercionForm cformat, int location,
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index fc2cbeb2083..a2b044d6583 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -83,6 +83,7 @@ typedef enum ParseExprKind
 	EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */
 	EXPR_KIND_CYCLE_MARK,		/* cycle mark value */
 	EXPR_KIND_PROPGRAPH_PROPERTY,	/* derived property expression */
+	EXPR_KIND_TYPECAST_FORMAT,	/* CAST FORMAT */
 } ParseExprKind;

diff --git a/src/test/regress/expected/cast.out b/src/test/regress/expected/cast.out
new file mode 100644
index 00000000000..c15a1828291
--- /dev/null
+++ b/src/test/regress/expected/cast.out
@@ -0,0 +1,312 @@
+create function ret_settxt() returns setof text as
+$$
+begin
+    return query execute 'select 1 union all select 1';
+end;
+$$
+language plpgsql immutable;
+-- check CAST FORMAT expression, the following should all fail
+select cast(NULL as date format ret_settxt()); -- cannot return a set
+ERROR:  set-returning functions are not allowed in CAST FORMAT expressions
+LINE 1: select cast(NULL as date format ret_settxt());
+                                        ^
+select cast(NULL as date format (select 1::text where false));
+ERROR:  cannot use subquery in CAST FORMAT expression
+LINE 1: select cast(NULL as date format (select 1::text where false)...
+                                        ^
+select cast(NULL as date format (string_agg(NULL, ' ')));
+ERROR:  aggregate functions are not allowed in CAST FORMAT expressions
+LINE 1: select cast(NULL as date format (string_agg(NULL, ' ')));
+                                         ^
+select cast(NULL as date format (string_agg(NULL, ' ') over () ));
+ERROR:  window functions are not allowed in CAST FORMAT expressions
+LINE 1: select cast(NULL as date format (string_agg(NULL, ' ') over ...
+                                         ^
+select cast(NULL as date format NULL::int);
+ERROR:  CAST FORMAT expression is not of type text
+LINE 1: select cast(NULL as date format NULL::int);
+                                        ^
+select cast('1' as date format B'01');
+ERROR:  CAST FORMAT expression is not of type text
+LINE 1: select cast('1' as date format B'01');
+                                       ^
+-- CAST FORMAT is restricted to the source and target types used by to_char, to_date, to_timestamp, and to_number.
+-- The following should all fail
+select cast('-34,338,492' as bigint format '99G999G999');
+ERROR:  cannot cast type text to bigint using formatted template
+LINE 1: select cast('-34,338,492' as bigint format '99G999G999');
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+select cast(array[1] as text format 'YYYY');
+ERROR:  formatted type cast is not supported for array type
+LINE 1: select cast(array[1] as text format 'YYYY');
+                    ^
+select cast('1' as timestamp[] format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to timestamp without time zone[] using formatted template
+LINE 1: select cast('1' as timestamp[] format 'YYYY-MM-DD');
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+select cast('2012-13-12' as timestamp format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to timestamp without time zone using formatted template
+LINE 1: select cast('2012-13-12' as timestamp format 'YYYY-MM-DD');
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+select cast('2012-13-12' as time format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to time without time zone using formatted template
+LINE 1: select cast('2012-13-12' as time format 'YYYY-MM-DD');
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+select cast('2012-13-12' as timetz format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to time with time zone using formatted template
+LINE 1: select cast('2012-13-12' as timetz format 'YYYY-MM-DD');
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+select cast('2012-13-12' as interval format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to interval using formatted template
+LINE 1: select cast('2012-13-12' as interval format 'YYYY-MM-DD');
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+select cast('1'::text as unknown format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to unknown using formatted template
+LINE 1: select cast('1'::text as unknown format 'YYYY-MM-DD');
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+select cast('1' as bool format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to boolean using formatted template
+LINE 1: select cast('1' as bool format 'YYYY-MM-DD');
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+select cast('1' as json format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to json using formatted template
+LINE 1: select cast('1' as json format 'YYYY-MM-DD');
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+select cast('1'::json as text format 'YYYY-MM-DD');
+ERROR:  cannot cast type json to text using formatted template
+LINE 1: select cast('1'::json as text format 'YYYY-MM-DD');
+               ^
+DETAIL:  Only categories of numeric, string, datetime, and timespan source data types are supported for formatted type casting
+select cast('1' as anyelement format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to anyelement while using a format template
+select cast('1' as anyenum format 'YYYY-MM-DD');
+ERROR:  cannot cast type unknown to anyenum
+LINE 1: select cast('1' as anyenum format 'YYYY-MM-DD');
+               ^
+select cast('1' as anyarray format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to anyarray while using a format template
+select cast(null::anyelement as anyelement format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to anyelement while using a format template
+select cast('2012-12-12 12:00'::timetz as text format 'YYYY-MM-DD HH:MI:SS TZ');
+ERROR:  cannot cast type time with time zone to text using formatted template
+LINE 1: select cast('2012-12-12 12:00'::timetz as text format 'YYYY-...
+               ^
+DETAIL:  Only categories of numeric, string, datetime, and timespan source data types are supported for formatted type casting
+select cast(null::regclass as text format 'YYYY-MM-DD HH:MI:SS TZ');
+ERROR:  cannot cast type regclass to text using formatted template
+LINE 1: select cast(null::regclass as text format 'YYYY-MM-DD HH:MI:...
+               ^
+DETAIL:  Only categories of numeric, string, datetime, and timespan source data types are supported for formatted type casting
+select cast(null::int2 as numeric format null);
+ERROR:  cannot cast type smallint to numeric using formatted template
+select cast(null::date as timestamptz format null);
+ERROR:  cannot cast type date to timestamp with time zone using formatted template
+select cast(null::time as timestamptz format null);
+ERROR:  cannot cast type time without time zone to timestamp with time zone
+LINE 1: select cast(null::time as timestamptz format null);
+               ^
+-- CAST FORMAT is not supported for binary coercible type cast
+select cast('2022-01-01' as unknown format null);
+ERROR:  cannot cast type text to text while using a format template
+DETAIL:  binary coercible type cast is not supported while using a format template
+select cast('1' as text format '1'::text);
+ERROR:  cannot cast type text to text while using a format template
+LINE 1: select cast('1' as text format '1'::text);
+               ^
+DETAIL:  binary coercible type cast is not supported while using a format template
+select cast('1'::text as text format '1'::text);
+ERROR:  cannot cast type text to text while using a format template
+DETAIL:  binary coercible type cast is not supported while using a format template
+select cast('2012-12-12 12:00'::timetz as text format 'YYYY-MM-DD HH:MI:SS TZ'); -- error
+ERROR:  cannot cast type time with time zone to text using formatted template
+LINE 1: select cast('2012-12-12 12:00'::timetz as text format 'YYYY-...
+               ^
+DETAIL:  Only categories of numeric, string, datetime, and timespan source data types are supported for formatted type casting
+select cast('2012-13-12' as date format 'YYYY-DD-MM'); -- ok
+    date    
+------------
+ 12-13-2012
+(1 row)
+
+select cast('2012-13-12' as date format 'YYYY-MM-DD'); -- error
+ERROR:  date/time field value out of range: "2012-13-12"
+select cast('1' as date format 'YYYY-MM-DD');
+    date    
+------------
+ 01-01-0001
+(1 row)
+
+select cast('1' collate "C" as date format 'YYYY-MM-DD');
+    date    
+------------
+ 01-01-0001
+(1 row)
+
+select cast('2012-13-12' as date format 'YYYY-DD-MM') as date;
+    date    
+------------
+ 12-13-2012
+(1 row)
+
+select cast('2012-13-12' as timestamptz format 'YYYY-DD-MM') as date;
+             date             
+------------------------------
+ Thu Dec 13 00:00:00 2012 PST
+(1 row)
+
+select cast('2012-13-12'::text as timestamp format 'YYYY-DD-MM') as date; -- error
+ERROR:  cannot cast type text to timestamp without time zone using formatted template
+LINE 1: select cast('2012-13-12'::text as timestamp format 'YYYY-DD-...
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+select cast('1' as timestamp format 'YYYY-MM-DD') = to_timestamp('1', 'YYYY-MM-DD');
+ERROR:  cannot cast type text to timestamp without time zone using formatted template
+LINE 1: select cast('1' as timestamp format 'YYYY-MM-DD') = to_times...
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+select cast('2026-01-28 13:29:12.324606+01'::text as timestamp format 'YYYY-MM-DD') =
+            to_timestamp('2026-01-28 13:29:12.324606+01'::text, 'YYYY-MM-DD');
+ERROR:  cannot cast type text to timestamp without time zone using formatted template
+LINE 1: select cast('2026-01-28 13:29:12.324606+01'::text as timesta...
+               ^
+HINT:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+select cast('1' as date format 'YYYY-MM-DD') = to_date('1', 'YYYY-MM-DD');
+ ?column? 
+----------
+ t
+(1 row)
+
+-- test with domain
+create domain d1 as date check (value <> '0001-01-01');
+select cast('1' as text format 'YYYY-MM-DD'); -- error
+ERROR:  cannot cast type text to text while using a format template
+LINE 1: select cast('1' as text format 'YYYY-MM-DD');
+               ^
+DETAIL:  binary coercible type cast is not supported while using a format template
+select cast('1' as d1 format 'YYYY-MM-DD'); -- error
+ERROR:  value for domain d1 violates check constraint "d1_check"
+select cast('1' as date format 'MM-DD'); -- ok
+     date      
+---------------
+ 01-01-0001 BC
+(1 row)
+
+select cast('1' as d1 format 'MM-DD'); -- ok
+      d1       
+---------------
+ 01-01-0001 BC
+(1 row)
+
+select cast('1'::text collate "C" as date format 'YYYY-MM-DD');
+    date    
+------------
+ 01-01-0001
+(1 row)
+
+select cast('1' as date format 'YYYY-MM-DD') = to_date('1', 'YYYY-MM-DD') as expect_true;
+ expect_true 
+-------------
+ t
+(1 row)
+
+create table tcast(col1 text, col2 text, col3 date, col4 timestamptz, col5 int8);
+insert into tcast(col1, col2, col5)
+  values('2022-12-13', 'YYYY-MM-DD', 1234),
+        ('2022-12-01', 'YYYY-DD-MM', -1234);
+select cast(col1 as date format col2) from tcast;
+    col1    
+------------
+ 12-13-2022
+ 01-12-2022
+(2 rows)
+
+select cast(col1 as date format col3) from tcast; -- error
+ERROR:  CAST FORMAT expression is not of type text
+LINE 1: select cast(col1 as date format col3) from tcast;
+                                        ^
+select cast(col1 as date format col3::text) from tcast; -- ok
+ col1 
+------
+ 
+ 
+(2 rows)
+
+create function imm_const() returns text as $$ begin return 'YYYY-MM-DD'; end; $$ language plpgsql immutable;
+select cast(col1 as date format imm_const()) from tcast;
+    col1    
+------------
+ 12-13-2022
+ 12-01-2022
+(2 rows)
+
+create index s1 on tcast(to_date(col1, 'YYYY-MM-DD')); -- error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index s1 on tcast(to_date(col1, 'YYYY-MM-DD'));
+                                 ^
+create index s1 on tcast(cast(col1 as date format 'YYYY-MM-DD')); -- error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index s1 on tcast(cast(col1 as date format 'YYYY-MM-D...
+                                 ^
+create view tcast_v1 as
+   select cast(col1 as date format 'YYYY-MM-DD') as to_date,
+         cast(col1 as timestamptz format 'YYYY-MM-DD') as to_timestamptz,
+         cast(NULL::interval as text format 'YYYY-MM-DD') as to_txt0,
+         cast(col1::timestamp as text format 'YYYY-MM-DD') as to_txt1,
+         cast(col3 as text format 'YYYY-MM-DD') as to_txt2,
+         cast(col4 as text format 'YYYY-MM-DD') as to_txt3,
+         cast(numeric 'inf' as text format 'YYYY-MM-DD') as to_txt4,
+         cast(bigint '12324' as text format 'YYYY-MM-DD') as to_txt5
+   from tcast;
+select pg_get_viewdef('tcast_v1', true);
+                                       pg_get_viewdef                                       
+--------------------------------------------------------------------------------------------
+  SELECT CAST( col1 AS date FORMAT 'YYYY-MM-DD'::text) AS to_date,                         +
+     CAST( col1 AS timestamp with time zone FORMAT 'YYYY-MM-DD'::text) AS to_timestamptz,  +
+     CAST( NULL::interval AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt0,                   +
+     CAST( col1::timestamp without time zone AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt1,+
+     CAST( col3 AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt2,                             +
+     CAST( col4 AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt3,                             +
+     CAST( 'Infinity'::numeric AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt4,              +
+     CAST( '12324'::bigint AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt5                   +
+    FROM tcast;
+(1 row)
+
+create view tcast_v2 as
+   select cast(col5 as text format '9.99EEEE') as to_txt0,
+         cast(col5::float8 as text format '9.99EEEE') as to_txt1,
+         cast(col5::float4 as text format '9.99EEEE') as to_txt2,
+         cast(col5::numeric as text format '9.99EEEE') as to_txt3,
+         cast(col5::int2 as text format '9.99EEEE') as to_txt4,
+         cast(col5::int4 as text format '9.99EEEE') as to_txt5
+   from tcast;
+select pg_get_viewdef('tcast_v2', true);
+                                pg_get_viewdef                                 
+-------------------------------------------------------------------------------
+  SELECT CAST( col5 AS text FORMAT '9.99EEEE'::text) AS to_txt0,              +
+     CAST( col5::double precision AS text FORMAT '9.99EEEE'::text) AS to_txt1,+
+     CAST( col5::real AS text FORMAT '9.99EEEE'::text) AS to_txt2,            +
+     CAST( col5::numeric AS text FORMAT '9.99EEEE'::text) AS to_txt3,         +
+     CAST( col5::smallint AS text FORMAT '9.99EEEE'::text) AS to_txt4,        +
+     CAST( col5::integer AS text FORMAT '9.99EEEE'::text) AS to_txt5          +
+    FROM tcast;
+(1 row)
+
+select * from tcast_v2;
+  to_txt0  |  to_txt1  |  to_txt2  |  to_txt3  |  to_txt4  |  to_txt5  
+-----------+-----------+-----------+-----------+-----------+-----------
+  1.23e+03 |  1.23e+03 |  1.23e+03 |  1.23e+03 |  1.23e+03 |  1.23e+03
+ -1.23e+03 | -1.23e+03 | -1.23e+03 | -1.23e+03 | -1.23e+03 | -1.23e+03
+(2 rows)
+
+drop function ret_settxt;
+drop view tcast_v1, tcast_v2;
+drop table tcast;
diff --git a/src/test/regress/expected/collate.linux.utf8.out b/src/test/regress/expected/collate.linux.utf8.out
index c6e84c27b69..129130a4f1b 100644
--- a/src/test/regress/expected/collate.linux.utf8.out
+++ b/src/test/regress/expected/collate.linux.utf8.out
@@ -463,6 +463,30 @@ SELECT to_char(date '2010-04-01', 'DD TMMON YYYY' COLLATE "tr_TR");
  01 NİS 2010
 (1 row)
 
+SELECT CAST(date '2010-02-01' as text format 'DD TMMON YYYY');
+    text     
+-------------
+ 01 ŞUB 2010
+(1 row)
+
+SELECT CAST(date '2010-02-01' as text format 'DD TMMON YYYY' COLLATE "tr_TR");
+    text     
+-------------
+ 01 ŞUB 2010
+(1 row)
+
+SELECT CAST(date '2010-04-01' as text format 'DD TMMON YYYY');
+    text     
+-------------
+ 01 NIS 2010
+(1 row)
+
+SELECT CAST(date '2010-04-01' as text format  'DD TMMON YYYY' COLLATE "tr_TR");
+    text     
+-------------
+ 01 NİS 2010
+(1 row)
+
 -- to_date
 SELECT to_date('01 ŞUB 2010', 'DD TMMON YYYY');
   to_date   
@@ -479,6 +503,21 @@ SELECT to_date('01 Şub 2010', 'DD TMMON YYYY');
 SELECT to_date('1234567890ab 2010', 'TMMONTH YYYY'); -- fail
 ERROR:  invalid value "1234567890ab" for "MONTH"
 DETAIL:  The given value did not match any of the allowed values for this field.
+SELECT CAST('01 ŞUB 2010' as date format 'DD TMMON YYYY');
+    date    
+------------
+ 02-01-2010
+(1 row)
+
+SELECT CAST('01 ŞUB 2010' as date format 'DD TMMON YYYY'); -- ok
+    date    
+------------
+ 02-01-2010
+(1 row)
+
+SELECT CAST('1234567890ab 2010' as date format 'TMMONTH YYYY'); -- fail
+ERROR:  invalid value "1234567890ab" for "MONTH"
+DETAIL:  The given value did not match any of the allowed values for this field.
 -- backwards parsing
 CREATE VIEW collview1 AS SELECT * FROM collate_test1 WHERE b COLLATE "C" >= 'bbc';
 CREATE VIEW collview2 AS SELECT a, b FROM collate_test1 ORDER BY b COLLATE "C";
diff --git a/src/test/regress/expected/horology.out b/src/test/regress/expected/horology.out
index 32cf62b6741..95ca730113e 100644
--- a/src/test/regress/expected/horology.out
+++ b/src/test/regress/expected/horology.out
@@ -3324,72 +3324,144 @@ SELECT to_timestamp('2011-12-18 11:38 EST', 'YYYY-MM-DD HH12:MI TZ');
  Sun Dec 18 08:38:00 2011 PST
 (1 row)
 
+SELECT cast('2011-12-18 11:38 EST' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');
+         timestamptz          
+------------------------------
+ Sun Dec 18 08:38:00 2011 PST
+(1 row)
+
 SELECT to_timestamp('2011-12-18 11:38 -05', 'YYYY-MM-DD HH12:MI TZ');
          to_timestamp         
 ------------------------------
  Sun Dec 18 08:38:00 2011 PST
 (1 row)
 
+SELECT cast('2011-12-18 11:38 -05' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');
+         timestamptz          
+------------------------------
+ Sun Dec 18 08:38:00 2011 PST
+(1 row)
+
 SELECT to_timestamp('2011-12-18 11:38 +01:30', 'YYYY-MM-DD HH12:MI TZ');
          to_timestamp         
 ------------------------------
  Sun Dec 18 02:08:00 2011 PST
 (1 row)
 
+SELECT cast('2011-12-18 11:38 +01:30' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');
+         timestamptz          
+------------------------------
+ Sun Dec 18 02:08:00 2011 PST
+(1 row)
+
 SELECT to_timestamp('2011-12-18 11:38 MSK', 'YYYY-MM-DD HH12:MI TZ');  -- dyntz
          to_timestamp         
 ------------------------------
  Sat Dec 17 23:38:00 2011 PST
 (1 row)
 
+SELECT cast('2011-12-18 11:38 MSK' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');  -- dyntz
+         timestamptz          
 
  Sun Dec 18 08:38:24 2011 PST
 (1 row)
 
+SELECT cast('2011-12-18 11:38ESTFOO24' as timestamptz format 'YYYY-MM-DD HH12:MITZFOOSS');
+         timestamptz          
+------------------------------
+ Sun Dec 18 08:38:24 2011 PST
+(1 row)
+
 SELECT to_timestamp('2011-12-18 11:38-05FOO24', 'YYYY-MM-DD HH12:MITZFOOSS');
          to_timestamp         
 ------------------------------
  Sun Dec 18 08:38:24 2011 PST
 (1 row)
 
+SELECT cast('2011-12-18 11:38-05FOO24' as timestamptz format 'YYYY-MM-DD HH12:MITZFOOSS');
+         timestamptz          
+------------------------------
+ Sun Dec 18 08:38:24 2011 PST
+(1 row)
+
 SELECT to_timestamp('2011-12-18 11:38 JUNK', 'YYYY-MM-DD HH12:MI TZ');  -- error
 ERROR:  invalid value "JUNK" for "TZ"
 DETAIL:  Time zone abbreviation is not recognized.
+SELECT cast('2011-12-18 11:38 JUNK' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');  -- error
+ERROR:  invalid value "JUNK" for "TZ"
+DETAIL:  Time zone abbreviation is not recognized.
 SELECT to_timestamp('2011-12-18 11:38 ...', 'YYYY-MM-DD HH12:MI TZ');  -- error
 ERROR:  invalid value ".." for "TZ"
 DETAIL:  Value must be an integer.
+SELECT cast('2011-12-18 11:38 ...' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');  -- error
+ERROR:  invalid value ".." for "TZ"
+DETAIL:  Value must be an integer.
 SELECT to_timestamp('2011-12-18 11:38 -05', 'YYYY-MM-DD HH12:MI OF');
          to_timestamp         
 ------------------------------
  Sun Dec 18 08:38:00 2011 PST
 (1 row)
 
+SELECT cast ('2011-12-18 11:38 -05' as timestamptz format 'YYYY-MM-DD HH12:MI OF');
+         timestamptz          
+------------------------------
+ Sun Dec 18 08:38:00 2011 PST
+(1 row)
+
 SELECT to_timestamp('2011-12-18 11:38 +01:30', 'YYYY-MM-DD HH12:MI OF');
          to_timestamp         
 ------------------------------
  Sun Dec 18 02:08:00 2011 PST
 (1 row)
 
+SELECT cast('2011-12-18 11:38 +01:30' as timestamptz format 'YYYY-MM-DD HH12:MI OF');
+         timestamptz          
+------------------------------
+ Sun Dec 18 02:08:00 2011 PST
+(1 row)
+
 SELECT to_timestamp('2011-12-18 11:38 +xyz', 'YYYY-MM-DD HH12:MI OF');  -- error
 ERROR:  invalid value "xy" for "OF"
 DETAIL:  Value must be an integer.
+SELECT cast('2011-12-18 11:38 +xyz' as timestamptz format 'YYYY-MM-DD HH12:MI OF');  -- error
+ERROR:  invalid value "xy" for "OF"
+DETAIL:  Value must be an integer.
 SELECT to_timestamp('2011-12-18 11:38 +01:xyz', 'YYYY-MM-DD HH12:MI OF');  -- error
 ERROR:  invalid value "xy" for "OF"
 DETAIL:  Value must be an integer.
+SELECT cast('2011-12-18 11:38 +01:xyz' as timestamptz format 'YYYY-MM-DD HH12:MI OF');  -- error
+ERROR:  invalid value "xy" for "OF"
+DETAIL:  Value must be an integer.
 SELECT to_timestamp('2018-11-02 12:34:56.025', 'YYYY-MM-DD HH24:MI:SS.MS');
            to_timestamp           
 ----------------------------------
  Fri Nov 02 12:34:56.025 2018 PDT
 (1 row)
 
+SELECT cast('2018-11-02 12:34:56.025' as timestamptz format 'YYYY-MM-DD HH24:MI:SS.MS');
+           timestamptz            
+----------------------------------
+ Fri Nov 02 12:34:56.025 2018 PDT
+(1 row)
+
 SELECT i, to_timestamp('2018-11-02 12:34:56', 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
  i |         to_timestamp         
 ---+------------------------------
@@ -3469,6 +3541,8 @@ SELECT i, to_timestamp('2018-11-02 12:34:56.123456', 'YYYY-MM-DD HH24:MI:SS.FF'
 
 SELECT i, to_timestamp('2018-11-02 12:34:56.123456789', 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
 ERROR:  date/time field value out of range: "2018-11-02 12:34:56.123456789"
+SELECT i, cast('2018-11-02 12:34:56.123456789' as timestamptz format 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
+ERROR:  date/time field value out of range: "2018-11-02 12:34:56.123456789"
 SELECT i, to_timestamp('20181102123456123456', 'YYYYMMDDHH24MISSFF' || i) FROM generate_series(1, 6) i;
  i |            to_timestamp             
 ---+-------------------------------------
@@ -3486,18 +3560,36 @@ SELECT to_date('1 4 1902', 'Q MM YYYY');  -- Q is ignored
  04-01-1902
 (1 row)

 
+SELECT cast('2458872' as date format 'J');
+    date    
+------------
+ 01-23-2020
+(1 row)
+
 --
 -- Check handling of BC dates
 --
@@ -3833,12 +3925,24 @@ SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
  2012-12-12 12:00:00 PST
 (1 row)

@@ -3868,6 +3972,32 @@ SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
 
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD HH:MI:SS TZ'),
+        to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
+            text            |          to_char           
+----------------------------+----------------------------
+ 2012-12-12 12:00:00 -01:30 | 2012-12-12 12:00:00 -01:30
+(1 row)
+
+SELECT cast('2012-12-12 12:00'::date as text format 'YYYY-MM-DD HH:MI:SS TZ'),
+        to_char('2012-12-12 12:00'::date, 'YYYY-MM-DD HH:MI:SS TZ');
+            text            |          to_char           
+----------------------------+----------------------------
+ 2012-12-12 12:00:00 -01:30 | 2012-12-12 12:00:00 -01:30
+(1 row)
+
+SELECT cast('12:00'::time as text format 'HH:MI:SS'),
+        to_char('12:00'::time, 'HH:MI:SS');
+   text   | to_char  
+----------+----------
+ 12:00:00 | 12:00:00
+(1 row)
+
+SELECT cast('2012-12-12 12:00'::timetz as text format 'YYYY-MM-DD HH:MI:SS TZ');
+ERROR:  cannot cast type time with time zone to text using formatted template
+LINE 1: SELECT cast('2012-12-12 12:00'::timetz as text format 'YYYY-...
+               ^
+DETAIL:  Only categories of numeric, string, datetime, and timespan source data types are supported for formatted type casting
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD SSSS');
      to_char      
 ------------------
diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out
index a16e3ccdb2e..4bcaaaf04a7 100644
--- a/src/test/regress/expected/interval.out
+++ b/src/test/regress/expected/interval.out
@@ -2263,12 +2263,24 @@ SELECT to_char('infinity'::interval, 'YYYY');
  
 (1 row)
 
+SELECT cast('infinity'::interval as text format 'YYYY');
+ text 
+------
+ 
+(1 row)
+
 SELECT to_char('-infinity'::interval, 'YYYY');
  to_char 
 ---------
  
 (1 row)
 
+SELECT cast('-infinity'::interval as text format 'YYYY');
+ text 
+------
+ 
+(1 row)
+
 -- "ago" can only appear once at the end of an interval.
 SELECT INTERVAL '42 days 2 seconds ago ago';
 ERROR:  invalid input syntax for type interval: "42 days 2 seconds ago ago"
diff --git a/src/test/regress/expected/numeric.out b/src/test/regress/expected/numeric.out
index c58e232a263..09264b98bfb 100644
--- a/src/test/regress/expected/numeric.out
+++ b/src/test/regress/expected/numeric.out
@@ -2264,147 +2264,286 @@ SELECT to_number('-34,338,492', '99G999G999');
  -34338492
 (1 row)
 
+SELECT CAST('-34,338,492' as numeric FORMAT  '99G999G999');
+  numeric  
+-----------
+ -34338492
+(1 row)
+
 SELECT to_number('-34,338,492.654,878', '99G999G999D999G999');
     to_number     
 ------------------
  -34338492.654878
 (1 row)
 
+SELECT CAST('-34,338,492.654,878' as numeric FORMAT '99G999G999D999G999');
+     numeric      
+------------------
+ -34338492.654878
+(1 row)
+
 SELECT to_number('<564646.654564>', '999999.999999PR');
    to_number    
 ----------------
  -564646.654564
 (1 row)
 
+SELECT CAST('<564646.654564>' as numeric FORMAT '999999.999999PR');
+    numeric     
+----------------
+ -564646.654564
+(1 row)
+
 SELECT to_number('0.00001-', '9.999999S');
  to_number 
 -----------
   -0.00001
 (1 row)
 
+SELECT CAST('0.00001-' as numeric FORMAT '9.999999S');
+ numeric  
+----------
+ -0.00001
+(1 row)
+
 SELECT to_number('5.01-', 'FM9.999999S');
  to_number 
 -----------
      -5.01
 (1 row)
 
+SELECT CAST('5.01-' as numeric FORMAT 'FM9.999999S');
+ numeric 
+---------
+   -5.01
+(1 row)
+
 SELECT to_number('5.01-', 'FM9.999999MI');
  to_number 
 -----------
      -5.01
 (1 row)
 
+SELECT CAST('5.01-' as numeric FORMAT 'FM9.999999MI');
+ numeric 
+---------
+   -5.01
+(1 row)
+
 SELECT to_number('5 4 4 4 4 8 . 7 8', '9 9 9 9 9 9 . 9 9');
  to_number 
 -----------
  544448.78
 (1 row)
 
+SELECT CAST('5 4 4 4 4 8 . 7 8' as numeric FORMAT '9 9 9 9 9 9 . 9 9');
+  numeric  
+-----------
+ 544448.78
+(1 row)
+
 SELECT to_number('.01', 'FM9.99');
  to_number 
 -----------
       0.01
 (1 row)
 
+SELECT CAST('.01' as numeric FORMAT 'FM9.99');
+ numeric 
+---------
+    0.01
+(1 row)
+
 SELECT to_number('.0', '99999999.99999999');
  to_number 
 -----------
        0.0
 (1 row)
 
+SELECT CAST('.0' as numeric FORMAT '99999999.99999999');
+ numeric 
+---------
+     0.0
+(1 row)
+
 SELECT to_number('0', '99.99');
  to_number 
 -----------
          0
 (1 row)
 
+SELECT CAST('0' as numeric FORMAT '99.99');
+ numeric 
+---------
+       0
+(1 row)
+
 SELECT to_number('.-01', 'S99.99');
  to_number 
 -----------
      -0.01
 (1 row)
 
+SELECT CAST('.-01' as numeric FORMAT 'S99.99');
+ numeric 
+---------
+   -0.01
+(1 row)
+
 SELECT to_number('.01-', '99.99S');
  to_number 
 -----------
      -0.01
 (1 row)
 
+SELECT CAST('.01-' as numeric FORMAT '99.99S');
+ numeric 
+---------
+   -0.01
+(1 row)
+
 SELECT to_number(' . 0 1-', ' 9 9 . 9 9 S');
  to_number 
 -----------
      -0.01
 (1 row)
 
+SELECT CAST(' . 0 1-' as numeric FORMAT ' 9 9 . 9 9 S');
+ numeric 
+---------
+   -0.01
+(1 row)
+
 SELECT to_number('34,50','999,99');
  to_number 
 -----------
       3450
 (1 row)
 
+SELECT CAST('34,50' as numeric FORMAT '999,99');
+ numeric 
+---------
+    3450
+(1 row)
+
 SELECT to_number('123,000','999G');
  to_number 
 -----------
        123
 (1 row)
 
+SELECT CAST('123,000' as numeric FORMAT '999G');
+ numeric 
+---------
+     123
+(1 row)
+
 SELECT to_number('123456','999G999');
  to_number 
 -----------
     123456
 (1 row)
 
+SELECT CAST('123456' as numeric FORMAT '999G999');
+ numeric 
+---------
+  123456
+(1 row)
+
 SELECT to_number('$1234.56','L9,999.99');
  to_number 
 -----------
    1234.56
 (1 row)
 
+SELECT CAST('$1234.56' as numeric FORMAT 'L9,999.99');
+ numeric 
+---------
+ 1234.56
+(1 row)
+
 SELECT to_number('$1234.56','L99,999.99');
  to_number 
 -----------
    1234.56
 (1 row)
 
+SELECT CAST('$1234.56' as numeric FORMAT 'L99,999.99');
+ numeric 
+---------
+ 1234.56
+(1 row)
+
 SELECT to_number('$1,234.56','L99,999.99');
  to_number 
 -----------
    1234.56
 (1 row)
 
+SELECT CAST('$1,234.56' as numeric FORMAT 'L99,999.99');
+ numeric 
+---------
+ 1234.56
+(1 row)
+
 SELECT to_number('1234.56','L99,999.99');
  to_number 
 -----------
    1234.56
 (1 row)
 
+SELECT CAST('1234.56' as numeric FORMAT 'L99,999.99');
+ numeric 
+---------
+ 1234.56
+(1 row)
+
 SELECT to_number('1,234.56','L99,999.99');
  to_number 
 -----------
    1234.56
 (1 row)
 
+SELECT CAST('1,234.56' as numeric FORMAT 'L99,999.99');
+ numeric 
+---------
+ 1234.56
+(1 row)
+
 SELECT to_number('42nd', '99th');
  to_number 
 -----------
         42
 (1 row)
 
+SELECT CAST('42nd' as numeric FORMAT '99th');
+ numeric 
+---------
+      42
+(1 row)
+
 SELECT to_number('123456', '99999V99');
         to_number        
 -------------------------
  1234.560000000000000000
 (1 row)
 
+SELECT CAST('123456' as numeric FORMAT '99999V99');
+         numeric         
 
 -- Some additional tests for RN input
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 734da057c34..fdacfeae8ec 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
 # NB: temp.sql does reconnects which transiently uses 2 connections,
 # so keep this parallel group to at most 19 tests
 # ----------
-test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
+test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml cast
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/cast.sql b/src/test/regress/sql/cast.sql
new file mode 100644
index 00000000000..2999ed11198
--- /dev/null
+++ b/src/test/regress/sql/cast.sql
@@ -0,0 +1,107 @@
+create function ret_settxt() returns setof text as
+$$
+begin
+    return query execute 'select 1 union all select 1';
+end;
+$$
+language plpgsql immutable;
+
+-- check CAST FORMAT expression, the following should all fail
+select cast(NULL as date format ret_settxt()); -- cannot return a set
+select cast(NULL as date format (select 1::text where false));
+select cast(NULL as date format (string_agg(NULL, ' ')));
+select cast(NULL as date format (string_agg(NULL, ' ') over () ));
+select cast(NULL as date format NULL::int);
+select cast('1' as date format B'01');
+
+-- CAST FORMAT is restricted to the source and target types used by to_char, to_date, to_timestamp, and to_number.
+-- The following should all fail
+select cast('-34,338,492' as bigint format '99G999G999');
+select cast(array[1] as text format 'YYYY');
+select cast('1' as timestamp[] format 'YYYY-MM-DD');
+select cast('2012-13-12' as timestamp format 'YYYY-MM-DD');
+select cast('2012-13-12' as time format 'YYYY-MM-DD');
+select cast('2012-13-12' as timetz format 'YYYY-MM-DD');
+select cast('2012-13-12' as interval format 'YYYY-MM-DD');
+select cast('1'::text as unknown format 'YYYY-MM-DD');
+select cast('1' as bool format 'YYYY-MM-DD');
+select cast('1' as json format 'YYYY-MM-DD');
+select cast('1'::json as text format 'YYYY-MM-DD');
+select cast('1' as anyelement format 'YYYY-MM-DD');
+select cast('1' as anyenum format 'YYYY-MM-DD');
+select cast('1' as anyarray format 'YYYY-MM-DD');
+select cast(null::anyelement as anyelement format 'YYYY-MM-DD');
+select cast('2012-12-12 12:00'::timetz as text format 'YYYY-MM-DD HH:MI:SS TZ');
+select cast(null::regclass as text format 'YYYY-MM-DD HH:MI:SS TZ');
+select cast(null::int2 as numeric format null);
+select cast(null::date as timestamptz format null);
+select cast(null::time as timestamptz format null);
+
+-- CAST FORMAT is not supported for binary coercible type cast
+select cast('2022-01-01' as unknown format null);
+select cast('1' as text format '1'::text);
+select cast('1'::text as text format '1'::text);
+
+select cast('2012-12-12 12:00'::timetz as text format 'YYYY-MM-DD HH:MI:SS TZ'); -- error
+select cast('2012-13-12' as date format 'YYYY-DD-MM'); -- ok
+select cast('2012-13-12' as date format 'YYYY-MM-DD'); -- error
+select cast('1' as date format 'YYYY-MM-DD');
+select cast('1' collate "C" as date format 'YYYY-MM-DD');
+select cast('2012-13-12' as date format 'YYYY-DD-MM') as date;
+select cast('2012-13-12' as timestamptz format 'YYYY-DD-MM') as date;
+select cast('2012-13-12'::text as timestamp format 'YYYY-DD-MM') as date; -- error
+select cast('1' as timestamp format 'YYYY-MM-DD') = to_timestamp('1', 'YYYY-MM-DD');
+select cast('2026-01-28 13:29:12.324606+01'::text as timestamp format 'YYYY-MM-DD') =
+            to_timestamp('2026-01-28 13:29:12.324606+01'::text, 'YYYY-MM-DD');
+select cast('1' as date format 'YYYY-MM-DD') = to_date('1', 'YYYY-MM-DD');
+
+-- test with domain
+create domain d1 as date check (value <> '0001-01-01');
+select cast('1' as text format 'YYYY-MM-DD'); -- error
+select cast('1' as d1 format 'YYYY-MM-DD'); -- error
+select cast('1' as date format 'MM-DD'); -- ok
+select cast('1' as d1 format 'MM-DD'); -- ok
+
+select cast('1'::text collate "C" as date format 'YYYY-MM-DD');
+select cast('1' as date format 'YYYY-MM-DD') = to_date('1', 'YYYY-MM-DD') as expect_true;
+
+create table tcast(col1 text, col2 text, col3 date, col4 timestamptz, col5 int8);
+insert into tcast(col1, col2, col5)
+  values('2022-12-13', 'YYYY-MM-DD', 1234),
+        ('2022-12-01', 'YYYY-DD-MM', -1234);
+
+select cast(col1 as date format col2) from tcast;
+select cast(col1 as date format col3) from tcast; -- error
+select cast(col1 as date format col3::text) from tcast; -- ok
+
+create function imm_const() returns text as $$ begin return 'YYYY-MM-DD'; end; $$ language plpgsql immutable;
+select cast(col1 as date format imm_const()) from tcast;
+create index s1 on tcast(to_date(col1, 'YYYY-MM-DD')); -- error
+create index s1 on tcast(cast(col1 as date format 'YYYY-MM-DD')); -- error
+
+create view tcast_v1 as
+   select cast(col1 as date format 'YYYY-MM-DD') as to_date,
+         cast(col1 as timestamptz format 'YYYY-MM-DD') as to_timestamptz,
+         cast(NULL::interval as text format 'YYYY-MM-DD') as to_txt0,
+         cast(col1::timestamp as text format 'YYYY-MM-DD') as to_txt1,
+         cast(col3 as text format 'YYYY-MM-DD') as to_txt2,
+         cast(col4 as text format 'YYYY-MM-DD') as to_txt3,
+         cast(numeric 'inf' as text format 'YYYY-MM-DD') as to_txt4,
+         cast(bigint '12324' as text format 'YYYY-MM-DD') as to_txt5
+   from tcast;
+
+select pg_get_viewdef('tcast_v1', true);
+
+create view tcast_v2 as
+   select cast(col5 as text format '9.99EEEE') as to_txt0,
+         cast(col5::float8 as text format '9.99EEEE') as to_txt1,
+         cast(col5::float4 as text format '9.99EEEE') as to_txt2,
+         cast(col5::numeric as text format '9.99EEEE') as to_txt3,
+         cast(col5::int2 as text format '9.99EEEE') as to_txt4,
+         cast(col5::int4 as text format '9.99EEEE') as to_txt5
+   from tcast;
+select pg_get_viewdef('tcast_v2', true);
+select * from tcast_v2;
+drop function ret_settxt;
+drop view tcast_v1, tcast_v2;
+drop table tcast;
diff --git a/src/test/regress/sql/collate.linux.utf8.sql b/src/test/regress/sql/collate.linux.utf8.sql
index 132d13af0a8..f8ba5a0e815 100644
--- a/src/test/regress/sql/collate.linux.utf8.sql
+++ b/src/test/regress/sql/collate.linux.utf8.sql
@@ -182,12 +182,20 @@ SELECT to_char(date '2010-02-01', 'DD TMMON YYYY' COLLATE "tr_TR");
 SELECT to_char(date '2010-04-01', 'DD TMMON YYYY');
 SELECT to_char(date '2010-04-01', 'DD TMMON YYYY' COLLATE "tr_TR");
 
+SELECT CAST(date '2010-02-01' as text format 'DD TMMON YYYY');
+SELECT CAST(date '2010-02-01' as text format 'DD TMMON YYYY' COLLATE "tr_TR");
+SELECT CAST(date '2010-04-01' as text format 'DD TMMON YYYY');
+SELECT CAST(date '2010-04-01' as text format  'DD TMMON YYYY' COLLATE "tr_TR");
+
 -- to_date
 
 SELECT to_date('01 ŞUB 2010', 'DD TMMON YYYY');
 SELECT to_date('01 Şub 2010', 'DD TMMON YYYY');
 SELECT to_date('1234567890ab 2010', 'TMMONTH YYYY'); -- fail
 
+SELECT CAST('01 ŞUB 2010' as date format 'DD TMMON YYYY');
+SELECT CAST('01 ŞUB 2010' as date format 'DD TMMON YYYY'); -- ok
+SELECT CAST('1234567890ab 2010' as date format 'TMMONTH YYYY'); -- fail
 
 -- backwards parsing
 
diff --git a/src/test/regress/sql/horology.sql b/src/test/regress/sql/horology.sql
index 8978249a5dc..714e375b088 100644
--- a/src/test/regress/sql/horology.sql
+++ b/src/test/regress/sql/horology.sql
@@ -539,21 +539,46 @@ SELECT to_timestamp('2011-12-18 11:38 -05:20', 'YYYY-MM-DD HH12:MI TZH:TZM');
 SELECT to_timestamp('2011-12-18 11:38 20',     'YYYY-MM-DD HH12:MI TZM');
 
 SELECT to_timestamp('2011-12-18 11:38 EST', 'YYYY-MM-DD HH12:MI TZ');
+SELECT cast('2011-12-18 11:38 EST' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');
+
 SELECT to_timestamp('2011-12-18 11:38 -05', 'YYYY-MM-DD HH12:MI TZ');
+SELECT cast('2011-12-18 11:38 -05' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');
+
 SELECT to_timestamp('2011-12-18 11:38 +01:30', 'YYYY-MM-DD HH12:MI TZ');
+SELECT cast('2011-12-18 11:38 +01:30' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');
+
 SELECT to_timestamp('2011-12-18 11:38 MSK', 'YYYY-MM-DD HH12:MI TZ');  -- dyntz
+SELECT cast('2011-12-18 11:38 MSK' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');  -- dyntz
+
 SELECT to_timestamp('2011-12-18 00:00 LMT', 'YYYY-MM-DD HH24:MI TZ');  -- dyntz
+SELECT cast('2011-12-18 00:00 LMT' as timestamptz format 'YYYY-MM-DD HH24:MI TZ');  -- dyntz
+
 SELECT to_timestamp('2011-12-18 11:38ESTFOO24', 'YYYY-MM-DD HH12:MITZFOOSS');
+SELECT cast('2011-12-18 11:38ESTFOO24' as timestamptz format 'YYYY-MM-DD HH12:MITZFOOSS');
+
 SELECT to_timestamp('2011-12-18 11:38-05FOO24', 'YYYY-MM-DD HH12:MITZFOOSS');
+SELECT cast('2011-12-18 11:38-05FOO24' as timestamptz format 'YYYY-MM-DD HH12:MITZFOOSS');
+
 SELECT to_timestamp('2011-12-18 11:38 JUNK', 'YYYY-MM-DD HH12:MI TZ');  -- error
+SELECT cast('2011-12-18 11:38 JUNK' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');  -- error
+
 SELECT to_timestamp('2011-12-18 11:38 ...', 'YYYY-MM-DD HH12:MI TZ');  -- error
+SELECT cast('2011-12-18 11:38 ...' as timestamptz format 'YYYY-MM-DD HH12:MI TZ');  -- error
 
 SELECT to_timestamp('2011-12-18 11:38 -05', 'YYYY-MM-DD HH12:MI OF');
+SELECT cast ('2011-12-18 11:38 -05' as timestamptz format 'YYYY-MM-DD HH12:MI OF');
+
 SELECT to_timestamp('2011-12-18 11:38 +01:30', 'YYYY-MM-DD HH12:MI OF');
+SELECT cast('2011-12-18 11:38 +01:30' as timestamptz format 'YYYY-MM-DD HH12:MI OF');
+
 SELECT to_timestamp('2011-12-18 11:38 +xyz', 'YYYY-MM-DD HH12:MI OF');  -- error
+SELECT cast('2011-12-18 11:38 +xyz' as timestamptz format 'YYYY-MM-DD HH12:MI OF');  -- error
+
 SELECT to_timestamp('2011-12-18 11:38 +01:xyz', 'YYYY-MM-DD HH12:MI OF');  -- error
+SELECT cast('2011-12-18 11:38 +01:xyz' as timestamptz format 'YYYY-MM-DD HH12:MI OF');  -- error
 
 SELECT to_timestamp('2018-11-02 12:34:56.025', 'YYYY-MM-DD HH24:MI:SS.MS');
+SELECT cast('2018-11-02 12:34:56.025' as timestamptz format 'YYYY-MM-DD HH24:MI:SS.MS');
 
 SELECT i, to_timestamp('2018-11-02 12:34:56', 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
 SELECT i, to_timestamp('2018-11-02 12:34:56.1', 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
@@ -563,11 +588,15 @@ SELECT i, to_timestamp('2018-11-02 12:34:56.1234', 'YYYY-MM-DD HH24:MI:SS.FF' ||
 SELECT i, to_timestamp('2018-11-02 12:34:56.12345', 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
 SELECT i, to_timestamp('2018-11-02 12:34:56.123456', 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
 SELECT i, to_timestamp('2018-11-02 12:34:56.123456789', 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
+SELECT i, cast('2018-11-02 12:34:56.123456789' as timestamptz format 'YYYY-MM-DD HH24:MI:SS.FF' || i) FROM generate_series(1, 6) i;
 SELECT i, to_timestamp('20181102123456123456', 'YYYYMMDDHH24MISSFF' || i) FROM generate_series(1, 6) i;

 --
 -- Check handling of BC dates
@@ -677,7 +706,9 @@ SELECT to_date('2147483647 01', 'CC YY');

 --
 -- Check behavior with SQL-style fixed-GMT-offset time zone (cf bug #8572)
@@ -692,6 +723,17 @@ SELECT '2012-12-12 12:00'::timestamptz;
 SELECT '2012-12-12 12:00 America/New_York'::timestamptz;
 
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD HH:MI:SS TZ'),
+        to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
+
+SELECT cast('2012-12-12 12:00'::date as text format 'YYYY-MM-DD HH:MI:SS TZ'),
+        to_char('2012-12-12 12:00'::date, 'YYYY-MM-DD HH:MI:SS TZ');
+
+SELECT cast('12:00'::time as text format 'HH:MI:SS'),
+        to_char('12:00'::time, 'HH:MI:SS');
+
+SELECT cast('2012-12-12 12:00'::timetz as text format 'YYYY-MM-DD HH:MI:SS TZ');
+
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD SSSS');
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD SSSSS');
 
diff --git a/src/test/regress/sql/interval.sql b/src/test/regress/sql/interval.sql
index 43bc793925e..a5b4d63e9a4 100644
--- a/src/test/regress/sql/interval.sql
+++ b/src/test/regress/sql/interval.sql
@@ -801,7 +801,9 @@ SELECT 'infinity'::interval::time;
 SELECT '-infinity'::interval::time;
 
 SELECT to_char('infinity'::interval, 'YYYY');
+SELECT cast('infinity'::interval as text format 'YYYY');
 SELECT to_char('-infinity'::interval, 'YYYY');
+SELECT cast('-infinity'::interval as text format 'YYYY');
 
 -- "ago" can only appear once at the end of an interval.
 SELECT INTERVAL '42 days 2 seconds ago ago';
diff --git a/src/test/regress/sql/numeric.sql b/src/test/regress/sql/numeric.sql
index 640c6d92f4c..fd7bc26887f 100644
--- a/src/test/regress/sql/numeric.sql
+++ b/src/test/regress/sql/numeric.sql
@@ -1065,34 +1065,58 @@ SELECT to_char('100'::numeric, 'f"ool\\"999');
 --
 SET lc_numeric = 'C';
 SELECT to_number('-34,338,492', '99G999G999');
+SELECT CAST('-34,338,492' as numeric FORMAT  '99G999G999');
 SELECT to_number('-34,338,492.654,878', '99G999G999D999G999');
+SELECT CAST('-34,338,492.654,878' as numeric FORMAT '99G999G999D999G999');
 SELECT to_number('<564646.654564>', '999999.999999PR');
+SELECT CAST('<564646.654564>' as numeric FORMAT '999999.999999PR');
 SELECT to_number('0.00001-', '9.999999S');
+SELECT CAST('0.00001-' as numeric FORMAT '9.999999S');
 SELECT to_number('5.01-', 'FM9.999999S');
+SELECT CAST('5.01-' as numeric FORMAT 'FM9.999999S');
 SELECT to_number('5.01-', 'FM9.999999MI');
+SELECT CAST('5.01-' as numeric FORMAT 'FM9.999999MI');
 SELECT to_number('5 4 4 4 4 8 . 7 8', '9 9 9 9 9 9 . 9 9');
+SELECT CAST('5 4 4 4 4 8 . 7 8' as numeric FORMAT '9 9 9 9 9 9 . 9 9');
 SELECT to_number('.01', 'FM9.99');
+SELECT CAST('.01' as numeric FORMAT 'FM9.99');
 SELECT to_number('.0', '99999999.99999999');
+SELECT CAST('.0' as numeric FORMAT '99999999.99999999');
 SELECT to_number('0', '99.99');
+SELECT CAST('0' as numeric FORMAT '99.99');
 SELECT to_number('.-01', 'S99.99');
+SELECT CAST('.-01' as numeric FORMAT 'S99.99');
 SELECT to_number('.01-', '99.99S');
+SELECT CAST('.01-' as numeric FORMAT '99.99S');
 SELECT to_number(' . 0 1-', ' 9 9 . 9 9 S');
+SELECT CAST(' . 0 1-' as numeric FORMAT ' 9 9 . 9 9 S');
 SELECT to_number('34,50','999,99');
+SELECT CAST('34,50' as numeric FORMAT '999,99');
 SELECT to_number('123,000','999G');
+SELECT CAST('123,000' as numeric FORMAT '999G');
 SELECT to_number('123456','999G999');
+SELECT CAST('123456' as numeric FORMAT '999G999');
 SELECT to_number('$1234.56','L9,999.99');
+SELECT CAST('$1234.56' as numeric FORMAT 'L9,999.99');
 SELECT to_number('$1234.56','L99,999.99');
+SELECT CAST('$1234.56' as numeric FORMAT 'L99,999.99');
 SELECT to_number('$1,234.56','L99,999.99');
+SELECT CAST('$1,234.56' as numeric FORMAT 'L99,999.99');
 SELECT to_number('1234.56','L99,999.99');
+SELECT CAST('1234.56' as numeric FORMAT 'L99,999.99');
 SELECT to_number('1,234.56','L99,999.99');
+SELECT CAST('1,234.56' as numeric FORMAT 'L99,999.99');
 SELECT to_number('42nd', '99th');
+SELECT CAST('42nd' as numeric FORMAT '99th');
 SELECT to_number('123456', '99999V99');
+SELECT CAST('123456' as numeric FORMAT '99999V99');

---

From: jian he <[email-masked]>
Date: Mon, 30 Mar 2026 16:09:24 +0800

> HINT:  You might need to add explicit type casts.

The nearby v6 fixed these two issues, but,
src1=# SELECT cast('2012-12-12 12:00'::timetz as text format
'YYYY-MM-DD HH:MI:SS TZ');
ERROR:  cannot cast type time with time zone to text using formatted template
LINE 1: SELECT cast('2012-12-12 12:00'::timetz as text format
               ^
DETAIL:  Only categories of numeric, string, datetime, and timespan
source data types are supported for formatted type casting

The errdeatil above is not good and needs further improvement, since
timetz is a datetime category data type.

---

From: Zsolt Parragi <[email-masked]>
Date: Mon, 30 Mar 2026 23:17:52 +0100

v6 is better, but I found a few more questions:

+select pg_get_viewdef('tcast_v1', true);
+                                       pg_get_viewdef
+--------------------------------------------------------------------------------------------
+  SELECT CAST( col1 AS date FORMAT 'YYYY-MM-DD'::text) AS to_date,
                     +

Is that space after "CAST(" intentional?

Is exprTypmod(fmt) correct, shouldn't it use -1?

This one also shows a not so userfriendly error:

postgres=# SELECT CAST('hello' AS name FORMAT 'test') ;
2026-03-30 22:12:53.651 UTC [750980] ERROR:  function
pg_catalog.to_char(text, text) does not exist
2026-03-30 22:12:53.651 UTC [750980] DETAIL:  No function of that name
accepts the given argument types.
2026-03-30 22:12:53.651 UTC [750980] HINT:  You might need to add
explicit type casts.
2026-03-30 22:12:53.651 UTC [750980] STATEMENT:  SELECT CAST('hello'
AS name FORMAT 'test') ;
ERROR:  function pg_catalog.to_char(text, text) does not exist
DETAIL:  No function of that name accepts the given argument types.
HINT:  You might need to add explicit type casts.

And varchar/bpchar adds details about binary coercible casts:

SELECT CAST('hello' AS bpchar FORMAT 'test') ;
2026-03-30 22:14:12.081 UTC [750980] ERROR:  cannot cast type text to
character while using a format template at character 8
2026-03-30 22:14:12.081 UTC [750980] DETAIL:  binary coercible type
cast is not supported while using a format template
2026-03-30 22:14:12.081 UTC [750980] STATEMENT:  SELECT CAST('hello'
AS bpchar FORMAT 'test') ;
ERROR:  cannot cast type text to character while using a format template
LINE 1: SELECT CAST('hello' AS bpchar FORMAT 'test') ;

---

From: jian he <[email-masked]>
Date: Tue, 31 Mar 2026 15:47:53 +0800

> +select pg_get_viewdef('tcast_v1', true);
I have removed this white spce.

> Is exprTypmod(fmt) correct, shouldn't it use -1?
yes. it should be -1.

> SELECT CAST('hello' AS bpchar FORMAT 'test') ;

now:
SELECT CAST('hello' AS name FORMAT 'test') ;
ERROR:  cannot cast type text to name using formatted template

SELECT CAST('hello' AS bpchar FORMAT 'test') ;
ERROR:  cannot cast type text to character while using a format template
LINE 1: SELECT CAST('hello' AS bpchar FORMAT 'test') ;
               ^
DETAIL:  binary coercible type cast is not supported while using a
format template

type Text to type name is not binary coercible, see outpout of:

select *, castfunc::regproc from pg_cast
where castsource = 'text'::regtype and casttarget = 'name'::regtype;

--
jian
https://www.enterprisedb.com/
From e07a575e81069a91ff7b9168cd4585ac5d4d10bf Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Tue, 31 Mar 2026 15:45:17 +0800
Subject: [PATCH v7 1/1] CAST(expr AS type FORMAT 'template')

 src/backend/parser/parse_coerce.c             | 325 ++++++++++++++++-
 src/test/regress/expected/cast.out            | 329 ++++++++++++++++++
 src/test/regress/sql/cast.sql                 | 114 ++++++
 22 files changed, 1257 insertions(+), 14 deletions(-)

 				if (WALK(tc->arg))
 					return true;
+				if (WALK(tc->format))
+					return true;
 				if (WALK(tc->typeName))
 					return true;
 			}
 
+static Node *
 
 	n->arg = arg;
+	n->format = NULL;
 	n->typeName = typename;
 	n->location = location;
 	return (Node *) n;
 
 			break;

 			/*
index 913ca53666f..70c45e011bf 100644

 	/*
 	 * If the target is a fixed-length type, it may need a length coercion as
@@ -131,6 +151,242 @@ coerce_to_target_type(ParseState *pstate, Node *expr, Oid exprtype,
 	return result;
 }
 
+ /*
+		s_typcategory = TYPCATEGORY_STRING;
+			 * FIXME: errdetail is wrong, since timetz is a category of
+					errdetail("Only timestamptz, text, numeric and date data type are supported for formatted type casting"),
+								   exprType(fmt), TEXTOID, -1,
 
 /*
  * coerce_type()
@@ -158,6 +414,18 @@ Node *
@@ -166,6 +434,22 @@ coerce_type(ParseState *pstate, Node *node,
@@ -175,6 +459,18 @@ coerce_type(ParseState *pstate, Node *node,
@@ -197,6 +493,18 @@ coerce_type(ParseState *pstate, Node *node,
@@ -230,6 +538,17 @@ coerce_type(ParseState *pstate, Node *node,

 			/*
 
+		if (tc->format)
+			ereport(ERROR,

 
 			/*
 
 			/*
index 7bc12589e40..040eabe09bd 100644

index 00000000000..e08714bed93
@@ -0,0 +1,329 @@
+-- CAST FORMAT is restricted to the source and target types used by to_char, to_date, to_timestamp, and to_number
+select cast('hello' as name format 'test');
+ERROR:  cannot cast type text to name using formatted template
+select cast('hello' as bpchar format 'test');
+ERROR:  cannot cast type text to character while using a format template
+LINE 1: select cast('hello' as bpchar format 'test');
+DETAIL:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+DETAIL:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+DETAIL:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+DETAIL:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+DETAIL:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+DETAIL:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+DETAIL:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+DETAIL:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+DETAIL:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+DETAIL:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+DETAIL:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+DETAIL:  Only timestamptz, text, numeric and date data type are supported for formatted type casting
+-------------------------------------------------------------------------------------------
+  SELECT CAST(col1 AS date FORMAT 'YYYY-MM-DD'::text) AS to_date,                         +
+     CAST(col1 AS timestamp with time zone FORMAT 'YYYY-MM-DD'::text) AS to_timestamptz,  +
+     CAST(NULL::interval AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt0,                   +
+     CAST(col1::timestamp without time zone AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt1,+
+     CAST(col3 AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt2,                             +
+     CAST(col4 AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt3,                             +
+     CAST('Infinity'::numeric AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt4,              +
+     CAST('12324'::bigint AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt5                   +
+explain (verbose, costs off)
+select cast(col5::float8 as text format '9.99EEEE') as to_txt1,
+ cast(col5::numeric as text format '9.99EEEE') as to_txt2
+from tcast;
+                                                           QUERY PLAN                                                            
+---------------------------------------------------------------------------------------------------------------------------------
+ Seq Scan on public.tcast
+   Output: CAST((col5)::double precision AS text FORMAT '9.99EEEE'::text), CAST((col5)::numeric AS text FORMAT '9.99EEEE'::text)
+------------------------------------------------------------------------------
+  SELECT CAST(col5 AS text FORMAT '9.99EEEE'::text) AS to_txt0,              +
+     CAST(col5::double precision AS text FORMAT '9.99EEEE'::text) AS to_txt1,+
+     CAST(col5::real AS text FORMAT '9.99EEEE'::text) AS to_txt2,            +
+     CAST(col5::numeric AS text FORMAT '9.99EEEE'::text) AS to_txt3,         +
+     CAST(col5::smallint AS text FORMAT '9.99EEEE'::text) AS to_txt4,        +
+     CAST(col5::integer AS text FORMAT '9.99EEEE'::text) AS to_txt5          +

 

 

 

 

 

 
  
 (1 row)

 (1 row)

 

 

 

 

 

 

 

 

 # ----------
index 00000000000..e7062f44e42
@@ -0,0 +1,114 @@
+-- CAST FORMAT is restricted to the source and target types used by to_char, to_date, to_timestamp, and to_number
+select cast('hello' as name format 'test');
+select cast('hello' as bpchar format 'test');
+explain (verbose, costs off)
+select cast(col5::float8 as text format '9.99EEEE') as to_txt1,
+ cast(col5::numeric as text format '9.99EEEE') as to_txt2
+from tcast;

 
 
 -- backwards parsing

---

From: Corey Huinker <[email-masked]>
Date: Tue, 31 Mar 2026 13:47:43 -0400

> > +select pg_get_viewdef('tcast_v1', true);

Everything's passing, moving the tests out of citext, etc into cast.sql is
good.

I think the next step it to bring each TODO and FIXME into the thread, and
explain the factors that prevent you from being certain about what to do in
those situations.

---

From: Robert Haas <[email-masked]>
Date: Thu, 18 Jun 2026 10:51:45 -0400

On Tue, Mar 31, 2026 at 1:48 PM Corey Huinker <[email-masked]> wrote:
> Everything's passing, moving the tests out of citext, etc into cast.sql is good.
>
> I think the next step it to bring each TODO and FIXME into the thread, and explain the factors that prevent you from being certain about what to do in those situations.

IMHO, this patch as currently written is basically dead on arrival.
The v7 patch still works by constructing calls to
pg_catalog.{to_date,to_number,to_timestamp,to_char} based on the input
data type. But I think it's been clearly stated on this thread that we
need some kind of more generic infrastructure. Vik said it best: "we
need to find a way to make this generic so that custom types can
define formatting rules for themselves." I completely agree. The
extensible type system in PostgreSQL is one of the project's greatest
triumphs, and I do not think anyone is going to be enthusiastic about
committing a patch that purports to implement a flavor of casting but
is completely unextensible by out-of-core types and doesn't even cover
all the in-core types for which it might be interesting. Even if
someone is, -1 from me.

I suggest backing up to David G. Johnston's comment here: "How about
changing the specification for create type.  Right now input functions
must declare either 1 or 3 arguments.  Let’s also allow for 2 and
4-argument functions where the 2nd or 4th is where the format is
passed.  If a data type input function lacks one of those signatures
it is a runtime error if a format clause is attached to its cast
expression.  For output, we go from having zero input arguments to
zero or one, with the same resolution behavior." I'm not sure that
David's proposal here is really the best thing, but it's the kind of
thing that *could* be right, i.e. a generic infrastructure that can
work for any choice of data type.

The reason I somewhat hesitate to endorse that specific proposal is
that I'm not convinced that we should actually treat this as a form of
casting. Casts can be set to IMPLICIT or ASSIGNMENT or EXPLICIT, and
they can be WITHOUT FUNCTION or WITH INOUT, and none of that can be
relevant here. A CAST with FORMAT always needs to be implemented by a
function, is always explicit from a syntax point of view, and the code
to implement probably looks pretty different from the code needed for
a non-FORMAT cast. I am somewhat inclined to think we want something
that's like a cast function but actually a wholly separate mechanism,
e.g. a new pg_formatter catalog. I note that Jian He proposed putting
something in pg_type but I don't see how that can work, since there
are two types involved.

I don't accept the argument that we should start with this and extend
it later. The patch as proposed is just syntactic sugar. Said another
way, there is existing syntax that already delivers the functionality.
So I don't see why we would rush out support for a bit of new syntax;
anyone who wants to use this functionality can already do so. Getting
the infrastructure right is, IMHO, the interesting part of the
project, and I think that work needs to be done first.

-- 
Robert Haas
EDB: http://www.enterprisedb.com

---

From: Haibo Yan <[email-masked]>
Date: Sun, 28 Jun 2026 22:19:53 -0700

> project, and I think that work needs to be done first.

Robert pointed out earlier that hard-coding specific functions like
to_date and to_char in the parser would make the feature feel like a
set of special cases rather than a general mechanism.  I agree, and this
version moves to a catalog-driven design.

I added a new pg_formatter catalog.  A row in this catalog represents
one formatted conversion from a source type to a target type, and points
to a formatter function with this signature:

formatter(source_type, text) returns target_type

The second argument is the FORMAT expression coerced to text.

I kept this separate from pg_cast because formatted casts have different
semantics from ordinary casts.  Ordinary casts have concepts like implicit
casts, assignment casts, binary-compatible casts, and I/O casts.  A
formatted cast is different: it is always explicit, it has an extra format
expression, and it needs a function that receives both the source value and
the format string.  Putting this into pg_cast seemed likely to add special
cases there, while a separate catalog keeps the model simpler.

Another important point in this version is that built-in and user-defined
formatters use the same mechanism.  The parser and analyzer do not know
about to_date, to_char, or to_timestamp.  Built-in datetime/string
formatters are registered as catalog rows, and user-defined formatters are
registered the same way.  If no formatter exists for the exact source and
target type pair, the formatted cast fails.  It does not fall back to
ordinary cast resolution.

I split the work into four patches.

Patch 1 only adds parser support for the syntax:

CAST(expr AS type FORMAT format_expr)

It extends the raw TypeCast node with an optional format field and
updates the grammar and raw expression walker.  Parse analysis still
rejects formatted casts in this patch, so this is only the syntax and raw
parse-tree representation.

Patch 2 adds the catalog and DDL infrastructure.  It introduces
pg_formatter, syscache support, CREATE FORMATTER, DROP FORMATTER,
dependency handling, object-address support, and pg_dump support.  At this
point CAST ... FORMAT still does not execute; the patch only adds the
catalog object model.

Patch 3 is where formatted casts are resolved.  During parse analysis, the
source expression is transformed, the FORMAT expression is transformed and
coerced to text, and pg_formatter is searched by exact source and target
type.  I also treat unknown source literals as text, so cases like this
resolve naturally:

CAST('2026-06-28' AS date FORMAT 'YYYY-MM-DD')

I added a CoerceViaFormatter node rather than using a plain FuncExpr.
This preserves CAST(... FORMAT ...) syntax in ruleutils and pg_dump, and
allows the dependency walker to record a dependency on the pg_formatter
row itself.  Execution still uses the normal function-call machinery, so no
new executor opcode is needed.

Patch 4 adds an initial set of built-in formatter functions and bootstrap
pg_formatter rows for common datetime/string cases:

text/varchar/bpchar <-> date
text/varchar/bpchar <-> timestamp
text/varchar/bpchar <-> timestamptz

These built-ins are reached through the same catalog lookup path as
user-defined formatters.  They are not special-cased in parse analysis.  The
wrappers reuse PostgreSQL’s existing formatting code and are marked STRICT,
so NULL input or NULL FORMAT returns NULL.

This gives us SQL-standard-style CAST ... FORMAT support for the common
datetime/string cases, but the series intentionally does not claim complete
SQL-standard datetime-template conformance.  The built-ins reuse
PostgreSQL’s existing formatting template behavior.  Additional types such
as time, timetz, numeric, and interval, or stricter standard-template
coverage, can be added later as formatter functions without changing the
parser or the CoerceViaFormatter representation.

The main goal of this redesign is to make CAST ... FORMAT a real
catalog-driven facility rather than a parser rewrite to a few built-in
functions.  I think this addresses Robert’s concern better and gives us a
path for both built-in and extension-defined formatters.

I have attached the full patch series for review.  Comments are welcome.

Regards,
Haibo

---

From: jian he <[email-masked]>
Date: Mon, 29 Jun 2026 16:47:58 +0800

> work for any choice of data type.

I initially tried adding a 4th text argument to existing type input functions
(like date_in, array_in) to support format-driven casting from cstring to a
type's internal representation. This approach turned out to be a dead end due to
two irreconcilable constraints.

If the 4th argument defaults to NULL: pg_proc.proisstrict must be set to
false to prevent the function from short-circuiting and returning NULL whenever
no format is supplied. But eval_const_expressions relies on type input functions
being strict to safely constant-fold T_CoerceViaIO nodes — marking them all
non-strict would silently defeat a large class of optimizations.

If the 4th argument defaults to a non-NULL value: there is no sensible
sentinel, any chosen text value would be indistinguishable from a real format
string intentionally passed by the caller.

The fourth argument must have a DEFAULT value,
otherwise, we will have a duplicated function name for the data type
input function, which seems not ideal.
------------------------------------------------------------------
At first I thought the 4th argument on the input function was really
necessary. Consider:
CAST('{2022-21-01}' AS DATE[] FORMAT 'YYYY-DD-MM');
For this to work, we need converting each array element to date using
the specified
format — array_in would need to receive the format string somehow.

But given the analysis above, that path is closed. So the only remaining option
for making array format casting work is to duplicate a large portion of
array_in's logic into a dedicated array input formatting function:
array_typformatin.
------------------------------------------------------------------
This thread doesn't mention how CAST FORMAT deals with array types.
I imagine the mechanism is applies the format template to each array element.

For example:
CAST('{2022-01-01, 2022-21-01}' AS DATE[] FORMAT 'YYYY-DD-MM');

Apply the format template ('YYYY-DD-MM')  to ``2022-01-01`` and
``2022-21-01`` during processing date_in.
------------------------------------------------------------------
Similar to CREATE TYPE (typmodin/typmodout), the attached patch
extends the pg_type catalog,
add typformatin and typformatout to pg_type.

These two fields are the function OIDs for CAST FORMAT.

typformatin: the function converts a text value to the type's
internal representation given a format template.  It must have
signature (text, text) -> type.

typformatout allows values of the data type to be converted to text using a
format template. It must have the signature (type, text) -> text,
where the second
argument is the format template.

With this patch, CAST(expr AS type FORMAT 'template') works for any data type
that has a valid typformatin or typformatout support function registered.
However, the feature remains tightly scoped to text-based conversions: FORMAT is
only meaningful when the cast target or source is text data type, CAST FORMAT
between two non-text types such as int8 to int4 is not supported.

--
jian
https://www.enterprisedb.com/
From ea0f465db370cf05901a37606d69a656c4e0f1e6 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Mon, 29 Jun 2026 11:39:41 +0800
Subject: [PATCH v9 2/3] coerceUnknownConst delicated function for coerce const

---
 src/backend/parser/parse_coerce.c | 297 ++++++++++++++++--------------
 1 file changed, 159 insertions(+), 138 deletions(-)

diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index 3b92ae2a920..e2edde40fc9 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -52,6 +52,11 @@ static Node *coerce_record_to_complex(ParseState *pstate, Node *node,
 									  int location);
 static bool is_complex_array(Oid typid);
 static bool typeIsOfTypedTable(Oid reltypeId, Oid reloftypeId);
+static Node *coerceUnknownConst(ParseState *pstate, Node *node,
+								Oid inputTypeId,
+								Oid targetTypeId, int32 targetTypeMod,
+								CoercionContext ccontext, CoercionForm cformat,
+								int location);

 /*
@@ -231,145 +236,15 @@ coerce_type(ParseState *pstate, Node *node,
 		}
 	}
 	if (inputTypeId == UNKNOWNOID && IsA(node, Const))
-	{
-		/*
-		 * Input is a string constant with previously undetermined type. Apply
-		 * the target type's typinput function to it to produce a constant of
-		 * the target type.
-		 *
-		 * NOTE: this case cannot be folded together with the other
-		 * constant-input case, since the typinput function does not
-		 * necessarily behave the same as a type conversion function. For
-		 * example, int4's typinput function will reject "1.2", whereas
-		 * float-to-int type conversion will round to integer.
-		 *
-		 * XXX if the typinput function is not immutable, we really ought to
-		 * postpone evaluation of the function call until runtime. But there
-		 * is no way to represent a typinput function call as an expression
-		 * tree, because C-string values are not Datums. (XXX This *is*
-		 * possible as of 7.3, do we want to do it?)
-		 */
-		Const	   *con = (Const *) node;
-		Const	   *newcon = makeNode(Const);
-		Oid			baseTypeId;
-		int32		baseTypeMod;
-		int32		inputTypeMod;
-		Type		baseType;
-		ParseCallbackState pcbstate;
+		return coerceUnknownConst(pstate,
+								  node,
+								  inputTypeId,
+								  targetTypeId,
+								  targetTypeMod,
+								  ccontext,
+								  cformat,
+								  location);
 
-		/*
-		 * If the target type is a domain, we want to call its base type's
-		 * input routine, not domain_in().  This is to avoid premature failure
-		 * when the domain applies a typmod: existing input routines follow
-		 * implicit-coercion semantics for length checks, which is not always
-		 * what we want here.  The needed check will be applied properly
-		 * inside coerce_to_domain().
-		 */
-		baseTypeMod = targetTypeMod;
-		baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
-
-		/*
-		 * For most types we pass typmod -1 to the input routine, because
-		 * existing input routines follow implicit-coercion semantics for
-		 * length checks, which is not always what we want here.  Any length
-		 * constraint will be applied later by our caller.  An exception
-		 * however is the INTERVAL type, for which we *must* pass the typmod
-		 * or it won't be able to obey the bizarre SQL-spec input rules. (Ugly
-		 * as sin, but so is this part of the spec...)
-		 */
-		if (baseTypeId == INTERVALOID)
-			inputTypeMod = baseTypeMod;
-		else
-			inputTypeMod = -1;
-
-		baseType = typeidType(baseTypeId);
-
-		newcon->consttype = baseTypeId;
-		newcon->consttypmod = inputTypeMod;
-		newcon->constcollid = typeTypeCollation(baseType);
-		newcon->constlen = typeLen(baseType);
-		newcon->constbyval = typeByVal(baseType);
-		newcon->constisnull = con->constisnull;
-
-		/*
-		 * We use the original literal's location regardless of the position
-		 * of the coercion.  This is a change from pre-9.2 behavior, meant to
-		 * simplify life for pg_stat_statements.
-		 */
-		newcon->location = con->location;
-
-		/*
-		 * Set up to point at the constant's text if the input routine throws
-		 * an error.
-		 */
-		setup_parser_errposition_callback(&pcbstate, pstate, con->location);
-
-		/*
-		 * We assume here that UNKNOWN's internal representation is the same
-		 * as CSTRING.
-		 */
-		if (!con->constisnull)
-			newcon->constvalue = stringTypeDatum(baseType,
-												 DatumGetCString(con->constvalue),
-												 inputTypeMod);
-		else
-			newcon->constvalue = stringTypeDatum(baseType,
-												 NULL,
-												 inputTypeMod);
-
-		/*
-		 * If it's a varlena value, force it to be in non-expanded
-		 * (non-toasted) format; this avoids any possible dependency on
-		 * external values and improves consistency of representation.
-		 */
-		if (!con->constisnull && newcon->constlen == -1)
-			newcon->constvalue =
-				PointerGetDatum(PG_DETOAST_DATUM(newcon->constvalue));
-
-#ifdef RANDOMIZE_ALLOCATED_MEMORY
-
-		/*
-		 * For pass-by-reference data types, repeat the conversion to see if
-		 * the input function leaves any uninitialized bytes in the result. We
-		 * can only detect that reliably if RANDOMIZE_ALLOCATED_MEMORY is
-		 * enabled, so we don't bother testing otherwise.  The reason we don't
-		 * want any instability in the input function is that comparison of
-		 * Const nodes relies on bytewise comparison of the datums, so if the
-		 * input function leaves garbage then subexpressions that should be
-		 * identical may not get recognized as such.  See pgsql-hackers
-		 * discussion of 2008-04-04.
-		 */
-		if (!con->constisnull && !newcon->constbyval)
-		{
-			Datum		val2;
-
-			val2 = stringTypeDatum(baseType,
-								   DatumGetCString(con->constvalue),
-								   inputTypeMod);
-			if (newcon->constlen == -1)
-				val2 = PointerGetDatum(PG_DETOAST_DATUM(val2));
-			if (!datumIsEqual(newcon->constvalue, val2, false, newcon->constlen))
-				elog(WARNING, "type %s has unstable input conversion for \"%s\"",
-					 typeTypeName(baseType), DatumGetCString(con->constvalue));
-		}
-#endif
-
-		cancel_parser_errposition_callback(&pcbstate);
-
-		result = (Node *) newcon;
-
-		/* If target is a domain, apply constraints. */
-		if (baseTypeId != targetTypeId)
-			result = coerce_to_domain(result,
-									  baseTypeId, baseTypeMod,
-									  targetTypeId,
-									  ccontext, cformat, location,
-									  false);
-
-		ReleaseSysCache(baseType);
-
-		return result;
-	}
 	if (IsA(node, Param) &&
 		pstate != NULL && pstate->p_coerce_param_hook != NULL)
 	{
@@ -546,6 +421,152 @@ coerce_type(ParseState *pstate, Node *node,
 	return NULL;				/* keep compiler quiet */
 }
 
+static Node *
+coerceUnknownConst(ParseState *pstate, Node *node, Oid inputTypeId,
+				   Oid targetTypeId, int32 targetTypeMod,
+				   CoercionContext ccontext, CoercionForm cformat,
+				   int location)
+{
+	Node	   *result;
+
+	/*
+	 * Input is a string constant with previously undetermined type. Apply the
+	 * target type's typinput function to it to produce a constant of the
+	 * target type.
+	 *
+	 * NOTE: this case cannot be folded together with the other constant-input
+	 * case, since the typinput function does not necessarily behave the same
+	 * as a type conversion function. For example, int4's typinput function
+	 * will reject "1.2", whereas float-to-int type conversion will round to
+	 * integer.
+	 *
+	 * XXX if the typinput function is not immutable, we really ought to
+	 * postpone evaluation of the function call until runtime. But there is no
+	 * way to represent a typinput function call as an expression tree,
+	 * because C-string values are not Datums. (XXX This *is* possible as of
+	 * 7.3, do we want to do it?)
+	 */
+	Const	   *con = (Const *) node;
+	Const	   *newcon = makeNode(Const);
+	Oid			baseTypeId;
+	int32		baseTypeMod;
+	int32		inputTypeMod;
+	Type		baseType;
+	ParseCallbackState pcbstate;
+
+	/*
+	 * If the target type is a domain, we want to call its base type's input
+	 * routine, not domain_in().  This is to avoid premature failure when the
+	 * domain applies a typmod: existing input routines follow
+	 * implicit-coercion semantics for length checks, which is not always what
+	 * we want here.  The needed check will be applied properly inside
+	 * coerce_to_domain().
+	 */
+	baseTypeMod = targetTypeMod;
+	baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
+
+	/*
+	 * For most types we pass typmod -1 to the input routine, because existing
+	 * input routines follow implicit-coercion semantics for length checks,
+	 * which is not always what we want here.  Any length constraint will be
+	 * applied later by our caller.  An exception however is the INTERVAL
+	 * type, for which we *must* pass the typmod or it won't be able to obey
+	 * the bizarre SQL-spec input rules. (Ugly as sin, but so is this part of
+	 * the spec...)
+	 */
+	if (baseTypeId == INTERVALOID)
+		inputTypeMod = baseTypeMod;
+	else
+		inputTypeMod = -1;
+
+	baseType = typeidType(baseTypeId);
+
+	newcon->consttype = baseTypeId;
+	newcon->consttypmod = inputTypeMod;
+	newcon->constcollid = typeTypeCollation(baseType);
+	newcon->constlen = typeLen(baseType);
+	newcon->constbyval = typeByVal(baseType);
+	newcon->constisnull = con->constisnull;
+
+	/*
+	 * We use the original literal's location regardless of the position of
+	 * the coercion.  This is a change from pre-9.2 behavior, meant to
+	 * simplify life for pg_stat_statements.
+	 */
+	newcon->location = con->location;
+
+	/*
+	 * Set up to point at the constant's text if the input routine throws an
+	 * error.
+	 */
+	setup_parser_errposition_callback(&pcbstate, pstate, con->location);
+
+	/*
+	 * We assume here that UNKNOWN's internal representation is the same as
+	 * CSTRING.
+	 */
+	if (!con->constisnull)
+		newcon->constvalue = stringTypeDatum(baseType,
+											 DatumGetCString(con->constvalue),
+											 inputTypeMod);
+	else
+		newcon->constvalue = stringTypeDatum(baseType,
+											 NULL,
+											 inputTypeMod);
+
+	/*
+	 * If it's a varlena value, force it to be in non-expanded (non-toasted)
+	 * format; this avoids any possible dependency on external values and
+	 * improves consistency of representation.
+	 */
+	if (!con->constisnull && newcon->constlen == -1)
+		newcon->constvalue =
+			PointerGetDatum(PG_DETOAST_DATUM(newcon->constvalue));
+
+#ifdef RANDOMIZE_ALLOCATED_MEMORY
+
+	/*
+	 * For pass-by-reference data types, repeat the conversion to see if the
+	 * input function leaves any uninitialized bytes in the result. We can
+	 * only detect that reliably if RANDOMIZE_ALLOCATED_MEMORY is enabled, so
+	 * we don't bother testing otherwise.  The reason we don't want any
+	 * instability in the input function is that comparison of Const nodes
+	 * relies on bytewise comparison of the datums, so if the input function
+	 * leaves garbage then subexpressions that should be identical may not get
+	 * recognized as such.  See pgsql-hackers discussion of 2008-04-04.
+	 */
+	if (!con->constisnull && !newcon->constbyval)
+	{
+		Datum		val2;
+
+		val2 = stringTypeDatum(baseType,
+							   DatumGetCString(con->constvalue),
+							   inputTypeMod);
+		if (newcon->constlen == -1)
+			val2 = PointerGetDatum(PG_DETOAST_DATUM(val2));
+		if (!datumIsEqual(newcon->constvalue, val2, false, newcon->constlen))
+			elog(WARNING, "type %s has unstable input conversion for \"%s\"",
+				 typeTypeName(baseType), DatumGetCString(con->constvalue));
+	}
+#endif
+
+	cancel_parser_errposition_callback(&pcbstate);
+
+	result = (Node *) newcon;
+
+	/* If target is a domain, apply constraints. */
+	if (baseTypeId != targetTypeId)
+		result = coerce_to_domain(result,
+								  baseTypeId, baseTypeMod,
+								  targetTypeId,
+								  ccontext, cformat, location,
+								  false);
+
+	ReleaseSysCache(baseType);
+
+	return result;
+}
+
 
 /*
  * can_coerce_type()
-- 
2.34.1

From c0e3982a3d713ccacdd2cbeb84de48eab5e774a0 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Mon, 29 Jun 2026 15:33:34 +0800
Subject: [PATCH v9 1/3] Add typformatin and typformatout to pg_type
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

PostgreSQL allows a base data type to declare optional support functions in
pg_type that define type-specific behaviors, e.g. typmodin/typmodout for type
modifier handling.  This commit adds two more:

typformatin  allows the data type to be a target of format-driven input
                casting.  The function converts a text value to the type's
                internal representation given a format template.  It must
                have signature (text, text) -> type, where the first
                argument is the source text and the second is the format
                template.

typformatout allows values of the data type to be converted to text
                using a format template.  It must have signature
                (type, text) -> text, where the second argument is the
                format template.

typformatin is unlikely to work for array types as-is; supporting
array-element-level format casting requires special handling.

TYPFORMAT_IN and TYPFORMAT_OUT can be specified in CREATE TYPE or via
ALTER TYPE SET, and require superuser privilege.

User-defined types can participate by registering their own formatting
functions.

These support functions enable the SQL-standard CAST(expr AS type FORMAT
'template') construct introduced in the next commit.

The following built-in types are assigned formatting support functions:
    typformatin:  date → to_date, numeric → to_number,
                timestamptz → to_timestamp
    typformatout: int4, int8, float4, float8, numeric, interval,
                timestamp, timestamptz → to_char

discussion: https://postgr.es/m/CACJufxGqm7cYQ5C65Eoh1z-f+aMdhv9_7V=[email-masked]
commitfest: https://commitfest.postgresql.org/patch/5957
---
 doc/src/sgml/ref/alter_type.sgml          |  19 +++
 doc/src/sgml/ref/create_type.sgml         |  57 ++++++-
 src/backend/catalog/heap.c                |   4 +
 src/backend/catalog/pg_type.c             |  18 +++
 src/backend/catalog/system_functions.sql  |  11 ++
 src/backend/commands/typecmds.c           | 172 ++++++++++++++++++++++
 src/backend/utils/cache/lsyscache.c       |  77 ++++++++++
 src/include/catalog/pg_type.dat           |   2 +
 src/include/catalog/pg_type.h             |   6 +
 src/test/regress/expected/create_type.out |  23 +++
 src/test/regress/expected/oidjoins.out    |   2 +
 src/test/regress/sql/create_type.sql      |  22 +++
 12 files changed, 412 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/ref/alter_type.sgml b/doc/src/sgml/ref/alter_type.sgml
index 025a3ee48f5..f7ba47c870c 100644
--- a/doc/src/sgml/ref/alter_type.sgml
+++ b/doc/src/sgml/ref/alter_type.sgml
@@ -194,6 +194,25 @@ ALTER TYPE <replaceable class="parameter">name</replaceable> SET ( <replaceable
          requires superuser privilege.
         </para>
        </listitem>
+
+       <listitem>
+        <para>
+         <literal>TYPFORMAT_IN</literal> can be set to the name of a type-specific
+         input format function, or <literal>NONE</literal> to remove
+         the type's input format function.  Using this option
+         requires superuser privilege.
+        </para>
+       </listitem>
+
+       <listitem>
+        <para>
+         <literal>TYPFORMAT_OUT</literal> can be set to the name of a type-specific
+         output format function, or <literal>NONE</literal> to remove
+         the type's output format function.  Using this option
+         requires superuser privilege.
+        </para>
+       </listitem>
+
        <listitem>
         <para>
          <literal>SUBSCRIPT</literal> can be set to the name of a type-specific
diff --git a/doc/src/sgml/ref/create_type.sgml b/doc/src/sgml/ref/create_type.sgml
index 994dfc65268..115a7593b3b 100644
--- a/doc/src/sgml/ref/create_type.sgml
+++ b/doc/src/sgml/ref/create_type.sgml
@@ -44,6 +44,8 @@ CREATE TYPE <replaceable class="parameter">name</replaceable> (
     [ , TYPMOD_IN = <replaceable class="parameter">type_modifier_input_function</replaceable> ]
     [ , TYPMOD_OUT = <replaceable class="parameter">type_modifier_output_function</replaceable> ]
     [ , ANALYZE = <replaceable class="parameter">analyze_function</replaceable> ]
+    [ , TYPFORMAT_IN = <replaceable class="parameter">type_format_input_function</replaceable> ]
+    [ , TYPFORMAT_OUT = <replaceable class="parameter">type_format_out_function</replaceable> ]
     [ , SUBSCRIPT = <replaceable class="parameter">subscript_function</replaceable> ]
     [ , INTERNALLENGTH = { <replaceable class="parameter">internallength</replaceable> | VARIABLE } ]
     [ , PASSEDBYVALUE ]
@@ -210,7 +212,9 @@ CREATE TYPE <replaceable class="parameter">name</replaceable>
    <replaceable class="parameter">send_function</replaceable>,
    <replaceable class="parameter">type_modifier_input_function</replaceable>,
    <replaceable class="parameter">type_modifier_output_function</replaceable>,
-   <replaceable class="parameter">analyze_function</replaceable>, and
+   <replaceable class="parameter">analyze_function</replaceable>,
+   <replaceable class="parameter">type_format_input_function</replaceable>,
+   <replaceable class="parameter">type_format_out_function</replaceable>, and
    <replaceable class="parameter">subscript_function</replaceable>
    are optional.  Generally these functions have to be coded in C
    or another low-level language.
@@ -332,6 +336,30 @@ CREATE TYPE <replaceable class="parameter">name</replaceable>
    in <filename>src/include/commands/vacuum.h</filename>.
   </para>
 
+  <para>
+   The optional <replaceable class="parameter">type_format_input_function</replaceable>
+   converts a text value to the type's internal representation using a format
+   template, supporting the <literal>CAST(<replaceable>expr</replaceable> AS
+   <replaceable>type</replaceable> FORMAT '<replaceable>template</replaceable>')</literal>
+   SQL syntax for input.  It must be declared to take two arguments of type
+   <type>text</type> — the first is the value to convert and the second is the
+   format pattern — and return the data type being defined.  For example,
+   <function>to_date</function> serves as the format input function for
+   <type>date</type>, and <function>to_number</function> for <type>numeric</type>.
+  </para>
+
+  <para>
+   The optional <replaceable class="parameter">type_format_out_function</replaceable>
+   converts a value of the type to a text representation using a format template,
+   supporting the <literal>CAST(<replaceable>expr</replaceable> AS text FORMAT
+   '<replaceable>template</replaceable>')</literal> SQL syntax for output.  It must
+   be declared to take two arguments — the first of the data type being defined and
+   the second of type <type>text</type> (the format pattern) — and return
+   <type>text</type>.  For example, <function>to_char</function> serves as the
+   format output function for <type>numeric</type>, <type>timestamp</type>, and
+   other types.
+  </para>
+
   <para>
    The optional <replaceable class="parameter">subscript_function</replaceable>
    allows the data type to be subscripted in SQL commands.  Specifying this
@@ -721,6 +749,33 @@ CREATE TYPE <replaceable class="parameter">name</replaceable>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><replaceable class="parameter">type_format_input_function</replaceable></term>
+    <listitem>
+     <para>
+      The name of a function that converts a text value to the type's
+      internal form using a format template.  The function must accept two
+      <type>text</type> arguments (value and format pattern) and return the
+      new data type.  See the description of <literal>TYPFORMAT_IN</literal>
+      above for details.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">type_format_output_function</replaceable></term>
+    <listitem>
+     <para>
+      The name of a function that converts a value of the type to a text
+      representation using a format template.  The function must accept the
+      data type as its first argument and a <type>text</type> format pattern
+      as its second, and return <type>text</type>.  See the description of
+      <literal>TYPFORMAT_OUT</literal> above for details.
+     </para>
+    </listitem>
+   </varlistentry>
+
+
    <varlistentry>
     <term><replaceable class="parameter">subscript_function</replaceable></term>
     <listitem>
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 88087654de9..7152c16bd34 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -1085,6 +1085,8 @@ AddNewRelationType(const char *typeName,
 				   InvalidOid,	/* typmodin procedure - none */
 				   InvalidOid,	/* typmodout procedure - none */
 				   InvalidOid,	/* analyze procedure - default */
+				   InvalidOid,	/* input format procedure */
+				   InvalidOid,	/* output format procedure */
 				   InvalidOid,	/* subscript procedure - none */
 				   InvalidOid,	/* array element type - irrelevant */
 				   false,		/* this is not an array type */
@@ -1411,6 +1413,8 @@ heap_create_with_catalog(const char *relname,
 				   InvalidOid,	/* typmodin procedure - none */
 				   InvalidOid,	/* typmodout procedure - none */
 				   F_ARRAY_TYPANALYZE,	/* array analyze procedure */
+				   InvalidOid,	/* array don't have input format procedure */
+				   InvalidOid,	/* array don't have output format procedure */
 				   F_ARRAY_SUBSCRIPT_HANDLER,	/* array subscript procedure */
 				   new_type_oid,	/* array element type - the rowtype */
 				   true,		/* yes, this is an array type */
diff --git a/src/backend/catalog/pg_type.c b/src/backend/catalog/pg_type.c
index fc369c35aa6..a3e60f7a2fc 100644
--- a/src/backend/catalog/pg_type.c
+++ b/src/backend/catalog/pg_type.c
@@ -113,6 +113,8 @@ TypeShellMake(const char *typeName, Oid typeNamespace, Oid ownerId)
 	values[Anum_pg_type_typmodin - 1] = ObjectIdGetDatum(InvalidOid);
 	values[Anum_pg_type_typmodout - 1] = ObjectIdGetDatum(InvalidOid);
 	values[Anum_pg_type_typanalyze - 1] = ObjectIdGetDatum(InvalidOid);
+	values[Anum_pg_type_typformatin - 1] = ObjectIdGetDatum(InvalidOid);
+	values[Anum_pg_type_typformatout - 1] = ObjectIdGetDatum(InvalidOid);
 	values[Anum_pg_type_typalign - 1] = CharGetDatum(TYPALIGN_INT);
 	values[Anum_pg_type_typstorage - 1] = CharGetDatum(TYPSTORAGE_PLAIN);
 	values[Anum_pg_type_typnotnull - 1] = BoolGetDatum(false);
@@ -210,6 +212,8 @@ TypeCreate(Oid newTypeOid,
 		   Oid typmodinProcedure,
 		   Oid typmodoutProcedure,
 		   Oid analyzeProcedure,
+		   Oid inputformatProcedure,
+		   Oid outputformatProcedure,
 		   Oid subscriptProcedure,
 		   Oid elementType,
 		   bool isImplicitArray,
@@ -369,6 +373,8 @@ TypeCreate(Oid newTypeOid,
 	values[Anum_pg_type_typmodin - 1] = ObjectIdGetDatum(typmodinProcedure);
 	values[Anum_pg_type_typmodout - 1] = ObjectIdGetDatum(typmodoutProcedure);
 	values[Anum_pg_type_typanalyze - 1] = ObjectIdGetDatum(analyzeProcedure);
+	values[Anum_pg_type_typformatin - 1] = ObjectIdGetDatum(inputformatProcedure);
+	values[Anum_pg_type_typformatout - 1] = ObjectIdGetDatum(outputformatProcedure);
 	values[Anum_pg_type_typalign - 1] = CharGetDatum(alignment);
 	values[Anum_pg_type_typstorage - 1] = CharGetDatum(storage);
 	values[Anum_pg_type_typnotnull - 1] = BoolGetDatum(typeNotNull);
@@ -677,6 +683,18 @@ GenerateTypeDependencies(HeapTuple typeTuple,
 		add_exact_object_address(&referenced, addrs_normal);
 	}
 
+	if (OidIsValid(typeForm->typformatin))
+	{
+		ObjectAddressSet(referenced, ProcedureRelationId, typeForm->typformatin);
+		add_exact_object_address(&referenced, addrs_normal);
+	}
+
+	if (OidIsValid(typeForm->typformatout))
+	{
+		ObjectAddressSet(referenced, ProcedureRelationId, typeForm->typformatout);
+		add_exact_object_address(&referenced, addrs_normal);
+	}
+
 	if (OidIsValid(typeForm->typsubscript))
 	{
 		ObjectAddressSet(referenced, ProcedureRelationId, typeForm->typsubscript);
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index c3c0a6e84ed..5089c41c407 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -366,3 +366,14 @@ CREATE OR REPLACE FUNCTION ts_debug(document text,
 BEGIN ATOMIC
     SELECT * FROM ts_debug(get_current_ts_config(), $1);
 END;
+
+ALTER TYPE timestamptz SET (TYPFORMAT_IN = to_timestamp);
+
+ALTER TYPE int8 SET (TYPFORMAT_OUT = to_char);
+ALTER TYPE int4 SET (TYPFORMAT_OUT = to_char);
+ALTER TYPE interval SET (TYPFORMAT_OUT = to_char);
+ALTER TYPE numeric SET (TYPFORMAT_OUT = to_char);
+ALTER TYPE float8 SET (TYPFORMAT_OUT = to_char);
+ALTER TYPE float4 SET (TYPFORMAT_OUT = to_char);
+ALTER TYPE timestamp SET (TYPFORMAT_OUT = to_char);
+ALTER TYPE timestamptz SET (TYPFORMAT_OUT = to_char);
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index e9c3215ccec..64c94ecf828 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -94,6 +94,8 @@ typedef struct
 	bool		updateTypmodin;
 	bool		updateTypmodout;
 	bool		updateAnalyze;
+	bool		updateTypformatin;
+	bool		updateTypformatout;
 	bool		updateSubscript;
 	/* New values for relevant attributes */
 	char		storage;
@@ -102,6 +104,8 @@ typedef struct
 	Oid			typmodinOid;
 	Oid			typmodoutOid;
 	Oid			analyzeOid;
+	Oid			typformatinOid;
+	Oid			typformatoutOid;
 	Oid			subscriptOid;
 } AlterTypeRecurseParams;
 
@@ -124,6 +128,8 @@ static Oid	findTypeSendFunction(List *procname, Oid typeOid);
 static Oid	findTypeTypmodinFunction(List *procname);
 static Oid	findTypeTypmodoutFunction(List *procname);
 static Oid	findTypeAnalyzeFunction(List *procname, Oid typeOid);
+static Oid	findTypeTypformatinFunction(List *procname, Oid typeOid);
+static Oid	findTypeTypformatoutFunction(List *procname, Oid typeOid);
 static Oid	findTypeSubscriptingFunction(List *procname, Oid typeOid);
 static Oid	findRangeSubOpclass(List *opcname, Oid subtype);
 static Oid	findRangeCanonicalFunction(List *procname, Oid typeOid);
@@ -163,6 +169,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	List	   *typmodinName = NIL;
 	List	   *typmodoutName = NIL;
 	List	   *analyzeName = NIL;
+	List	   *inputformatName = NIL;
+	List	   *outputformatName = NIL;
 	List	   *subscriptName = NIL;
 	char		category = TYPCATEGORY_USER;
 	bool		preferred = false;
@@ -182,6 +190,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	DefElem    *typmodinNameEl = NULL;
 	DefElem    *typmodoutNameEl = NULL;
 	DefElem    *analyzeNameEl = NULL;
+	DefElem    *typformatinNameEl = NULL;
+	DefElem    *typformatoutNameEl = NULL;
 	DefElem    *subscriptNameEl = NULL;
 	DefElem    *categoryEl = NULL;
 	DefElem    *preferredEl = NULL;
@@ -199,6 +209,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	Oid			typmodinOid = InvalidOid;
 	Oid			typmodoutOid = InvalidOid;
 	Oid			analyzeOid = InvalidOid;
+	Oid			typformatinOid = InvalidOid;
+	Oid			typformatoutOid = InvalidOid;
 	Oid			subscriptOid = InvalidOid;
 	char	   *array_type;
 	Oid			array_oid;
@@ -305,6 +317,10 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 		else if (strcmp(defel->defname, "analyze") == 0 ||
 				 strcmp(defel->defname, "analyse") == 0)
 			defelp = &analyzeNameEl;
+		else if (strcmp(defel->defname, "typformat_in") == 0)
+			defelp = &typformatinNameEl;
+		else if (strcmp(defel->defname, "typformat_out") == 0)
+			defelp = &typformatoutNameEl;
 		else if (strcmp(defel->defname, "subscript") == 0)
 			defelp = &subscriptNameEl;
 		else if (strcmp(defel->defname, "category") == 0)
@@ -374,6 +390,11 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 		typmodoutName = defGetQualifiedName(typmodoutNameEl);
 	if (analyzeNameEl)
 		analyzeName = defGetQualifiedName(analyzeNameEl);
+	if (typformatinNameEl)
+		inputformatName = defGetQualifiedName(typformatinNameEl);
+	if (typformatoutNameEl)
+		outputformatName = defGetQualifiedName(typformatoutNameEl);
+
 	if (subscriptNameEl)
 		subscriptName = defGetQualifiedName(subscriptNameEl);
 	if (categoryEl)
@@ -500,6 +521,12 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	if (analyzeName)
 		analyzeOid = findTypeAnalyzeFunction(analyzeName, typoid);
 
+	if (inputformatName)
+		typformatinOid = findTypeTypformatinFunction(inputformatName, typoid);
+
+	if (outputformatName)
+		typformatoutOid = findTypeTypformatoutFunction(outputformatName, typoid);
+
 	/*
 	 * Likewise look up the subscripting function if any.  If it is not
 	 * specified, but a typelem is specified, allow that if
@@ -552,6 +579,12 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	if (analyzeOid && !object_ownercheck(ProcedureRelationId, analyzeOid, GetUserId()))
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
 					   NameListToString(analyzeName));
+	if (typformatinOid && !object_ownercheck(ProcedureRelationId, typformatinOid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
+					   NameListToString(inputformatName));
+	if (typformatoutOid && !object_ownercheck(ProcedureRelationId, typformatoutOid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
+					   NameListToString(outputformatName));
 	if (subscriptOid && !object_ownercheck(ProcedureRelationId, subscriptOid, GetUserId()))
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
 					   NameListToString(subscriptName));
@@ -590,6 +623,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 				   typmodinOid, /* typmodin procedure */
 				   typmodoutOid,	/* typmodout procedure */
 				   analyzeOid,	/* analyze procedure */
+				   typformatinOid,	/* typformatin procedure */
+				   typformatoutOid, /* typformatout procedure */
 				   subscriptOid,	/* subscript procedure */
 				   elemType,	/* element type ID */
 				   false,		/* this is not an implicit array type */
@@ -632,6 +667,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 			   typmodinOid,		/* typmodin procedure */
 			   typmodoutOid,	/* typmodout procedure */
 			   F_ARRAY_TYPANALYZE,	/* analyze procedure */
+			   InvalidOid,		/* array don't have input format procedure */
+			   InvalidOid,		/* array don't output format procedure */
 			   F_ARRAY_SUBSCRIPT_HANDLER,	/* array subscript procedure */
 			   typoid,			/* element type ID */
 			   true,			/* yes this is an array type */
@@ -1075,6 +1112,8 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 				   InvalidOid,	/* typmodin procedure - none */
 				   InvalidOid,	/* typmodout procedure - none */
 				   analyzeProcedure,	/* analyze procedure */
+				   InvalidOid,	/* typformatin procedure - none */
+				   InvalidOid,	/* typformatout procedure - none */
 				   InvalidOid,	/* subscript procedure - none */
 				   InvalidOid,	/* no array element type */
 				   false,		/* this isn't an array */
@@ -1116,6 +1155,8 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 			   InvalidOid,		/* typmodin procedure - none */
 			   InvalidOid,		/* typmodout procedure - none */
 			   F_ARRAY_TYPANALYZE,	/* analyze procedure */
+			   InvalidOid,		/* typformatin procedure - none */
+			   InvalidOid,		/* typformatout procedure - none */
 			   F_ARRAY_SUBSCRIPT_HANDLER,	/* array subscript procedure */
 			   address.objectId,	/* element type ID */
 			   true,			/* yes this is an array type */
@@ -1238,6 +1279,8 @@ DefineEnum(CreateEnumStmt *stmt)
 				   InvalidOid,	/* typmodin procedure - none */
 				   InvalidOid,	/* typmodout procedure - none */
 				   InvalidOid,	/* analyze procedure - default */
+				   InvalidOid,	/* typformatin procedure - none */
+				   InvalidOid,	/* typformatout procedure - none */
 				   InvalidOid,	/* subscript procedure - none */
 				   InvalidOid,	/* element type ID */
 				   false,		/* this is not an array type */
@@ -1279,6 +1322,8 @@ DefineEnum(CreateEnumStmt *stmt)
 			   InvalidOid,		/* typmodin procedure - none */
 			   InvalidOid,		/* typmodout procedure - none */
 			   F_ARRAY_TYPANALYZE,	/* analyze procedure */
+			   InvalidOid,		/* typformatin procedure - none */
+			   InvalidOid,		/* typformatout procedure - none */
 			   F_ARRAY_SUBSCRIPT_HANDLER,	/* array subscript procedure */
 			   enumTypeAddr.objectId,	/* element type ID */
 			   true,			/* yes this is an array type */
@@ -1592,6 +1637,8 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 				   InvalidOid,	/* typmodin procedure - none */
 				   InvalidOid,	/* typmodout procedure - none */
 				   F_RANGE_TYPANALYZE,	/* analyze procedure */
+				   InvalidOid,	/* typformatin procedure - none */
+				   InvalidOid,	/* typformatout procedure - none */
 				   InvalidOid,	/* subscript procedure - none */
 				   InvalidOid,	/* element type ID - none */
 				   false,		/* this is not an array type */
@@ -1659,6 +1706,8 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 				   InvalidOid,	/* typmodin procedure - none */
 				   InvalidOid,	/* typmodout procedure - none */
 				   F_MULTIRANGE_TYPANALYZE, /* analyze procedure */
+				   InvalidOid,	/* typformatin procedure - none */
+				   InvalidOid,	/* typformatout procedure - none */
 				   InvalidOid,	/* subscript procedure - none */
 				   InvalidOid,	/* element type ID - none */
 				   false,		/* this is not an array type */
@@ -1698,6 +1747,8 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 			   InvalidOid,		/* typmodin procedure - none */
 			   InvalidOid,		/* typmodout procedure - none */
 			   F_ARRAY_TYPANALYZE,	/* analyze procedure */
+			   InvalidOid,		/* typformatin procedure - none */
+			   InvalidOid,		/* typformatout procedure - none */
 			   F_ARRAY_SUBSCRIPT_HANDLER,	/* array subscript procedure */
 			   typoid,			/* element type ID */
 			   true,			/* yes this is an array type */
@@ -1737,6 +1788,8 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 			   InvalidOid,		/* typmodin procedure - none */
 			   InvalidOid,		/* typmodout procedure - none */
 			   F_ARRAY_TYPANALYZE,	/* analyze procedure */
+			   InvalidOid,		/* typformatin procedure - none */
+			   InvalidOid,		/* typformatout procedure - none */
 			   F_ARRAY_SUBSCRIPT_HANDLER,	/* array subscript procedure */
 			   multirangeOid,	/* element type ID */
 			   true,			/* yes this is an array type */
@@ -2301,6 +2354,89 @@ findTypeAnalyzeFunction(List *procname, Oid typeOid)
 	return procOid;
 }
 
+static Oid
+findTypeTypformatoutFunction(List *procname, Oid typeOid)
+{
+	Oid			argList[2];
+	Oid			procOid;
+
+	/*
+	 * typformatout functions must take exactly two arguments: the first is
+	 * the data type being formatted (typeOid), and the second is text (the
+	 * format string). They must return text. given a value of the type and a
+	 * format pattern, produce a text representation.
+	 */
+	argList[0] = typeOid;
+	argList[1] = TEXTOID;
+
+	procOid = LookupFuncName(procname, 2, argList, true);
+	if (!OidIsValid(procOid))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_FUNCTION),
+				errmsg("function %s does not exist",
+					   func_signature_string(procname, 2, NIL, argList)),
+				errhint("A data type typformatout function must accept exactly two arguments: the first must be the data type itself (%s) and the second must be %s (the format pattern)",
+						format_type_be(typeOid),
+						format_type_be(TEXTOID)));
+
+	if (get_func_rettype(procOid) != TEXTOID)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				errmsg("typformatout function %s must return type %s",
+					   NameListToString(procname), format_type_be(TEXTOID)));
+
+	/* Just a warning for now, per comments in findTypeInputFunction */
+	if (func_volatile(procOid) == PROVOLATILE_VOLATILE)
+		ereport(WARNING,
+				errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				errmsg("typformatout function %s should not be volatile",
+					   NameListToString(procname)));
+
+	return procOid;
+}
+
+static Oid
+findTypeTypformatinFunction(List *procname, Oid typeOid)
+{
+	Oid			argList[2];
+	Oid			procOid;
+
+	/*
+	 * typformatin functions always take two text argument and return that
+	 * target data type.
+	 *
+	 * No need to worry about array type, since we can use the array type get
+	 * the array element type and element type typmod
+	 */
+	argList[0] = TEXTOID;
+	argList[1] = TEXTOID;
+
+	procOid = LookupFuncName(procname, 2, argList, true);
+	if (!OidIsValid(procOid))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_FUNCTION),
+				errmsg("function %s does not exist",
+					   func_signature_string(procname, 2, NIL, argList)),
+				errhint("A data type typformatin function must accept two argument of type %s.",
+						format_type_be(TEXTOID)));
+
+	if (get_func_rettype(procOid) != typeOid)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				errmsg("typformatin function %s must return type %s",
+					   NameListToString(procname),
+					   format_type_be(typeOid)));
+
+	/* Just a warning for now, per comments in findTypeInputFunction */
+	if (func_volatile(procOid) == PROVOLATILE_VOLATILE)
+		ereport(WARNING,
+				errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				errmsg("typformatin function %s should not be volatile",
+					   NameListToString(procname)));
+
+	return procOid;
+}
+
 static Oid
 findTypeSubscriptingFunction(List *procname, Oid typeOid)
 {
@@ -4481,6 +4617,32 @@ AlterType(AlterTypeStmt *stmt)
 			/* Replacing an analyze function requires superuser. */
 			requireSuper = true;
 		}
+		else if (strcmp(defel->defname, "typformat_in") == 0)
+		{
+			if (defel->arg != NULL)
+				atparams.typformatinOid =
+					findTypeTypformatinFunction(defGetQualifiedName(defel),
+												typeOid);
+			else
+				atparams.typformatinOid = InvalidOid;	/* NONE, remove function */
+
+			atparams.updateTypformatin = true;
+			/* Replacing an typformat_in function requires superuser. */
+			requireSuper = true;
+		}
+		else if (strcmp(defel->defname, "typformat_out") == 0)
+		{
+			if (defel->arg != NULL)
+				atparams.typformatoutOid =
+					findTypeTypformatoutFunction(defGetQualifiedName(defel),
+												 typeOid);
+			else
+				atparams.typformatoutOid = InvalidOid;	/* NONE, remove function */
+
+			atparams.updateTypformatout = true;
+			/* Replacing an typformat_out function requires superuser. */
+			requireSuper = true;
+		}
 		else if (strcmp(defel->defname, "subscript") == 0)
 		{
 			if (defel->arg != NULL)
@@ -4650,6 +4812,16 @@ AlterTypeRecurse(Oid typeOid, bool isImplicitArray,
 		replaces[Anum_pg_type_typanalyze - 1] = true;
 		values[Anum_pg_type_typanalyze - 1] = ObjectIdGetDatum(atparams->analyzeOid);
 	}
+	if (atparams->updateTypformatin)
+	{
+		replaces[Anum_pg_type_typformatin - 1] = true;
+		values[Anum_pg_type_typformatin - 1] = ObjectIdGetDatum(atparams->typformatinOid);
+	}
+	if (atparams->updateTypformatout)
+	{
+		replaces[Anum_pg_type_typformatout - 1] = true;
+		values[Anum_pg_type_typformatout - 1] = ObjectIdGetDatum(atparams->typformatoutOid);
+	}
 	if (atparams->updateSubscript)
 	{
 		replaces[Anum_pg_type_typsubscript - 1] = true;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 036de5f79ef..beb8a87f1c2 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -3391,6 +3391,83 @@ type_is_collatable(Oid typid)
 	return OidIsValid(get_typcollation(typid));
 }
 
+/*
+ * getTypeInputFormatInfo
+ *
+ *		Get info needed for converting values of a type to internal form base on a format
+ */
+void
+getTypeInputFormatInfo(Oid type, Oid *typInputFormat, bool missing_ok)
+{
+	HeapTuple	typeTuple;
+	Form_pg_type pt;
+
+	typeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(type));
+	if (!HeapTupleIsValid(typeTuple))
+		elog(ERROR, "cache lookup failed for type %u", type);
+	pt = (Form_pg_type) GETSTRUCT(typeTuple);
+
+	if (!pt->typisdefined)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("type %s is only a shell",
+					   format_type_be(type)));
+
+	if (!OidIsValid(pt->typformatin))
+	{
+		if (missing_ok)
+			*typInputFormat = InvalidOid;
+		else
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_FUNCTION),
+					errmsg("no typformatin function available for type %s",
+						   format_type_be(type)));
+	}
+	else
+		*typInputFormat = pt->typformatin;
+
+	ReleaseSysCache(typeTuple);
+}
+
+/*
+ * getTypeInputFormatInfo
+ *
+ *		Get info needed for converting values of a type to text based on a format
+ */
+void
+getTypeOutputFormatInfo(Oid type, Oid *typOutputFormat, bool missing_ok)
+{
+	HeapTuple	typeTuple;
+	Form_pg_type pt;
+
+	typeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(type));
+	if (!HeapTupleIsValid(typeTuple))
+		elog(ERROR, "cache lookup failed for type %u", type);
+	pt = (Form_pg_type) GETSTRUCT(typeTuple);
+
+	if (!pt->typisdefined)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("type %s is only a shell",
+					   format_type_be(type)));
+
+	if (!OidIsValid(pt->typformatout))
+	{
+		if (missing_ok)
+			*typOutputFormat = InvalidOid;
+		else
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_FUNCTION),
+					errmsg("no typformatout function available for type %s",
+						   format_type_be(type)));
+	}
+	else
+		*typOutputFormat = pt->typformatout;
+
+	ReleaseSysCache(typeTuple);
+}
+
+
 
 /*
  * get_typsubscript
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 42ee494601b..dc3ad9715e0 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -287,6 +287,7 @@
 { oid => '1082', array_type_oid => '1182', descr => 'date',
   typname => 'date', typlen => '4', typbyval => 't', typcategory => 'D',
   typinput => 'date_in', typoutput => 'date_out', typreceive => 'date_recv',
+  typformatin => 'to_date',
   typsend => 'date_send', typalign => 'i' },
 { oid => '1083', array_type_oid => '1183', descr => 'time of day',
   typname => 'time', typlen => '8', typbyval => 't', typcategory => 'D',
@@ -350,6 +351,7 @@
   typinput => 'numeric_in', typoutput => 'numeric_out',
   typreceive => 'numeric_recv', typsend => 'numeric_send',
   typmodin => 'numerictypmodin', typmodout => 'numerictypmodout',
+  typformatin => 'to_number',
   typalign => 'i', typstorage => 'm' },
 
 { oid => '1790', array_type_oid => '2201',
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 74183ec5a2e..f84a1a968bc 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -150,6 +150,10 @@ CATALOG(pg_type,1247,TypeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(71,TypeRelati
 	 */
 	regproc		typanalyze BKI_DEFAULT(-) BKI_ARRAY_DEFAULT(array_typanalyze) BKI_LOOKUP_OPT(pg_proc);
 
+	regproc		typformatin BKI_DEFAULT(-) BKI_LOOKUP_OPT(pg_proc);
+
+	regproc		typformatout BKI_DEFAULT(-) BKI_LOOKUP_OPT(pg_proc);
+
 	/* ----------------
 	 * typalign is the alignment required when storing a value of this
 	 * type.  It applies to storage on disk as well as most
@@ -371,6 +375,8 @@ extern ObjectAddress TypeCreate(Oid newTypeOid,
 								Oid typmodinProcedure,
 								Oid typmodoutProcedure,
 								Oid analyzeProcedure,
+								Oid inputformatProcedure,
+								Oid outputformatProcedure,
 								Oid subscriptProcedure,
 								Oid elementType,
 								bool isImplicitArray,
diff --git a/src/test/regress/expected/create_type.out b/src/test/regress/expected/create_type.out
index 5181c4290b4..119e2916c91 100644
--- a/src/test/regress/expected/create_type.out
+++ b/src/test/regress/expected/create_type.out
@@ -93,6 +93,29 @@ CREATE FUNCTION text_w_default_out(text_w_default)
 NOTICE:  argument type text_w_default is only a shell
 LINE 1: CREATE FUNCTION text_w_default_out(text_w_default)
                                            ^
+-- error: typformat_in input argument must be (text, text)
+CREATE TYPE int42 (
+   internallength = 4,
+   input = int42_in,
+   output = int42_out,
+   typformat_in = int42_in,
+   alignment = int4,
+   default = 42,
+   passedbyvalue
+);
+ERROR:  function int42_in(text, text) does not exist
+HINT:  A data type typformatin function must accept two argument of type text.
+-- error: typformat_in result type must be same as the result type
+CREATE TYPE int42 (
+   internallength = 4,
+   input = int42_in,
+   output = int42_out,
+   typformat_in = to_date,
+   alignment = int4,
+   default = 42,
+   passedbyvalue
+);
+ERROR:  typformatin function to_date must return type int42
 CREATE TYPE int42 (
    internallength = 4,
    input = int42_in,
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index d64169b7bf0..0c4d193d82c 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -69,6 +69,8 @@ NOTICE:  checking pg_type {typsend} => pg_proc {oid}
 NOTICE:  checking pg_type {typmodin} => pg_proc {oid}
 NOTICE:  checking pg_type {typmodout} => pg_proc {oid}
 NOTICE:  checking pg_type {typanalyze} => pg_proc {oid}
+NOTICE:  checking pg_type {typformatin} => pg_proc {oid}
+NOTICE:  checking pg_type {typformatout} => pg_proc {oid}
 NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
diff --git a/src/test/regress/sql/create_type.sql b/src/test/regress/sql/create_type.sql
index c25018029c2..8ccf35dbf1b 100644
--- a/src/test/regress/sql/create_type.sql
+++ b/src/test/regress/sql/create_type.sql
@@ -86,6 +86,28 @@ CREATE FUNCTION text_w_default_out(text_w_default)
    AS 'textout'
    LANGUAGE internal STRICT IMMUTABLE;
 
+-- error: typformat_in input argument must be (text, text)
+CREATE TYPE int42 (
+   internallength = 4,
+   input = int42_in,
+   output = int42_out,
+   typformat_in = int42_in,
+   alignment = int4,
+   default = 42,
+   passedbyvalue
+);
+
+-- error: typformat_in result type must be same as the result type
+CREATE TYPE int42 (
+   internallength = 4,
+   input = int42_in,
+   output = int42_out,
+   typformat_in = to_date,
+   alignment = int4,
+   default = 42,
+   passedbyvalue
+);
+
 CREATE TYPE int42 (
    internallength = 4,
    input = int42_in,
-- 
2.34.1

From 3b316fe48bd8a5c1edeb7160f03a97b2bc8da11a Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Mon, 29 Jun 2026 15:35:56 +0800
Subject: [PATCH v9 3/3] CAST(expr AS type FORMAT 'template')

context: https://wiki.postgresql.org/wiki/PostgreSQL_vs_SQL_Standard#Major_features_simply_not_implemented_yet
discussion: https://postgr.es/m/CACJufxGqm7cYQ5C65Eoh1z-f+aMdhv9_7V=[email-masked]
commitfest: https://commitfest.postgresql.org/patch/5957
---
 src/backend/parser/gram.y                     |  19 +
 src/backend/parser/parse_agg.c                |  11 +
 src/backend/parser/parse_coerce.c             | 250 ++++++++++++-
 src/backend/parser/parse_expr.c               |  50 +++
 src/backend/parser/parse_func.c               |   3 +
 src/backend/parser/parse_type.c               |  33 ++
 src/backend/parser/parse_utilcmd.c            |   1 +
 src/backend/utils/adt/ruleutils.c             |  20 +
 src/backend/utils/fmgr/fmgr.c                 |  39 ++
 src/include/fmgr.h                            |   3 +
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/parse_coerce.h             |   6 +
 src/include/parser/parse_node.h               |   1 +
 src/include/parser/parse_type.h               |   3 +
 src/include/utils/lsyscache.h                 |   4 +
 src/test/regress/expected/cast.out            | 345 ++++++++++++++++++
 .../regress/expected/collate.linux.utf8.out   |  39 ++
 src/test/regress/expected/create_type.out     |  78 ++++
 src/test/regress/expected/horology.out        | 133 +++++++
 src/test/regress/expected/interval.out        |  12 +
 src/test/regress/expected/numeric.out         | 147 +++++++-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/cast.sql                 | 123 +++++++
 src/test/regress/sql/collate.linux.utf8.sql   |   8 +
 src/test/regress/sql/create_type.sql          |  58 +++
 src/test/regress/sql/horology.sql             |  42 +++
 src/test/regress/sql/interval.sql             |   2 +
 src/test/regress/sql/numeric.sql              |  26 +-
 28 files changed, 1444 insertions(+), 15 deletions(-)
 create mode 100644 src/test/regress/expected/cast.out
 create mode 100644 src/test/regress/sql/cast.sql

index ff4e1388c55..ce1017bddfe 100644
@@ -16787,6 +16790,8 @@ func_expr_common_subexpr:
@@ -19943,12 +19948,26 @@ makeColumnRef(char *colname, List *indirection,
 	return (Node *) c;
 }
 
+static Node *
 
 	n->arg = arg;
+	n->format = NULL;
 	n->typeName = typename;
 	n->location = location;
 	return (Node *) n;
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index acb933392de..486dd573bbe 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -600,6 +600,14 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 
 			break;
 
+		case EXPR_KIND_TYPECAST_FORMAT:
+			if (isAgg)
+				err = _("aggregate functions are not allowed in CAST FORMAT expressions");
+			else
+				err = _("grouping operations are not allowed in CAST FORMAT expressions");
+
+			break;
+
 			/*
 			 * There is intentionally no default: case here, so that the
 			 * compiler will warn if we add a new ParseExprKind without
@@ -1045,6 +1053,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_FOR_PORTION:
 			err = _("window functions are not allowed in FOR PORTION OF expressions");
 			break;
+		case EXPR_KIND_TYPECAST_FORMAT:
+			err = _("window functions are not allowed in CAST FORMAT expressions");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index e2edde40fc9..c62317f4526 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -23,6 +23,7 @@
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_coerce.h"
+#include "parser/parse_func.h"
 #include "parser/parse_relation.h"
 #include "parser/parse_type.h"
 #include "utils/builtins.h"
@@ -56,7 +57,12 @@ static Node *coerceUnknownConst(ParseState *pstate, Node *node,
 								Oid inputTypeId,
 								Oid targetTypeId, int32 targetTypeMod,
 								CoercionContext ccontext, CoercionForm cformat,
-								int location);
+								Node *format, int location);
+static Node *coerce_type_with_format(ParseState *pstate, Node *node, Node *format,
+									 Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
+									 CoercionContext ccontext, CoercionForm cformat,
+									 Oid formatfunc, int location);
+static Oid	get_coerce_type_formatfunc(Node *expr, Oid exprtype, Oid targettype);

 /*
@@ -136,6 +142,65 @@ coerce_to_target_type(ParseState *pstate, Node *expr, Oid exprtype,
 	return result;
 }
 
+/*
+ * format - cast format template expression, this typically is NULL, but it's
+ * for CAST(expr as type FORMAT 'format_expr') construct.
+*/
+Node *
+coerce_to_target_type_fmt(ParseState *pstate, Node *expr, Oid exprtype,
+						  Oid targettype, int32 targettypmod,
+						  CoercionContext ccontext,
+						  CoercionForm cformat,
+						  int location,
+						  Node *format)
+{
+	Oid			formatfunc = InvalidOid;
+	Node	   *result;
+
+	if (!can_coerce_type(1, &exprtype, &targettype, ccontext))
+		return NULL;
+
+	formatfunc = get_coerce_type_formatfunc(expr, exprtype,
+											targettype);
+
+	if (!OidIsValid(formatfunc))
+		return NULL;
+
+	if (exprType(expr) == UNKNOWNOID && IsA(expr, Const) &&
+		IsA(format, Const))
+		result = coerceUnknownConst(pstate,
+									expr,
+									exprtype,
+									targettype,
+									targettypmod,
+									ccontext,
+									cformat,
+									format,
+									location);
+	else
+		result = coerce_type_with_format(pstate, expr, format,
+										 exprtype,
+										 targettype,
+										 targettypmod,
+										 ccontext,
+										 cformat,
+										 formatfunc,
+										 location);
+
+	/*
+	 * If the target is a fixed-length type, it may need a length coercion as
+	 * well as a type coercion.  If we find ourselves adding both, force the
+	 * inner coercion node to implicit display form.
+	 */
+	result = coerce_type_typmod(result,
+								targettype, targettypmod,
+								ccontext, cformat, location,
+								(result != expr && !IsA(result, Const)));
+
+	return result;
+}
+
+
 
 /*
  * coerce_type()
@@ -243,6 +308,7 @@ coerce_type(ParseState *pstate, Node *node,
 								  targetTypeMod,
 								  ccontext,
 								  cformat,
+								  NULL,
 								  location);
 
 	if (IsA(node, Param) &&
@@ -425,7 +491,7 @@ static Node *
 coerceUnknownConst(ParseState *pstate, Node *node, Oid inputTypeId,
 				   Oid targetTypeId, int32 targetTypeMod,
 				   CoercionContext ccontext, CoercionForm cformat,
-				   int location)
+				   Node *format, int location)
 {
 	Node	   *result;
 
@@ -505,14 +571,38 @@ coerceUnknownConst(ParseState *pstate, Node *node, Oid inputTypeId,
 	 * We assume here that UNKNOWN's internal representation is the same as
 	 * CSTRING.
 	 */
-	if (!con->constisnull)
-		newcon->constvalue = stringTypeDatum(baseType,
-											 DatumGetCString(con->constvalue),
-											 inputTypeMod);
+	if (!format)
+	{
+		if (!con->constisnull)
+			newcon->constvalue = stringTypeDatum(baseType,
+												 DatumGetCString(con->constvalue),
+												 inputTypeMod);
+		else
+			newcon->constvalue = stringTypeDatum(baseType,
+												 NULL,
+												 inputTypeMod);
+	}
 	else
-		newcon->constvalue = stringTypeDatum(baseType,
-											 NULL,
-											 inputTypeMod);
+	{
+		Const	   *fmtcon;
+
+		Assert(IsA(format, Const));
+
+		fmtcon = (Const *) format;
+
+		if (!con->constisnull)
+			newcon->constvalue = stringTypeDatumWithFormat(baseType,
+														   DatumGetCString(con->constvalue),
+														   fmtcon->constvalue,
+														   fmtcon->constisnull,
+														   fmtcon->constcollid);
+		else
+			newcon->constvalue = stringTypeDatumWithFormat(baseType,
+														   NULL,
+														   fmtcon->constvalue,
+														   fmtcon->constisnull,
+														   fmtcon->constcollid);
+	}
 
 	/*
 	 * If it's a varlena value, force it to be in non-expanded (non-toasted)
@@ -567,6 +657,111 @@ coerceUnknownConst(ParseState *pstate, Node *node, Oid inputTypeId,
 	return result;
 }
 
+/*
+ * "node" may contain CollateExpr
+*/
+static Node *
+coerce_type_with_format(ParseState *pstate, Node *node, Node *format,
+						Oid inputTypeId, Oid targetTypeId, int32 targetTypeMod,
+						CoercionContext ccontext, CoercionForm cformat,
+						Oid formatfunc, int location)
+{
+	HeapTuple	tp;
+	Form_pg_proc procstruct;
+	FuncExpr   *fexpr;
+	List	   *args;
+	Oid			baseTypeId;
+	int32		baseTypeMod;
+	Node	   *result;
+	bool		format_for_input;
+	Node	   *expr = node;
+	Node	   *origexpr = node;
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(formatfunc));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for function %u", formatfunc);
+	procstruct = (Form_pg_proc) GETSTRUCT(tp);
+
+	/*
+	 * These Asserts essentially check that function is a legal FORMAT
+	 * coercion function.  We can't make the seemingly obvious tests on
+	 * prorettype and proargtypes[0], even in the COERCION_PATH_FUNC case,
+	 * because of various binary-compatibility cases. TODO: comments need to
+	 * change!
+	 *
+	 * XXX the above comment need change TODO
+	 *
+	 */
+	/* Assert(targetTypeId == procstruct->prorettype); */
+	Assert(!procstruct->proretset);
+	Assert(procstruct->prokind == PROKIND_FUNCTION);
+	Assert(procstruct->pronargs == 2);
+	/* Assert(procstruct->proargtypes.values[0] == exprType(node)); */
+	Assert(procstruct->prorettype == TEXTOID || procstruct->proargtypes.values[1] == TEXTOID);
+
+	if (procstruct->prorettype == TEXTOID)
+		format_for_input = false;
+	else
+		format_for_input = true;
+
+	ReleaseSysCache(tp);
+
+	while (expr && IsA(expr, CollateExpr))
+		expr = (Node *) ((CollateExpr *) expr)->arg;
+
+	/*
+	 * input type format function require the source expression be type TEXT
+	 */
+	if (format_for_input)
+		expr = coerce_type(pstate, expr,
+						   inputTypeId,
+						   TEXTOID, -1,
+						   COERCION_IMPLICIT,
+						   COERCE_IMPLICIT_CAST,
+						   -1);
+
+	if (origexpr && IsA(origexpr, CollateExpr))
+	{
+		CollateExpr *coll = castNode(CollateExpr, origexpr);
+
+		CollateExpr *newcoll = makeNode(CollateExpr);
+
+		newcoll->arg = (Expr *) expr;
+		newcoll->collOid = coll->collOid;
+		newcoll->location = coll->location;
+		expr = (Node *) newcoll;
+	}
+
+	args = list_make1(expr);
+	args = lappend(args, format);
+
+	baseTypeMod = targetTypeMod;
+	baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
+
+	fexpr = makeFuncExpr(formatfunc,
+						 baseTypeId,
+						 args,
+						 InvalidOid,
+						 InvalidOid,
+						 COERCE_SQL_SYNTAX);
+	fexpr->location = location;
+
+	result = (Node *) fexpr;
+
+	if (baseTypeId != targetTypeId)
+		result = coerce_to_domain(result,
+								  baseTypeId,
+								  baseTypeMod,
+								  targetTypeId,
+								  ccontext,
+								  cformat,
+								  location,
+								  true);
+
+	return result;
+}
+
+
 
 /*
  * can_coerce_type()
@@ -3425,3 +3620,40 @@ typeIsOfTypedTable(Oid reltypeId, Oid reloftypeId)
 
 	return result;
 }
+
+static Oid
+get_coerce_type_formatfunc(Node *expr, Oid exprtype, Oid targettype)
+{
+	Oid			baseTypeId = getBaseType(exprtype);
+	Oid			targetbaseTypeId = getBaseType(targettype);
+	char		src_category;
+	bool		preferred1;
+	char		dst_category;
+	bool		preferred2;
+	Oid			formatfunc = InvalidOid;
+
+	while (expr && IsA(expr, CollateExpr))
+		expr = (Node *) ((CollateExpr *) expr)->arg;
+
+	if (IsA(expr, Const) && exprtype == UNKNOWNOID)
+		baseTypeId = TEXTOID;
+
+	if (baseTypeId == targetbaseTypeId)
+		return InvalidOid;
+
+	get_type_category_preferred(baseTypeId, &src_category, &preferred1);
+
+	get_type_category_preferred(targetbaseTypeId, &dst_category, &preferred2);
+
+	if (src_category == TYPCATEGORY_STRING && dst_category == TYPCATEGORY_STRING)
+		return InvalidOid;		/* TODO: maybe not return NULL */
+	else if (src_category != TYPCATEGORY_STRING && dst_category != TYPCATEGORY_STRING)
+		return InvalidOid;
+
+	if (dst_category == TYPCATEGORY_STRING)
+		getTypeOutputFormatInfo(baseTypeId, &formatfunc, true);
+	else
+		getTypeInputFormatInfo(targetbaseTypeId, &formatfunc, true);
+
+	return formatfunc;
+}
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 9adc9d4c0f6..f05fc2d0134 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -579,6 +579,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_GENERATED_COLUMN:
 		case EXPR_KIND_CYCLE_MARK:
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
+		case EXPR_KIND_TYPECAST_FORMAT:
 			/* okay */
 			break;
 
@@ -1892,6 +1893,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("cannot use subquery in FOR PORTION OF expression");
 			break;
+		case EXPR_KIND_TYPECAST_FORMAT:
+			err = _("cannot use subquery in CAST FORMAT expression");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -2795,6 +2799,50 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
 	if (location < 0)
 		location = tc->typeName->location;
 
+	if (tc->format)
+	{
+		Node	   *format = NULL;
+
+		int			fmtlocation = exprLocation(tc->format);
+
+		format = transformExpr(pstate, tc->format, EXPR_KIND_TYPECAST_FORMAT);
+
+		format = coerce_to_target_type(pstate, format,
+									   exprType(format), TEXTOID, -1,
+									   COERCION_IMPLICIT,
+									   COERCE_IMPLICIT_CAST,
+									   fmtlocation);
+		if (format == NULL)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot coerce FORMAT expression to type %s",
+						   format_type_be(TEXTOID)),
+					parser_errposition(pstate, fmtlocation));
+
+		assign_expr_collations(pstate, expr);
+
+		assign_expr_collations(pstate, format);
+
+		result = coerce_to_target_type_fmt(pstate, expr, inputType,
+										   targetType, targetTypmod,
+										   COERCION_EXPLICIT,
+										   COERCE_EXPLICIT_CAST,
+										   location,
+										   format);
+		if (result)
+			return result;
+
+		if (inputType == UNKNOWNOID && IsA(expr, Const))
+			inputType = TEXTOID;
+
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot cast type %s to %s using CAST( ... AS ... FORMAT ...)",
+					   format_type_be(inputType),
+					   format_type_be(targetType)),
+				parser_coercion_errposition(pstate, exprLocation(format), format));
+	}
+
 	result = coerce_to_target_type(pstate, expr, inputType,
 								   targetType, targetTypmod,
 								   COERCION_EXPLICIT,
@@ -3253,6 +3301,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "CYCLE";
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			return "property definition expression";
+		case EXPR_KIND_TYPECAST_FORMAT:
+			return "CAST FORMAT expression";
 		case EXPR_KIND_FOR_PORTION:
 			return "FOR PORTION OF";
 
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index fb306c05112..918353c43ab 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2797,6 +2797,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("set-returning functions are not allowed in FOR PORTION OF expressions");
 			break;
+		case EXPR_KIND_TYPECAST_FORMAT:
+			err = _("set-returning functions are not allowed in CAST FORMAT expressions");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_type.c b/src/backend/parser/parse_type.c
index bb7eccde9fd..1157b7f5aad 100644
--- a/src/backend/parser/parse_type.c
+++ b/src/backend/parser/parse_type.c
@@ -660,6 +660,39 @@ stringTypeDatum(Type tp, char *string, int32 atttypmod)
 	return OidInputFunctionCall(typinput, string, typioparam, atttypmod);
 }
 
+
+/*
+ * Given a type structure and a string, returns the internal representation
+ * of that string.  The "string" can be NULL to perform conversion of a NULL
+ * (which might result in failure, if the input function rejects NULLs).
+ *
+ * XXX this will use FORMAT expression's collation, because cstring don't have collation.
+ */
+Datum
+stringTypeDatumWithFormat(Type tp, char *string,
+						  Datum fmt, bool fmtisnull,
+						  Oid fmtcollation)
+{
+	FmgrInfo	flinfo;
+	Form_pg_type typform = (Form_pg_type) GETSTRUCT(tp);
+	Oid			typformtin = typform->typformatin;
+
+	if (!OidIsValid(typformtin))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot cast type %s to %s using CAST( ... AS ... FORMAT ...)",
+					   format_type_be(TEXTOID),
+					   format_type_be(typform->oid)),
+				errhint("Ensure type %s have valid typformatin function",
+						format_type_be(typform->oid)));
+
+	fmgr_info(typformtin, &flinfo);
+
+	return InputFunctionCallWithFormat(&flinfo, string, fmt, fmtisnull,
+									   fmtcollation);
+}
+
+
 /*
  * Given a typeid, return the type's typrelid (associated relation), if any.
  * Returns InvalidOid if type is not a composite type.
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index a049cc67ed6..8e947b85277 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -680,6 +680,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 		castnode = makeNode(TypeCast);
 		castnode->typeName = SystemTypeName("regclass");
 		castnode->arg = (Node *) snamenode;
+		castnode->format = NULL;
 		castnode->location = -1;
 		funccallnode = makeFuncCall(SystemFuncName("nextval"),
 									list_make1(castnode),
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 88de5c0481c..5c3174bfe70 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -11980,6 +11980,26 @@ get_func_sql_syntax(FuncExpr *expr, deparse_context *context)
 			get_rule_expr((Node *) lsecond(expr->args), context, false);
 			appendStringInfoString(buf, "))");
 			return true;
+		case F_TO_CHAR_INT4_TEXT:
+		case F_TO_CHAR_INT8_TEXT:
+		case F_TO_CHAR_FLOAT4_TEXT:
+		case F_TO_CHAR_FLOAT8_TEXT:
+		case F_TO_CHAR_NUMERIC_TEXT:
+		case F_TO_CHAR_INTERVAL_TEXT:
+		case F_TO_CHAR_TIMESTAMP_TEXT:
+		case F_TO_CHAR_TIMESTAMPTZ_TEXT:
+		case F_TO_NUMBER:
+		case F_TO_TIMESTAMP_TEXT_TEXT:
+		case F_TO_DATE:
+			/* CAST FORMAT */
+			appendStringInfoString(buf, "CAST(");
+			get_rule_expr((Node *) linitial(expr->args), context, false);
+			appendStringInfoString(buf, " AS ");
+			appendStringInfoString(buf, format_type_be(expr->funcresulttype));
+			appendStringInfoString(buf, " FORMAT ");
+			get_rule_expr((Node *) lsecond(expr->args), context, false);
+			appendStringInfoChar(buf, ')');
+			return true;
 	}
 	return false;
 }
diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c
index bfeceb7a92f..3a275e714a2 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -1565,6 +1565,45 @@ InputFunctionCall(FmgrInfo *flinfo, char *str, Oid typioparam, int32 typmod)
 	return result;
 }
 
+Datum
+InputFunctionCallWithFormat(FmgrInfo *flinfo, char *str,
+							Datum format, bool fmtisnull,
+							Oid fmtcollation)
+{
+	Datum		result;
+
+	LOCAL_FCINFO(fcinfo, 2);
+
+	if ((str == NULL || fmtisnull) && flinfo->fn_strict)
+		return (Datum) 0;		/* just return null result */
+
+	InitFunctionCallInfoData(*fcinfo, flinfo, 2, fmtcollation, NULL, NULL);
+
+	fcinfo->args[0].value = CStringGetTextDatum(str);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = format;
+	fcinfo->args[1].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	/* Should get null result if and only if str is NULL */
+	if (str == NULL)
+	{
+		if (!fcinfo->isnull)
+			elog(ERROR, "input function %u returned non-NULL",
+				 flinfo->fn_oid);
+	}
+	else
+	{
+		if (fcinfo->isnull)
+			elog(ERROR, "input function %u returned NULL",
+				 flinfo->fn_oid);
+	}
+
+	return result;
+}
+
+
 /*
  * Call a previously-looked-up datatype input function, with non-exception
  * handling of "soft" errors.
diff --git a/src/include/fmgr.h b/src/include/fmgr.h
index 38e143ac670..8e15cb4ed17 100644
--- a/src/include/fmgr.h
+++ b/src/include/fmgr.h
@@ -746,6 +746,9 @@ extern Datum OidFunctionCall9Coll(Oid functionId, Oid collation,
 /* Special cases for convenient invocation of datatype I/O functions. */
 extern Datum InputFunctionCall(FmgrInfo *flinfo, char *str,
 							   Oid typioparam, int32 typmod);
+extern Datum InputFunctionCallWithFormat(FmgrInfo *flinfo, char *str,
+										 Datum format, bool fmtisnull,
+										 Oid fmtcollation);
 extern bool InputFunctionCallSafe(FmgrInfo *flinfo, char *str,
 								  Oid typioparam, int32 typmod,
 								  Node *escontext,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 4133c404a6b..03ed3684e2e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -402,6 +402,7 @@ typedef struct TypeCast
 	NodeTag		type;
 	Node	   *arg;			/* the expression being casted */
 	TypeName   *typeName;		/* the target type */
+	Node	   *format;			/* the cast format template expression */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } TypeCast;
 
diff --git a/src/include/parser/parse_coerce.h b/src/include/parser/parse_coerce.h
index aabacd49b65..5a021fd6386 100644
--- a/src/include/parser/parse_coerce.h
+++ b/src/include/parser/parse_coerce.h
@@ -43,6 +43,12 @@ extern Node *coerce_to_target_type(ParseState *pstate,
 								   CoercionContext ccontext,
 								   CoercionForm cformat,
 								   int location);
+extern Node *coerce_to_target_type_fmt(ParseState *pstate, Node *expr, Oid exprtype,
+									   Oid targettype, int32 targettypmod,
+									   CoercionContext ccontext,
+									   CoercionForm cformat,
+									   int location,
+									   Node *format);
 extern bool can_coerce_type(int nargs, const Oid *input_typeids, const Oid *target_typeids,
 							CoercionContext ccontext);
 extern Node *coerce_type(ParseState *pstate, Node *node,
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index f7f4ba6c2a8..68aec0c689e 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -84,6 +84,7 @@ typedef enum ParseExprKind
 	EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */
 	EXPR_KIND_CYCLE_MARK,		/* cycle mark value */
 	EXPR_KIND_PROPGRAPH_PROPERTY,	/* derived property expression */
+	EXPR_KIND_TYPECAST_FORMAT,	/* CAST FORMAT */
 } ParseExprKind;

diff --git a/src/include/parser/parse_type.h b/src/include/parser/parse_type.h
index a335807b0b0..a6e4827951e 100644
--- a/src/include/parser/parse_type.h
+++ b/src/include/parser/parse_type.h
@@ -47,6 +47,9 @@ extern char *typeTypeName(Type t);
 extern Oid	typeTypeRelid(Type typ);
 extern Oid	typeTypeCollation(Type typ);
 extern Datum stringTypeDatum(Type tp, char *string, int32 atttypmod);
+extern Datum stringTypeDatumWithFormat(Type tp, char *string,
+									   Datum fmt, bool fmtisnull,
+									   Oid fmtcollation);
 
 extern Oid	typeidTypeRelid(Oid type_id);
 extern Oid	typeOrDomainTypeRelid(Oid type_id);
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 865980cb0f1..7884b54fa6d 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -190,6 +190,10 @@ extern void getTypeBinaryOutputInfo(Oid type, Oid *typSend, bool *typIsVarlena);
 extern Oid	get_typmodin(Oid typid);
 extern Oid	get_typcollation(Oid typid);
 extern bool type_is_collatable(Oid typid);
+extern void getTypeInputFormatInfo(Oid type, Oid *typInputFormat,
+								   bool missing_ok);
+extern void getTypeOutputFormatInfo(Oid type, Oid *typOutputFormat,
+									bool missing_ok);
 extern RegProcedure get_typsubscript(Oid typid, Oid *typelemp);
 extern const SubscriptRoutines *getSubscriptingRoutines(Oid typid,
 														Oid *typelemp);
diff --git a/src/test/regress/expected/cast.out b/src/test/regress/expected/cast.out
new file mode 100644
index 00000000000..8ee7978f870
--- /dev/null
+++ b/src/test/regress/expected/cast.out
@@ -0,0 +1,345 @@
+create function ret_settxt() returns setof text as
+$$
+begin
+    return query execute 'select 1 union all select 1';
+end;
+$$
+language plpgsql immutable;
+-- check CAST FORMAT expression, the following should all fail
+select cast(NULL as date format ret_settxt()); -- cannot return a set
+ERROR:  set-returning functions are not allowed in CAST FORMAT expressions
+LINE 1: select cast(NULL as date format ret_settxt());
+                                        ^
+select cast(NULL as date format (select 1::text where false));
+ERROR:  cannot use subquery in CAST FORMAT expression
+LINE 1: select cast(NULL as date format (select 1::text where false)...
+                                        ^
+select cast(NULL as date format (string_agg(NULL, ' ')));
+ERROR:  aggregate functions are not allowed in CAST FORMAT expressions
+LINE 1: select cast(NULL as date format (string_agg(NULL, ' ')));
+                                         ^
+select cast(NULL as date format (string_agg(NULL, ' ') over () ));
+ERROR:  window functions are not allowed in CAST FORMAT expressions
+LINE 1: select cast(NULL as date format (string_agg(NULL, ' ') over ...
+                                         ^
+select cast(NULL as date format NULL::int);
+ERROR:  cannot coerce FORMAT expression to type text
+LINE 1: select cast(NULL as date format NULL::int);
+                                        ^
+select cast('1' as date format B'01');
+ERROR:  cannot coerce FORMAT expression to type text
+LINE 1: select cast('1' as date format B'01');
+                                       ^
+-- CAST FORMAT is restricted to the source and target types used by to_char, to_date, to_timestamp, and to_number
+-- The following should all fail
+select cast('hello' as name format 'test');
+ERROR:  cannot cast type text to name using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('hello' as name format 'test');
+                                           ^
+select cast('hello' as bpchar format 'test');
+ERROR:  cannot cast type text to character using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('hello' as bpchar format 'test');
+                                             ^
+select cast('-34,338,492' as bigint format '99G999G999');
+ERROR:  cannot cast type text to bigint using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('-34,338,492' as bigint format '99G999G999');
+                                                   ^
+select cast(array[1] as text format 'YYYY');
+ERROR:  cannot cast type integer[] to text using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast(array[1] as text format 'YYYY');
+                                            ^
+select cast('1' as timestamp[] format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to timestamp without time zone[] using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('1' as timestamp[] format 'YYYY-MM-DD');
+                                              ^
+select cast('2012-13-12' as timestamp format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to timestamp without time zone using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('2012-13-12' as timestamp format 'YYYY-MM-DD');
+                                                     ^
+select cast('2012-13-12' as time format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to time without time zone using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('2012-13-12' as time format 'YYYY-MM-DD');
+                                                ^
+select cast('2012-13-12' as timetz format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to time with time zone using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('2012-13-12' as timetz format 'YYYY-MM-DD');
+                                                  ^
+select cast('2012-13-12' as interval format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to interval using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('2012-13-12' as interval format 'YYYY-MM-DD');
+                                                    ^
+select cast('1'::text as unknown format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to unknown using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('1'::text as unknown format 'YYYY-MM-DD');
+                                                ^
+select cast('1' as bool format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to boolean using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('1' as bool format 'YYYY-MM-DD');
+                                       ^
+select cast('1' as json format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to json using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('1' as json format 'YYYY-MM-DD');
+                                       ^
+select cast('1'::json as text format 'YYYY-MM-DD');
+ERROR:  cannot cast type json to text using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('1'::json as text format 'YYYY-MM-DD');
+                                             ^
+select cast('1' as anyelement format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to anyelement using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('1' as anyelement format 'YYYY-MM-DD');
+                                             ^
+select cast('1' as anyenum format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to anyenum using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('1' as anyenum format 'YYYY-MM-DD');
+                                          ^
+select cast('1' as anyarray format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to anyarray using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('1' as anyarray format 'YYYY-MM-DD');
+                                           ^
+select cast(null::anyelement as anyelement format 'YYYY-MM-DD');
+ERROR:  cannot cast type text to anyelement using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast(null::anyelement as anyelement format 'YYYY-MM-D...
+                                                          ^
+select cast('2012-12-12 12:00'::timetz as text format 'YYYY-MM-DD HH:MI:SS TZ');
+ERROR:  cannot cast type time with time zone to text using CAST( ... AS ... FORMAT ...)
+LINE 1: ...ct cast('2012-12-12 12:00'::timetz as text format 'YYYY-MM-D...
+                                                             ^
+select cast(null::regclass as text format 'YYYY-MM-DD HH:MI:SS TZ');
+ERROR:  cannot cast type regclass to text using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast(null::regclass as text format 'YYYY-MM-DD HH:MI:...
+                                                  ^
+select cast(null::int2 as numeric format null);
+ERROR:  cannot cast type smallint to numeric using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast(null::int2 as numeric format null);
+                                                 ^
+select cast(null::date as timestamptz format null);
+ERROR:  cannot cast type date to timestamp with time zone using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast(null::date as timestamptz format null);
+                                                     ^
+select cast(null::time as timestamptz format null);
+ERROR:  cannot cast type time without time zone to timestamp with time zone using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast(null::time as timestamptz format null);
+                                                     ^
+select cast('hello'::text collate "C" as date format 'test' collate "POSIX"); -- error
+ERROR:  collation mismatch between explicit collations "C" and "POSIX"
+LINE 1: ...t('hello'::text collate "C" as date format 'test' collate "P...
+                                                             ^
+select cast('hello'::text collate "C" as date format 'test' collate "POSIX"); -- error
+ERROR:  collation mismatch between explicit collations "C" and "POSIX"
+LINE 1: ...t('hello'::text collate "C" as date format 'test' collate "P...
+                                                             ^
+-- CAST FORMAT is not supported for binary coercible type cast
+select cast('2022-01-01' as unknown format null);
+ERROR:  cannot cast type text to unknown using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('2022-01-01' as unknown format null);
+                                                   ^
+select cast('1' as text format '1'::text);
+ERROR:  cannot cast type text to text using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('1' as text format '1'::text);
+                                       ^
+select cast('1'::text as text format '1'::text);
+ERROR:  cannot cast type text to text using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('1'::text as text format '1'::text);
+                                             ^
+select cast('2012-12-12 12:00'::timetz as text format 'YYYY-MM-DD HH:MI:SS TZ'); -- error
+ERROR:  cannot cast type time with time zone to text using CAST( ... AS ... FORMAT ...)
+LINE 1: ...ct cast('2012-12-12 12:00'::timetz as text format 'YYYY-MM-D...
+                                                             ^
+select cast('2012-13-12' as date format 'YYYY-DD-MM'); -- ok
+    date    
+------------
+ 12-13-2012
+(1 row)
+
+select cast('2012-13-12' as date format 'YYYY-MM-DD'); -- error
+ERROR:  date/time field value out of range: "2012-13-12"
+LINE 1: select cast('2012-13-12' as date format 'YYYY-MM-DD');
+                    ^
+select cast('1' as date format 'YYYY-MM-DD');
+    date    
+------------
+ 01-01-0001
+(1 row)
+
+select cast('1' collate "C" as date format 'YYYY-MM-DD');
+    date    
+------------
+ 01-01-0001
+(1 row)
+
+select cast('2012-13-12' as date format 'YYYY-DD-MM') as date;
+    date    
+------------
+ 12-13-2012
+(1 row)
+
+select cast('2012-13-12' as timestamptz format 'YYYY-DD-MM') as date;
+             date             
+------------------------------
+ Thu Dec 13 00:00:00 2012 PST
+(1 row)
+
+select cast('2012-13-12'::text as timestamp format 'YYYY-DD-MM') as date; -- error
+ERROR:  cannot cast type text to timestamp without time zone using CAST( ... AS ... FORMAT ...)
+LINE 1: ...elect cast('2012-13-12'::text as timestamp format 'YYYY-DD-M...
+                                                             ^
+select cast('1' as timestamp format 'YYYY-MM-DD') = to_timestamp('1', 'YYYY-MM-DD');
+ERROR:  cannot cast type text to timestamp without time zone using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('1' as timestamp format 'YYYY-MM-DD') = to_times...
+                                            ^
+select cast('2026-01-28 13:29:12.324606+01'::text as timestamp format 'YYYY-MM-DD') =
+            to_timestamp('2026-01-28 13:29:12.324606+01'::text, 'YYYY-MM-DD');
+ERROR:  cannot cast type text to timestamp without time zone using CAST( ... AS ... FORMAT ...)
+LINE 1: ...-28 13:29:12.324606+01'::text as timestamp format 'YYYY-MM-D...
+                                                             ^
+select cast('1' as date format 'YYYY-MM-DD') = to_date('1', 'YYYY-MM-DD');
+ ?column? 
+----------
+ t
+(1 row)
+
+-- test with domain
+create domain d1 as date check (value <> '0001-01-01');
+create domain d2 as text check (value <> '125.80-');
+select cast(-125.8::numeric as text format '999D99S');
+  text   
+---------
+ 125.80-
+(1 row)
+
+select cast(-125.8::numeric as d2 format '999D99S');
+ERROR:  value for domain d2 violates check constraint "d2_check"
+select cast('1' as text format 'YYYY-MM-DD'); -- error
+ERROR:  cannot cast type text to text using CAST( ... AS ... FORMAT ...)
+LINE 1: select cast('1' as text format 'YYYY-MM-DD');
+                                       ^
+select cast('1' as d1 format 'YYYY-MM-DD'); -- error
+ERROR:  value for domain d1 violates check constraint "d1_check"
+select cast('1' as date format 'MM-DD'); -- ok
+     date      
+---------------
+ 01-01-0001 BC
+(1 row)
+
+select cast('1' as d1 format 'MM-DD'); -- ok
+      d1       
+---------------
+ 01-01-0001 BC
+(1 row)
+
+create temp table tcast0 as select '1'::text as a;
+select cast(a as d1 format 'YYYY-MM-DD') from tcast0;
+ERROR:  value for domain d1 violates check constraint "d1_check"
+select cast('1'::text collate "C" as date format 'YYYY-MM-DD');
+    date    
+------------
+ 01-01-0001
+(1 row)
+
+select cast('1' as date format 'YYYY-MM-DD') = to_date('1', 'YYYY-MM-DD') as expect_true;
+ expect_true 
+-------------
+ t
+(1 row)
+
+create table tcast(col1 text, col2 text, col3 date, col4 timestamptz, col5 int8);
+insert into tcast(col1, col2, col5)
+  values('2022-12-13', 'YYYY-MM-DD', 1234),
+        ('2022-12-01', 'YYYY-DD-MM', -1234);
+select cast(col1 as date format col2) from tcast;
+    col1    
+------------
+ 12-13-2022
+ 01-12-2022
+(2 rows)
+
+select cast(col1 as date format col3) from tcast; -- error
+ERROR:  cannot coerce FORMAT expression to type text
+LINE 1: select cast(col1 as date format col3) from tcast;
+                                        ^
+select cast(col1 as date format col3::text) from tcast; -- ok
+ col1 
+------
+ 
+ 
+(2 rows)
+
+create function imm_const() returns text as $$ begin return 'YYYY-MM-DD'; end; $$ language plpgsql immutable;
+select cast(col1 as date format imm_const()) from tcast;
+    col1    
+------------
+ 12-13-2022
+ 12-01-2022
+(2 rows)
+
+create index s1 on tcast(to_date(col1, 'YYYY-MM-DD')); -- error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index s1 on tcast(to_date(col1, 'YYYY-MM-DD'));
+                                 ^
+create index s1 on tcast(cast(col1 as date format 'YYYY-MM-DD')); -- error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index s1 on tcast(cast(col1 as date format 'YYYY-MM-D...
+                                 ^
+create view tcast_v1 as
+   select cast(col1 as date format 'YYYY-MM-DD') as to_date,
+         cast(col1 as timestamptz format 'YYYY-MM-DD') as to_timestamptz,
+         cast(NULL::interval as text format 'YYYY-MM-DD') as to_txt0,
+         cast(col1::timestamp as text format 'YYYY-MM-DD') as to_txt1,
+      --    cast(col3 as text format 'YYYY-MM-DD') as to_txt2,
+         cast(col4 as text format 'YYYY-MM-DD') as to_txt3,
+         cast(numeric 'inf' as text format 'YYYY-MM-DD') as to_txt4,
+         cast(bigint '12324' as text format 'YYYY-MM-DD') as to_txt5
+   from tcast;
+select pg_get_viewdef('tcast_v1', true);
+                                      pg_get_viewdef                                       
+-------------------------------------------------------------------------------------------
+  SELECT CAST(col1 AS date FORMAT 'YYYY-MM-DD'::text) AS to_date,                         +
+     CAST(col1 AS timestamp with time zone FORMAT 'YYYY-MM-DD'::text) AS to_timestamptz,  +
+     CAST(NULL::interval AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt0,                   +
+     CAST(col1::timestamp without time zone AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt1,+
+     CAST(col4 AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt3,                             +
+     CAST('Infinity'::numeric AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt4,              +
+     CAST('12324'::bigint AS text FORMAT 'YYYY-MM-DD'::text) AS to_txt5                   +
+    FROM tcast;
+(1 row)
+
+explain (verbose, costs off)
+select cast(col5::float8 as text format '9.99EEEE') as to_txt1,
+ cast(col5::numeric as text format '9.99EEEE') as to_txt2
+from tcast;
+                                                           QUERY PLAN                                                            
+---------------------------------------------------------------------------------------------------------------------------------
+ Seq Scan on public.tcast
+   Output: CAST((col5)::double precision AS text FORMAT '9.99EEEE'::text), CAST((col5)::numeric AS text FORMAT '9.99EEEE'::text)
+(2 rows)
+
+create view tcast_v2 as
+   select cast(col5 as text format '9.99EEEE') as to_txt0,
+         cast(col5::float8 as text format '9.99EEEE') as to_txt1,
+         cast(col5::float4 as text format '9.99EEEE') as to_txt2,
+         cast(col5::numeric as text format '9.99EEEE') as to_txt3,
+      --    cast(col5::int2 as text format '9.99EEEE') as to_txt4,
+         cast(col5::int4 as text format '9.99EEEE') as to_txt5
+   from tcast;
+select pg_get_viewdef('tcast_v2', true);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT CAST(col5 AS text FORMAT '9.99EEEE'::text) AS to_txt0,              +
+     CAST(col5::double precision AS text FORMAT '9.99EEEE'::text) AS to_txt1,+
+     CAST(col5::real AS text FORMAT '9.99EEEE'::text) AS to_txt2,            +
+     CAST(col5::numeric AS text FORMAT '9.99EEEE'::text) AS to_txt3,         +
+     CAST(col5::integer AS text FORMAT '9.99EEEE'::text) AS to_txt5          +
+    FROM tcast;
+(1 row)
+
+select * from tcast_v2;
+  to_txt0  |  to_txt1  |  to_txt2  |  to_txt3  |  to_txt5  
+-----------+-----------+-----------+-----------+-----------
+  1.23e+03 |  1.23e+03 |  1.23e+03 |  1.23e+03 |  1.23e+03
+ -1.23e+03 | -1.23e+03 | -1.23e+03 | -1.23e+03 | -1.23e+03
+(2 rows)
+
+drop function ret_settxt;
+drop view tcast_v1, tcast_v2;
+drop table tcast;
+drop domain d1, d2;
diff --git a/src/test/regress/expected/collate.linux.utf8.out b/src/test/regress/expected/collate.linux.utf8.out
index c6e84c27b69..129130a4f1b 100644
--- a/src/test/regress/expected/collate.linux.utf8.out
+++ b/src/test/regress/expected/collate.linux.utf8.out
@@ -463,6 +463,30 @@ SELECT to_char(date '2010-04-01', 'DD TMMON YYYY' COLLATE "tr_TR");
  01 NİS 2010
 (1 row)
 
+SELECT CAST(date '2010-02-01' as text format 'DD TMMON YYYY');
+    text     
+-------------
+ 01 ŞUB 2010
+(1 row)
+
+SELECT CAST(date '2010-02-01' as text format 'DD TMMON YYYY' COLLATE "tr_TR");
+    text     
+-------------
+ 01 ŞUB 2010
+(1 row)
+
+SELECT CAST(date '2010-04-01' as text format 'DD TMMON YYYY');
+    text     
+-------------
+ 01 NIS 2010
+(1 row)
+
+SELECT CAST(date '2010-04-01' as text format  'DD TMMON YYYY' COLLATE "tr_TR");
+    text     
+-------------
+ 01 NİS 2010
+(1 row)
+
 -- to_date
 SELECT to_date('01 ŞUB 2010', 'DD TMMON YYYY');
   to_date   
@@ -479,6 +503,21 @@ SELECT to_date('01 Şub 2010', 'DD TMMON YYYY');
 SELECT to_date('1234567890ab 2010', 'TMMONTH YYYY'); -- fail
 ERROR:  invalid value "1234567890ab" for "MONTH"
 DETAIL:  The given value did not match any of the allowed values for this field.
+SELECT CAST('01 ŞUB 2010' as date format 'DD TMMON YYYY');
+    date    
+------------
+ 02-01-2010
+(1 row)
+
+SELECT CAST('01 ŞUB 2010' as date format 'DD TMMON YYYY'); -- ok
+    date    
+------------
+ 02-01-2010
+(1 row)
+
+SELECT CAST('1234567890ab 2010' as date format 'TMMONTH YYYY'); -- fail
+ERROR:  invalid value "1234567890ab" for "MONTH"
+DETAIL:  The given value did not match any of the allowed values for this field.
 -- backwards parsing
 CREATE VIEW collview1 AS SELECT * FROM collate_test1 WHERE b COLLATE "C" >= 'bbc';
 CREATE VIEW collview2 AS SELECT a, b FROM collate_test1 ORDER BY b COLLATE "C";
diff --git a/src/test/regress/expected/create_type.out b/src/test/regress/expected/create_type.out
index 119e2916c91..e05fc12e548 100644
--- a/src/test/regress/expected/create_type.out
+++ b/src/test/regress/expected/create_type.out
@@ -124,6 +124,15 @@ CREATE TYPE int42 (
    default = 42,
    passedbyvalue
 );
+CREATE OR REPLACE FUNCTION int42_fmtout(int42, text)
+RETURNS TEXT AS
+$$
+   BEGIN
+      RETURN TO_CHAR($1::text::int, $2);
+   END
+$$
+LANGUAGE PLPGSQL STRICT IMMUTABLE;
+ALTER TYPE int42 SET (TYPFORMAT_OUT = int42_fmtout);
 CREATE TYPE text_w_default (
    internallength = variable,
    input = text_w_default_in,
@@ -139,6 +148,32 @@ SELECT * FROM default_test;
  zippo | 42
 (1 row)
 
+SELECT CAST('42'::text::int42 AS TEXT FORMAT 'EEEE9'); -- error
+ERROR:  "EEEE" must be the last pattern used
+CONTEXT:  PL/pgSQL function int42_fmtout(int42,text) line 3 at RETURN
+SELECT CAST('42'::text::int42 AS TEXT FORMAT 'S999S'); -- error
+ERROR:  cannot use "S" twice
+CONTEXT:  PL/pgSQL function int42_fmtout(int42,text) line 3 at RETURN
+SELECT CAST('42'::text::int42 AS TEXT FORMAT '9.9V9'); -- error
+ERROR:  cannot use "V" and decimal point together
+CONTEXT:  PL/pgSQL function int42_fmtout(int42,text) line 3 at RETURN
+SELECT CAST('42'::text::int42 AS TEXT FORMAT 'RNEEEE'); -- error
+ERROR:  "EEEE" is incompatible with other formats
+DETAIL:  "EEEE" may only be used together with digit and decimal point patterns.
+CONTEXT:  PL/pgSQL function int42_fmtout(int42,text) line 3 at RETURN
+SELECT CAST('42'::text::int42 AS TEXT FORMAT '9.9.9'); -- error
+ERROR:  multiple decimal points
+CONTEXT:  PL/pgSQL function int42_fmtout(int42,text) line 3 at RETURN
+SELECT CAST(g::text::int42 AS TEXT FORMAT 'FM000') FROM (VALUES (-1), (0), (5), (100), (99)) s(g);
+  g   
+------
+ -001
+ 000
+ 005
+ 100
+ 099
+(5 rows)
+
 -- We need a shell type to test some CREATE TYPE failure cases with
 CREATE TYPE bogus_type;
 -- invalid: non-lowercase quoted identifiers
@@ -209,6 +244,49 @@ ERROR:  type "text_w_default" already exists
 DROP TYPE default_test_row CASCADE;
 NOTICE:  drop cascades to function get_default_test()
 DROP TABLE default_test;
+-- CAST(expr as type FORMAT 'fmt') with user-defined types
+CREATE TYPE datealilas;
+CREATE FUNCTION datealilas_in(cstring)
+   RETURNS datealilas
+   AS 'date_in'
+   LANGUAGE internal STRICT IMMUTABLE;
+NOTICE:  return type datealilas is only a shell
+CREATE FUNCTION datealilas_out(datealilas)
+   RETURNS cstring
+   AS 'date_out'
+   LANGUAGE internal STRICT IMMUTABLE;
+NOTICE:  argument type datealilas is only a shell
+LINE 1: CREATE FUNCTION datealilas_out(datealilas)
+                                       ^
+CREATE TYPE datealilas (
+   internallength = 4,
+   input = datealilas_in,
+   output = datealilas_out,
+   alignment = int4,
+   passedbyvalue
+);
+CREATE OR REPLACE FUNCTION datealilasfmtin(TEXT, TEXT)
+RETURNS datealilas AS
+$$
+   BEGIN
+      RETURN TO_DATE($1, $2)::text::datealilas;
+   END
+$$
+LANGUAGE PLPGSQL STRICT IMMUTABLE;
+ALTER TYPE datealilas SET (TYPFORMAT_IN = datealilasfmtin);
+CREATE TEMP TABLE castfmt(a0 int2 default 2, a datealilas, b int default 3);
+INSERT INTO castfmt(a)
+   SELECT cast('2022-01-13' AS datealilas FORMAT 'YYYY-MM-DD')
+   UNION ALL
+   SELECT cast('2022-13-11' AS datealilas FORMAT 'YYYY-DD-MM');
+SELECT CAST('2022-01-13' AS datealilas FORMAT 'YYYY-DD-MM'); -- error
+ERROR:  date/time field value out of range: "2022-01-13"
+LINE 1: SELECT CAST('2022-01-13' AS datealilas FORMAT 'YYYY-DD-MM');
+                    ^
+CONTEXT:  PL/pgSQL function datealilasfmtin(text,text) line 3 at RETURN
+SELECT CAST('2022-01-13'::text AS datealilas FORMAT 'YYYY-DD-MM'); -- error
+ERROR:  date/time field value out of range: "2022-01-13"
+CONTEXT:  PL/pgSQL function datealilasfmtin(text,text) line 3 at RETURN
 -- Check dependencies are established when creating a new type
 CREATE TYPE base_type;
 CREATE FUNCTION base_fn_in(cstring) RETURNS base_type AS 'boolin'
diff --git a/src/test/regress/expected/horology.out b/src/test/regress/expected/horology.out
index 32cf62b6741..93765733788 100644
--- a/src/test/regress/expected/horology.out
+++ b/src/test/regress/expected/horology.out
@@ -3324,72 +3324,152 @@ SELECT to_timestamp('2011-12-18 11:38 EST', 'YYYY-MM-DD HH12:MI TZ');
  Sun Dec 18 08:38:00 2011 PST
 (1 row)

 

 
 
+LINE 1: SELECT cast('2011-12-18 11:38 JUNK' as timestamptz format 'Y...
+LINE 1: SELECT cast('2011-12-18 11:38 ...' as timestamptz format 'YY...

+LINE 1: SELECT cast('2011-12-18 11:38 +xyz' as timestamptz format 'Y...
+LINE 1: SELECT cast('2011-12-18 11:38 +01:xyz' as timestamptz format...
 
@@ -3469,6 +3549,8 @@ SELECT i, to_timestamp('2018-11-02 12:34:56.123456', 'YYYY-MM-DD HH24:MI:SS.FF'
 
@@ -3486,18 +3568,36 @@ SELECT to_date('1 4 1902', 'Q MM YYYY');  -- Q is ignored
  04-01-1902
 (1 row)

 
+SELECT cast('2458872' as date format 'J');
+    date    
+------------
+ 01-23-2020
+(1 row)
+
 --
 -- Check handling of BC dates
 --
@@ -3833,12 +3933,24 @@ SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
  2012-12-12 12:00:00 PST
 (1 row)

@@ -3868,6 +3980,27 @@ SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
 
+SELECT cast('2012-12-12 12:00'::timestamptz as text format 'YYYY-MM-DD HH:MI:SS TZ'),
+        to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
+            text            |          to_char           
+----------------------------+----------------------------
+ 2012-12-12 12:00:00 -01:30 | 2012-12-12 12:00:00 -01:30
+(1 row)
+
+SELECT cast('2012-12-12 12:00'::date as text format 'YYYY-MM-DD HH:MI:SS TZ'),
+        to_char('2012-12-12 12:00'::date, 'YYYY-MM-DD HH:MI:SS TZ');
+ERROR:  cannot cast type date to text using CAST( ... AS ... FORMAT ...)
+LINE 1: ...LECT cast('2012-12-12 12:00'::date as text format 'YYYY-MM-D...
+                                                             ^
+SELECT cast('12:00'::time as text format 'HH:MI:SS'),
+        to_char('12:00'::time, 'HH:MI:SS');
+ERROR:  cannot cast type time without time zone to text using CAST( ... AS ... FORMAT ...)
+LINE 1: SELECT cast('12:00'::time as text format 'HH:MI:SS'),
+                                                 ^
+SELECT cast('2012-12-12 12:00'::timetz as text format 'YYYY-MM-DD HH:MI:SS TZ');
+ERROR:  cannot cast type time with time zone to text using CAST( ... AS ... FORMAT ...)
+LINE 1: ...CT cast('2012-12-12 12:00'::timetz as text format 'YYYY-MM-D...
+                                                             ^
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD SSSS');
      to_char      
 ------------------
diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out
index a16e3ccdb2e..4bcaaaf04a7 100644
--- a/src/test/regress/expected/interval.out
+++ b/src/test/regress/expected/interval.out
@@ -2263,12 +2263,24 @@ SELECT to_char('infinity'::interval, 'YYYY');
  
 (1 row)

 (1 row)

 

 

 

 

 

 

 

 
 
index 8fa0a6c47fb..b6ef7ccecb2 100644
 
 # ----------
index 00000000000..e991016f828
@@ -0,0 +1,123 @@
+select cast('hello'::text collate "C" as date format 'test' collate "POSIX"); -- error
+select cast('hello'::text collate "C" as date format 'test' collate "POSIX"); -- error
+create domain d2 as text check (value <> '125.80-');
+select cast(-125.8::numeric as text format '999D99S');
+select cast(-125.8::numeric as d2 format '999D99S');
+create temp table tcast0 as select '1'::text as a;
+select cast(a as d1 format 'YYYY-MM-DD') from tcast0;
+      --    cast(col3 as text format 'YYYY-MM-DD') as to_txt2,
+      --    cast(col5::int2 as text format '9.99EEEE') as to_txt4,
+drop domain d1, d2;
\ No newline at end of file

 
 
 -- backwards parsing
 
diff --git a/src/test/regress/sql/create_type.sql b/src/test/regress/sql/create_type.sql
index 8ccf35dbf1b..2e72589f62c 100644
--- a/src/test/regress/sql/create_type.sql
+++ b/src/test/regress/sql/create_type.sql
@@ -117,6 +117,17 @@ CREATE TYPE int42 (
    passedbyvalue
 );
 
+CREATE OR REPLACE FUNCTION int42_fmtout(int42, text)
+RETURNS TEXT AS
+$$
+   BEGIN
+      RETURN TO_CHAR($1::text::int, $2);
+   END
+$$
+LANGUAGE PLPGSQL STRICT IMMUTABLE;
+
+ALTER TYPE int42 SET (TYPFORMAT_OUT = int42_fmtout);
+
 CREATE TYPE text_w_default (
    internallength = variable,
    input = text_w_default_in,
@@ -131,6 +142,13 @@ INSERT INTO default_test DEFAULT VALUES;
 
 SELECT * FROM default_test;
 
+SELECT CAST('42'::text::int42 AS TEXT FORMAT 'EEEE9'); -- error
+SELECT CAST('42'::text::int42 AS TEXT FORMAT 'S999S'); -- error
+SELECT CAST('42'::text::int42 AS TEXT FORMAT '9.9V9'); -- error
+SELECT CAST('42'::text::int42 AS TEXT FORMAT 'RNEEEE'); -- error
+SELECT CAST('42'::text::int42 AS TEXT FORMAT '9.9.9'); -- error
+SELECT CAST(g::text::int42 AS TEXT FORMAT 'FM000') FROM (VALUES (-1), (0), (5), (100), (99)) s(g);
+
 -- We need a shell type to test some CREATE TYPE failure cases with
 CREATE TYPE bogus_type;
 
@@ -183,6 +201,46 @@ DROP TYPE default_test_row CASCADE;
 
 DROP TABLE default_test;
 
+-- CAST(expr as type FORMAT 'fmt') with user-defined types
+CREATE TYPE datealilas;
+
+CREATE FUNCTION datealilas_in(cstring)
+   RETURNS datealilas
+   AS 'date_in'
+   LANGUAGE internal STRICT IMMUTABLE;
+
+CREATE FUNCTION datealilas_out(datealilas)
+   RETURNS cstring
+   AS 'date_out'
+   LANGUAGE internal STRICT IMMUTABLE;
+
+CREATE TYPE datealilas (
+   internallength = 4,
+   input = datealilas_in,
+   output = datealilas_out,
+   alignment = int4,
+   passedbyvalue
+);
+CREATE OR REPLACE FUNCTION datealilasfmtin(TEXT, TEXT)
+RETURNS datealilas AS
+$$
+   BEGIN
+      RETURN TO_DATE($1, $2)::text::datealilas;
+   END
+$$
+LANGUAGE PLPGSQL STRICT IMMUTABLE;
+
+ALTER TYPE datealilas SET (TYPFORMAT_IN = datealilasfmtin);
+
+CREATE TEMP TABLE castfmt(a0 int2 default 2, a datealilas, b int default 3);
+INSERT INTO castfmt(a)
+   SELECT cast('2022-01-13' AS datealilas FORMAT 'YYYY-MM-DD')
+   UNION ALL
+   SELECT cast('2022-13-11' AS datealilas FORMAT 'YYYY-DD-MM');
+
+SELECT CAST('2022-01-13' AS datealilas FORMAT 'YYYY-DD-MM'); -- error
+SELECT CAST('2022-01-13'::text AS datealilas FORMAT 'YYYY-DD-MM'); -- error
+
 -- Check dependencies are established when creating a new type
 CREATE TYPE base_type;
 CREATE FUNCTION base_fn_in(cstring) RETURNS base_type AS 'boolin'
diff --git a/src/test/regress/sql/horology.sql b/src/test/regress/sql/horology.sql
index 8978249a5dc..714e375b088 100644
--- a/src/test/regress/sql/horology.sql
+++ b/src/test/regress/sql/horology.sql
@@ -539,21 +539,46 @@ SELECT to_timestamp('2011-12-18 11:38 -05:20', 'YYYY-MM-DD HH12:MI TZH:TZM');
 SELECT to_timestamp('2011-12-18 11:38 20',     'YYYY-MM-DD HH12:MI TZM');

---

From: Robert Haas <[email-masked]>
Date: Mon, 29 Jun 2026 14:13:59 -0400

> I have attached the full patch series for review.  Comments are welcome.

This looks good!

I haven't done a detailed review, but I skimmed over all of the
patches and I like the direction. I think we should consider calling
it a "format cast" instead of a "formatter". It would avoid needing to
make "formatter" a parser keyword.

---

From: Zsolt Parragi <[email-masked]>
Date: Mon, 29 Jun 2026 14:49:49 -0700

The latest patch fail make check in my testrun:

not ok 166   + collate.linux.utf8

With the condition written like this a non strict formatter function
ignores nulls, and possibly crashes the server.

Shouldn't pg_dump also handle the new TYPEFORMAT_IN? Currently it's
lost during a dump-restore.

---

From: Haibo Yan <[email-masked]>
Date: Tue, 30 Jun 2026 10:39:10 -0700

> make "formatter" a parser keyword.

Hi Robert,

Thanks for looking at the patches.
I liked your suggestion to call these “format casts” rather than
“formatters”, so I changed the series in that direction.  The DDL is
now:
------------------------------------------------------
CREATE FORMAT CAST (...)
DROP FORMAT CAST (...)
------------------------------------------------------
rather than CREATE/DROP FORMATTER, so the patch no longer adds
FORMATTER as a keyword.
I also renamed the related catalog and node names, so this now uses
pg_format_cast and CoerceViaFormatCast.  The
pg_dump/object-address/comment/extension code and the regression tests
have been updated to use the new terminology as well.
The rest of the design is the same as before: format casts are still
catalog-driven, and both built-in and user-defined cases go through
the same lookup path.

Attached is the updated v2 series.

Regards,
Haibo

---

From: jian he <[email-masked]>
Date: Wed, 1 Jul 2026 21:44:37 +0800

> “formatters”, so I changed the series in that direction.  The DDL is

In an earlier thread [1], [2], I proposed introducing:

CREATE CAST (source_type AS target_type)
    WITH [SAFE] FUNCTION function_name [ (argument_type [, ...]) ]

That drew some pushback over non-standard conformance.
Here, we are introducing

CREATE FORMAT CAST (source_type AS target_type)
    WITH FUNCTION function_name [ (argument_type [, ...]) ]

This syntax is quite close to CREATE CAST.
per https://www.postgresql.org/docs/devel/sql-createcast.html
CREATE FORMAT CAST would be an extension to the standard, though we
already have non-standard precedent: AS IMPLICIT.

Before proceeding, do we need to reach consensus on the syntax itself?

Does the following alternative syntax make sense for CAST FORMAT?

CREATE CAST (source_type AS target_type)
    WITH FUNCTION function_name [ (argument_type [, ...]) ]
    AS FORMAT
----------------------------------------------------------------------------------------
As I mentioned previously [3], there's an open question around this case:

does this mean the format template ('YYYY-DD-MM') should be applied to each
element (2022-01-01, 2022-21-01) individually via the cast format function?

This matters because it affects check_format_cast_function's handling of
argument_type. According to create_format_cast.sgml, the format function should
take exactly two arguments, with the last one being text. If array types are to
be supported, then argument_type would need to become either (source_type,
format_text) or (source_type, oid, int, format_text).

Sure, we can support formatted casts for array types later, but we still need to
think through the array-type formatting design now, so that the cast format
function's argument types won't need to change later to accommodate arrays.

[1]: https://www.postgresql.org/message-id/5ae9578e-f25e-49c5-97ab-ad27bc2050b5%40eisentraut.org
[2]: https://www.postgresql.org/message-id/attachment/192833/v23-0023-error-safe-for-user-defined-CREATE-CAST.patch
[3]: https://www.postgresql.org/message-id/CACJufxGVuCM4XFGqaqiV-VOEiqMtCZ3%2BT-%2BSrG-y6kqdLo1ZqA%40mail.gmail.com

---

From: Haibo Yan <[email-masked]>
Date: Wed, 1 Jul 2026 09:45:14 -0700

Hi Jian,

Thanks for looking at this.

> Before proceeding, do we need to reach consensus on the syntax itself?

yes, I agree that we should get agreement on the DDL syntax before going
much further.

I see the analogy with the earlier SAFE discussion, but I think this case is a
little different.  This patch creates a separate object kind rather than adding
another option to ordinary CREATE CAST

> Does the following alternative syntax make sense for CAST FORMAT?

My reason for preferring CREATE FORMAT CAST is that a format cast is not
really an ordinary cast with one more attribute.  Ordinary casts live in
pg_cast and are tied to things like cast context and cast method.  A format
cast is only considered for

CAST(expr AS type FORMAT fmt)

and not for ordinary casts without a FORMAT clause, assignment casts, or
implicit casts.  It also always needs a function whose second argument is the
FORMAT expression, not the destination typmod argument used by ordinary cast
functions.
So I think

CREATE CAST (...) WITH FUNCTION ... AS FORMAT

would make this look more like a pg_cast entry than it really is.  My
preference is to keep format casts separate from ordinary casts, both in the
catalog and in the DDL syntax.  Of course, if others prefer a different
spelling, we should settle that in the thread.

>     WITH FUNCTION function_name [ (argument_type [, ...]) ]

I don’t feel this patch should imply automatic element-wise formatting.
The current design is an exact source/target lookup in pg_format_cast.  So in
this case, after resolving the source expression, the lookup would be for a
format cast from the source type to date[] itself.  It would not automatically
fall back to a text -> date format cast and apply it to each array element.

That does not rule out element-wise array support later.  If we want that, one
possible design would be to mirror the existing array-coercion approach: look
up the scalar format cast for the element types and wrap it in an array-mapping
expression.  In that design, the scalar format-cast function signature would
still remain

function(source_element_type, text) returns target_element_type

and the array expression node would be responsible for iterating over the
array.

> function's argument types won't need to change later to accommodate arrays.

I think that should be a separate design decision.  The important point for
this patch is that it does not define automatic element-wise formatting, and I
don’t think we need to change the basic scalar function signature now in order
to leave room for such a design.

If people agree with this interpretation, I can add a sentence to the
documentation saying that format cast lookup is by exact source and target
type, and that the FORMAT clause is not automatically applied to array
elements.

Regards,
Haibo

---

From: "David G. Johnston" <[email-masked]>
Date: Wed, 1 Jul 2026 13:58:55 -0700

> would make this look more like a pg_cast entry than it really is.

I think the fact the standard put this inside the 'cast(...)' means it's
quite reasonable to consider the aspect part of a cast definition as
opposed to something wholly different.

When we issue "create table" both pg_class and pg_attribute are modified.
It seems quite reasonable that executing "create cast" causes both pg_cast
and pg_cast_format to be populated.

With two separate commands a cast could exist on pg_cast_format that
doesn't exist in pg_cast.  I'm unsure whether this is allowed or
desired...but seems a bit unexpected to me at least.

> >     WITH FUNCTION function_name [ (argument_type [, ...]) ]

Yes, the internal array formatting structure used by PostgreSQL isn't
(needn't be) user-configurable; the useful thing for the format to apply to
is the value of the elements of the array.

IMO the only change the addition of a format clause should make when
performing the cast is allowing a cast that would fail due to a syntax
error to succeed.

postgres=# select
cast(cast('{"2025-01-01","2026-02-02","2027-03-03"}'::text as
text[]) as date[]);
                date
------------------------------------
 {2025-01-01,2026-02-02,2027-03-03}
(1 row)

postgres=# select
cast(cast('{"2025T01-01","2026T02-02","2027T03-03"}'::text as
text[]) as date[]);
ERROR:  invalid input syntax for type date: "2025T01-01"

A format clause would make the second query not error with an appropriate
format specification - in which case it then behaves identically to the
first query.

If the SQL Standard somehow contradicts this intent I'd be curious to
understand what it does intend.

David J.

---

From: Haibo Yan <[email-masked]>
Date: Wed, 1 Jul 2026 17:00:58 -0700

Hi David,

Thanks, this is helpful.

On Wed, Jul 1, 2026 at 1:59 PM David G. Johnston
<[email-masked]> wrote:
> I think the fact the standard put this inside the 'cast(...)' means it's quite reasonable to consider the aspect part of a cast definition as opposed to something wholly different.
>
> When we issue "create table" both pg_class and pg_attribute are modified.  It seems quite reasonable that executing "create cast" causes both pg_cast and pg_cast_format to be populated.
>
> With two separate commands a cast could exist on pg_cast_format that doesn't exist in pg_cast.  I'm unsure whether this is allowed or desired...but seems a bit unexpected to me at least.
>

I see what you mean.  I had been thinking of format casts as separate because
they are only considered for

and not for ordinary casts without a FORMAT clause.  That is why the patch
currently uses a separate catalog and a separate CREATE FORMAT CAST command.

But your pg_class / pg_attribute analogy is a good point.  A separate
catalog does not necessarily imply a separate DDL command, and it may be
surprising if a format-cast entry can exist without any corresponding ordinary
cast relationship.

Maybe the question for the thread is whether a format cast should be modeled as
a separate object, or as additional information associated with an ordinary
cast.  The latter could still use a separate catalog internally, but be created
through some form of CREATE CAST.

> Yes, the internal array formatting structure used by PostgreSQL isn't (needn't be) user-configurable; the useful thing for the format to apply to is the value of the elements of the array.
>
> IMO the only change the addition of a format clause should make when performing the cast is allowing a cast that would fail due to a syntax error to succeed.
>
> postgres=# select cast(cast('{"2025-01-01","2026-02-02","2027-03-03"}'::text as
> text[]) as date[]);
>                 date
> postgres=# select cast(cast('{"2025T01-01","2026T02-02","2027T03-03"}'::text as
> text[]) as date[]);

If PostgreSQL would normally cast an array by applying the element cast to each
element, then it is natural to expect the FORMAT clause to affect that element
cast rather than the array container syntax.

CAST('{2025T01-01,2026T02-02}' AS date[] FORMAT 'YYYY"T"MM-DD')

could reasonably mean: parse the array structure as usual, and apply the
formatted text -> date conversion to each element.

The current patch does not implement that.  It does an exact source/target
lookup in pg_format_cast, so an array target would currently require a format
cast for the array type itself.  But I agree that this may not be the behavior
users would expect, especially given the existing array coercion behavior.

If we decide that element-wise array formatting is required, one possible
implementation would be to keep the scalar format-cast function signature as

and add a format-aware path in the array coercion logic.  The array expression
would iterate over elements and pass the same FORMAT expression to the scalar
format cast.  In that design, the array container itself would not need a
separate format-cast entry.

>
> A format clause would make the second query not error with an appropriate format specification - in which case it then behaves identically to the first query.

I agree that this is the important case for input parsing.  I think FORMAT is
also useful in the output direction, though.  For example,

CAST(date '2026-07-01' AS text FORMAT 'YYYY/MM/DD')

would succeed without FORMAT, but the FORMAT clause controls the textual
representation.

>
> If the SQL Standard somehow contradicts this intent I'd be curious to understand what it does intend.
>
> David J.
>

So I think we have two related design questions to settle:

whether format casts are separate objects or additional information
associated with ordinary casts;
whether element-wise array formatting should be part of the initial design.

Regards,
Haibo

---

From: jian he <[email-masked]>
Date: Thu, 2 Jul 2026 11:23:10 +0800

> separate format-cast entry.

In some cases, ArrayCoerceExpr might help, but not here.

CAST('{2025T01-01,2026T02-02}'::TEXT AS date[] FORMAT 'YYYY"T"MM-DD')

parse the array structure as usual, which array_in do all the heavy work.
arrray_in(cstring, oid, integer).
But once array_in is finished, we already get a date[] datum, then
FORMAT is no longer needed.
I also mentioned [1] that add a text argument to array_in is not possible.

I guess not supporting arrays in the initial patch should be fine,
since not all CASTs support CAST FORMAT.
That raises another question: for user-defined CREATE FORMAT CAST,
should we ban anyarray as the first argument type?
If we don't, will it conflict with array support once we implement it later?

[1]: https://www.postgresql.org/message-id/CACJufxGVuCM4XFGqaqiV-VOEiqMtCZ3%2BT-%2BSrG-y6kqdLo1ZqA%40mail.gmail.com

---

From: "David G. Johnston" <[email-masked]>
Date: Wed, 1 Jul 2026 21:08:13 -0700

> since not all CASTs support CAST FORMAT.

Personally I’d require solving the entire problem before putting any of it
in.  This doesn’t seem like a feature that needs two years to implement.
And it isn’t like we don’t have usable tools while we wait.  This is for
standards conformance more than anything.

David J.

---

From: Haibo Yan <[email-masked]>
Date: Wed, 1 Jul 2026 21:14:42 -0700

> I also mentioned [1] that add a text argument to array_in is not possible.

Thanks, I agree that ArrayCoerceExpr is not enough for that case.
the normal array input path would already have to parse the elements as dates
while constructing the date[] value, so the FORMAT clause cannot simply be
applied after array_in has returned.

> since not all CASTs support CAST FORMAT.

I think that means array support needs a separate design.  For the initial
patch, it seems better not to support formatted casts involving array types at
all.

> should we ban anyarray as the first argument type?

yes, I think polymorphic pseudo-types should becrejected.  The current patch
already rejects pseudo-types, so anyarray should not be accepted.  But to
avoid future ambiguity, I think we should also reject concrete array source or
target types for now.

Otherwise, a future element-wise array design would have to choose between an
exact array-level format cast and an element-wise scalar format cast.  I don’t
think we want both meanings.  If array support is added later, my preference
would be for it to mean element-wise application of a scalar format cast, with
the normal array machinery still handling the array container syntax.

> If we don't, will it conflict with array support once we implement it later?

So the initial rule could simply be: no polymorphic pseudo-types, and no array
source or target types for `CREATE FORMAT CAST`.  If people agree, I can update
the patch and documentation that way.

Regards,
Haibo

---

From: Peter Eisentraut <[email-masked]>
Date: Thu, 2 Jul 2026 17:13:50 +0200

> project, and I think that work needs to be done first.

I don't really understand the purpose of this feature to begin with. 
You can already call the formatting functions directly.  Does this cast 
syntax offer anything on top of that?

---

From: Robert Haas <[email-masked]>
Date: Thu, 2 Jul 2026 16:24:32 -0400

On Thu, Jul 2, 2026 at 11:13 AM Peter Eisentraut <[email-masked]> wrote
> syntax offer anything on top of that?

Standards compliance, maybe? I don't know what else there could be.

---

From: Robert Haas <[email-masked]>
Date: Thu, 2 Jul 2026 16:37:08 -0400

> When we issue "create table" both pg_class and pg_attribute are modified.  It seems quite reasonable that executing "create cast" causes both pg_cast and pg_cast_format to be populated.

I don't see how this would work. The existence of a regular cast
doesn't mean there has to be a format cast, and the existence of a
format cast doesn't mean there has to be a regular cast. Expressions
that use a format cast need to depend on the pg_format_cast entry, and
expressions that use a regular cast need to depend on the pg_cast
entry.

I mean, I'm not saying something else is totally impossible. You could
for example create the pg_cast entry always and make it some kind of
dummy entry when there's only a format cast. Then all casts could
depend on the pg_cast entry. But then we'd still have to have ALTER
CAST commands that modify the format-cast part of the object and the
non-format cast part of the object separately. That seems extremely
unpleasant to sort out, especially since right now a cast can't be
modified after it's created, and changing that might have security
implications. But even apart from that it doesn't seems like it will
make for a very awkward user experience.

I agree with Peter that I'm not really sure why we want to implement
this in the first place; it doesn't seem to add any value over just
having functions. The above is just a discussion about what we should
do (or what I think we should do) if we do want to have it.

---

From: "David G. Johnston" <[email-masked]>
Date: Thu, 2 Jul 2026 13:51:13 -0700

On Thursday, July 2, 2026, Robert Haas <[email-masked]> wrote:

> > When we issue "create table" both pg_class and pg_attribute are
> modified.  It seems quite reasonable that executing "create cast" causes
> both pg_cast and pg_cast_format to be populated.
>
> doesn't mean there has to be a format cast,

Agree

>
> and the existence of a
> format cast doesn't mean there has to be a regular cast.

Disagree

> But then we'd still have to have ALTER
>
> implications.

Then we don;t need the alter to be able to modify the non-format portion of
the cast.  I do imagine that the formatter is an optional component of a
cast.  So “alter cast … {attach|detach} formatter …”.  We don’t even need a
create command component though seems nice to keep it for convenience.

I don’t have any qualms adding this so as long as fits into the existing
system cleanly.  The array example does actually provide new ease-of-use
for an admittedly possibly rare use case.

David J.

---

From: Robert Haas <[email-masked]>
Date: Thu, 2 Jul 2026 17:16:52 -0400

>> format cast doesn't mean there has to be a regular cast.

OK, but on what basis?

If I want 'whatever'::thistype::thattype to fail and CAST(thistype AS
thattype FORMAT 'bumble') to succeed, then I need there to be a format
cast but not a regular cast. Apparently, you'd like to arbitrarily
disallow that case, but I can't see any logical reason for such a
prohibition. Essentially we'd be forcing the user to create a regular
cast that they don't want in order to be able to create a format cast
that they do want. If there were some reason why we had to couple the
two things together that way, that would be one thing, but as far as I
can see, there isn't.

---

From: "David G. Johnston" <[email-masked]>
Date: Thu, 2 Jul 2026 14:52:02 -0700

On Thursday, July 2, 2026, Robert Haas <[email-masked]> wrote:

> cast but not a regular cast

There is no wanting the formatless-variant to fail.  There is recognition
that sometime the formatless variant might fail with a syntax error and we
have a way to pass a format to make the cast work.

If there is no cast from thistype to thattype both will fail with cast not
found errors.  Once a cast exists the first one could pass or fail
depending on the content being casted.  If the content has an internal
structure/syntax the failure mode would then be a syntax error.  Overcoming
a syntax error is done by specifying a format.  But a format does you no
good if there isn’t some cast pathway already available to use it.  IOW a
format should never be required - some default exists, like for dates
today, that allows the non-format cast to work.  We just need some way to
pass in a format to that cast function if one is specified.

For me the presence of the word cast in the syntax drives this way of
thinking about the problem/design.  I’d rather just stick with our
polymorphic to_char functions if we want some way of outputting text with a
format at without having to call doing so a cast.

David J.

---

From: Robert Haas <[email-masked]>
Date: Thu, 2 Jul 2026 18:23:43 -0400

On Thu, Jul 2, 2026 at 5:52 PM David G. Johnston
<[email-masked]> wrote:
> There is no wanting the formatless-variant to fail.  There is recognition that sometime the formatless variant might fail with a syntax error and we have a way to pass a format to make the cast work.
 >
> If there is no cast from thistype to thattype both will fail with cast not found errors.  Once a cast exists the first one could pass or fail depending on the content being casted.  If the content has an internal structure/syntax the failure mode would then be a syntax error.  Overcoming a syntax error is done by specifying a format.  But a format does you no good if there isn’t some cast pathway already available to use it.  IOW a format should never be required - some default exists, like for dates today, that allows the non-format cast to work.  We just need some way to pass in a format to that cast function if one is specified.
>
> For me the presence of the word cast in the syntax drives this way of thinking about the problem/design.  I’d rather just stick with our polymorphic to_char functions if we want some way of outputting text with a format at without having to call doing so a cast.

I don't really know how to have a productive conversation about this
at this point. I agree that we might be better off just sticking with
our polymorphic to_char functions. But if we want to implement
CAST(... FORMAT ...) I do not understand how what you've written above
amounts to a coherent design proposal; it just doesn't make any sense
to me. If you spell it out with specific syntax, specific catalog
changes, and specific ways that things would work, then maybe I would
have an opinion, but right now all I can really say is that I disagree
strongly with the idea of trying to treat CAST(a AS b) and CAST(a AS b
FORMAT c) as two variants of the same thing just because they both
have CAST in the name. It matters whether they actually *do* the same
thing -- whether they could reasonably be implemented by the same code
-- and I think they can't.

---

From: "David G. Johnston" <[email-masked]>
Date: Thu, 2 Jul 2026 21:08:51 -0700

On Thu, Jul 2, 2026 at 3:23 PM Robert Haas <[email-masked]> wrote:

> > There is no wanting the formatless-variant to fail.  There is
> recognition that sometime the formatless variant might fail with a syntax
> error and we have a way to pass a format to make the cast work.
>  >
> > If there is no cast from thistype to thattype both will fail with cast
> not found errors.  Once a cast exists the first one could pass or fail
> a syntax error is done by specifying a format.
>

> at this point.
I'm not sure how to make the design less problematic: format is optional,
and for casts that need a format to succeed they provide a default.

But yes, without concrete userspace examples to reason with, this approach
of only being concerned with the userspace perspective leaves much to
resolve when it comes to actual implementation.  But I also don't generally
like the idea of choosing how we define the userspace based upon how easy
it might be to implement.  Especially if ostensibly the reference design is
coming from the SQL Standard.

But yeah, it is apparent to me I need to try and use the language of
implementation details and not stay conceptual.

David J.

---

From: Robert Haas <[email-masked]>
Date: Fri, 3 Jul 2026 05:49:46 -0400

On Fri, Jul 3, 2026 at 12:09 AM David G. Johnston
<[email-masked]> wrote:
> I'm not sure how to make the design less problematic: format is optional, and for casts that need a format to succeed they provide a default.

But that's not how casts work today. some_date::text shares neither
code nor behavior with to_char(some_date, 'some_built_in_constant').
It's completely separate. We're not going to rethink the basic design
of casts for the sake of CAST(...FORMAT...).

---

From: jian he <[email-masked]>
Date: Sat, 4 Jul 2026 09:09:22 +0800

> syntax offer anything on top of that?

https://www.postgresql.org/message-id/762ae707-7fdc-43d8-a77a-3a10d12ce21d%40postgresfriends.org

After this implemented, we can begin fully supporting:

<cast specification> ::=
    CAST <left paren>
        <cast operand> AS <cast target>
        [ FORMAT <cast template> ]
        [ <cast error behavior> ON CONVERSION ERROR ]
        <right paren>

---

From: Haibo Yan <[email-masked]>
Date: Mon, 6 Jul 2026 10:14:02 -0700

> syntax offer anything on top of that?

I agree that the feature would be hard to justify if it only covered
to_date() and to_char() under another spelling.

The cases where I think a generic mechanism could be useful are
broader than that.  For example:

    text <-> date/time        using datetime templates
    text <-> numeric          using number templates
    text <-> bytea            using hex/base64/escape
    text/bytea <-> extension types
                          using external representations such as
                          WKT/WKB/GeoJSON for geometry types

The first two are close to the existing formatting functions.  The bytea
case is currently handled by encode()/decode(), but it has the same general
shape: a conversion between a typed value and a textual representation,
controlled by a format argument.

For extension types, the case may be stronger.  PostGIS, for example, has
several external representations for geometry values: WKT, WKB, EWKT, GeoJSON,
and so on.  A catalog-driven format-cast mechanism would let an extension
register those conversions without the parser knowing any of the function
names.

Other types might also have useful representation variants, such as UUIDs or
network/address types, but the point is that this is not limited to the current
date/time functions.

So I agree that this should not be sold as just syntax sugar for the existing
date/time functions.  If it is worth doing, the value is the generic
infrastructure for FORMAT-aware conversions, including extension types, and not
just the initial built-in datetime cases.

Regards,
Haibo

---

From: Robert Haas <[email-masked]>
Date: Mon, 6 Jul 2026 14:18:04 -0400

> controlled by a format argument.

Sure, but in each case, the conversion function in question could just
be called directly.

It's also noteworthy that in each of these examples, one of the two
types is text, or maybe bytea, which, again, really makes you wonder
why this is designed as a cast-like mechanism. I guess you could have
CAST(now() to 'integer' FORMAT 'YYYY') but that seems fairly silly, so
I really don't understand why this is designed to take two arbitrary
types.

Aside from standards-compliance, the only value I can see in a feature
like this is if it's helpful to be able to name the type rather than
naming the conversion function. For instance, imagine that I could use
the same format string for conversions between a bunch of different
types. Then potentially it's handy to be able to write CAST(something
AS text FORMAT 'the string I always use') and you don't need to think
about which source type you've got at this particular call site. But
there are multiple problems with that idea. First, it seems unlikely
that the format string would be generic in that way. And second, we
already support function overloading, so you could just pick some
function name (like to_char!) and overload it to do the same thing.
Picking a function name also has the advantage of not privileging one
particular way of doing a conversion between two types over all
others, whereas this FORMAT CAST proposal requires you to decide on
one canonical method of formatting type X as type Y.

(Regular casts have this problem, too, to an extent.)

---

From: Haibo Yan <[email-masked]>
Date: Mon, 6 Jul 2026 13:15:28 -0700

Hi Robert,

Thanks, I think that is a fair criticism.

The examples I listed are mostly conversions between a typed value and a
textual or binary representation, not arbitrary conversions between unrelated
types.  So I agree that the current pg_format_cast design may be too broad if
it allows arbitrary source/target pairs.

I also see your point about function overloading.  If the only goal is generic
dispatch, an overloaded function name already provides that.  So the value of
CAST(... FORMAT ...) would need to be narrower than that: a standard syntax
for a type’s formatted external representation, not a general replacement for
named conversion functions.

Maybe the better boundary is that a formatted conversion should be between a
data type and an external representation type, probably text or bytea.

That would allow cases such as text -> date, numeric -> text, and
bytea -> some_type, and also the existing-style text <-> bytea encoding
case.  But it should not become a generic text -> text or bytea -> bytea
transformation mechanism, nor a second cast system for arbitrary type pairs.

Regards,
Haibo

---

From: Robert Haas <[email-masked]>
Date: Mon, 6 Jul 2026 16:27:41 -0400

> it allows arbitrary source/target pairs.

Well, I don't think the issue here is really about pg_format_cast. If
the syntax supports arbitrary pairs of types, the catalog
representation should do. But there's also the question of whether we
want to support the syntax.

> transformation mechanism, nor a second cast system for arbitrary type pairs.

But if this is in the SQL standard -- is it? -- then that ship has
already sailed. We're just left to wonder why they did it like that...

---

From: Haibo Yan <[email-masked]>
Date: Mon, 6 Jul 2026 20:56:44 -0700

> already sailed. We're just left to wonder why they did it like that...

Hi all,

I looked more closely at the standard and at the concerns raised here.

I now think my v2 patch is too broad.  It tries to make CAST(... FORMAT ...)
a generic user-defined format-cast mechanism, but SQL feature T839 is much
narrower: formatted casts between datetime types and character strings.

So I think the next version should move back closer to Jian’s original scope:
support T839 first, rather than introduce pg_format_cast and
CREATE FORMAT CAST.  I still think we should avoid ad-hoc parser rewrites to
specific function names, but the feature scope should be the standard
datetime/string cases, not arbitrary source/target pairs.

I also think it would be useful to retitle this thread around the standard
feature, for example:

    Support SQL feature T839: formatted casts between datetime and
character strings

That seems to describe the real target better than a generic format-cast
facility.

Regards,
Haibo

---

From: Robert Haas <[email-masked]>
Date: Tue, 7 Jul 2026 08:07:14 -0400

> That seems to describe the real target better than a generic format-cast

My current point of view is that your patch does exactly the right
thing but it's not clear whether we want it. I mean, if we eventually
want to support this feature with arbitrary types, there is no reason
not to do so from the start. Or at least, none that I can see. But
that does not address the question of whether we want the feature in
the first place, which is debatable.

---

From: Peter Eisentraut <[email-masked]>
Date: Thu, 9 Jul 2026 09:18:16 +0200

>          [ <cast error behavior> ON CONVERSION ERROR ]

The ON CONVERSION ERROR feature is independent of the FORMAT feature, 
isn't it?

---

From: Peter Eisentraut <[email-masked]>
Date: Thu, 9 Jul 2026 09:20:29 +0200

>                            WKT/WKB/GeoJSON for geometry types
One problem I see is that this would dictate that there is only one 
possible format language for each of these conversions.