From 8bb73db3ae0fdec4c0b53d0c3b67a2c3340d91de Mon Sep 17 00:00:00 2001 From: Haibo Yan Date: Wed, 24 Jun 2026 12:24:03 -0700 Subject: [PATCH] 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. Formatter 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..1729ba56013 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 formatter 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 formatter 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..b2d71dca4fa 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 formatter 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 formatter 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 formatter 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 formatter resolution mechanism is available. diff --git a/src/test/regress/sql/expressions.sql b/src/test/regress/sql/expressions.sql index 3b3048f9731..fb05990ed2d 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 formatter 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