From 82658dcbbd4a45844ae670e27cf57faf1ab1db7f Mon Sep 17 00:00:00 2001
From: Haibo Yan <tristan.yim@gmail.com>
Date: Wed, 24 Jun 2026 12:24:03 -0700
Subject: [PATCH 1/4] Add parser support for CAST ... FORMAT

Add grammar and raw parse-tree support for

    CAST(expr AS type FORMAT format_expr)

by storing the FORMAT expression in TypeCast.  Ordinary casts leave the
new field unset.

Parse analysis still rejects formatted casts in this patch.  Format cast
resolution and execution are added later, so this patch only establishes
the syntax and parse-node representation.
---
 src/backend/nodes/nodeFuncs.c             |  2 ++
 src/backend/parser/gram.y                 |  7 +++++++
 src/backend/parser/parse_expr.c           | 13 +++++++++++++
 src/include/nodes/parsenodes.h            |  1 +
 src/test/regress/expected/expressions.out | 23 +++++++++++++++++++++++
 src/test/regress/sql/expressions.sql      | 15 +++++++++++++++
 6 files changed, 61 insertions(+)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 2a2e00b372e..66495546179 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4581,6 +4581,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(tc->typeName))
 					return true;
+				if (WALK(tc->format))
+					return true;
 			}
 			break;
 		case T_CollateClause:
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ff4e1388c55..ef4881efc81 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -16787,6 +16787,13 @@ func_expr_common_subexpr:
 				}
 			| CAST '(' a_expr AS Typename ')'
 				{ $$ = makeTypeCast($3, $5, @1); }
+			| CAST '(' a_expr AS Typename FORMAT a_expr ')'
+				{
+					TypeCast   *n = (TypeCast *) makeTypeCast($3, $5, @1);
+
+					n->format = $7;
+					$$ = (Node *) n;
+				}
 			| EXTRACT '(' extract_list ')'
 				{
 					$$ = (Node *) makeFuncCall(SystemFuncName("extract"),
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 9adc9d4c0f6..ba6480acf12 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -2743,6 +2743,19 @@ transformTypeCast(ParseState *pstate, TypeCast *tc)
 	int32		targetTypmod;
 	int			location;
 
+	/*
+	 * Formatted casts (CAST(expr AS type FORMAT format_expr)) are parsed and
+	 * represented in the parse tree, but format cast resolution is not yet
+	 * implemented.  Reject such casts here rather than silently ignoring the
+	 * FORMAT clause.
+	 */
+	if (tc->format != NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("formatted casts are not implemented yet"),
+				 errdetail("No format cast resolution mechanism is available."),
+				 parser_errposition(pstate, exprLocation(tc->format))));
+
 	/* Look up the type name first */
 	typenameTypeIdAndMod(pstate, tc->typeName, &targetType, &targetTypmod);
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 4133c404a6b..759c6bfae54 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;			/* FORMAT expression, or NULL if none */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } TypeCast;
 
diff --git a/src/test/regress/expected/expressions.out b/src/test/regress/expected/expressions.out
index 730f7bc7eba..d39cd1b7fbd 100644
--- a/src/test/regress/expected/expressions.out
+++ b/src/test/regress/expected/expressions.out
@@ -561,3 +561,26 @@ from inttest;
 (3 rows)
 
 rollback;
+--
+-- CAST(expr AS type FORMAT format_expr)
+--
+-- The FORMAT clause is parsed and stored, but format cast resolution is not
+-- implemented yet, so parse analysis must reject it (not ignore it).
+-- basic form
+SELECT CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD');
+ERROR:  formatted casts are not implemented yet
+LINE 1: SELECT CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD');
+                                                ^
+DETAIL:  No format cast resolution mechanism is available.
+-- the format may be a general expression, not just a string literal
+SELECT CAST('2026-06-24' AS date FORMAT 'YYYY' || '-MM-DD');
+ERROR:  formatted casts are not implemented yet
+LINE 1: SELECT CAST('2026-06-24' AS date FORMAT 'YYYY' || '-MM-DD');
+                                                ^
+DETAIL:  No format cast resolution mechanism is available.
+-- a no-op-looking cast must still be rejected, not relabeled away
+SELECT CAST('abc'::text AS text FORMAT 'whatever');
+ERROR:  formatted casts are not implemented yet
+LINE 1: SELECT CAST('abc'::text AS text FORMAT 'whatever');
+                                               ^
+DETAIL:  No format cast resolution mechanism is available.
diff --git a/src/test/regress/sql/expressions.sql b/src/test/regress/sql/expressions.sql
index 3b3048f9731..36eae8c3f04 100644
--- a/src/test/regress/sql/expressions.sql
+++ b/src/test/regress/sql/expressions.sql
@@ -301,3 +301,18 @@ select
 from inttest;
 
 rollback;
+
+--
+-- CAST(expr AS type FORMAT format_expr)
+--
+-- The FORMAT clause is parsed and stored, but format cast resolution is not
+-- implemented yet, so parse analysis must reject it (not ignore it).
+
+-- basic form
+SELECT CAST('2026-06-24' AS date FORMAT 'YYYY-MM-DD');
+
+-- the format may be a general expression, not just a string literal
+SELECT CAST('2026-06-24' AS date FORMAT 'YYYY' || '-MM-DD');
+
+-- a no-op-looking cast must still be rejected, not relabeled away
+SELECT CAST('abc'::text AS text FORMAT 'whatever');
-- 
2.54.0

